Skip to content
Merged
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
10 changes: 4 additions & 6 deletions packages/opencode/src/server/routes/instance/httpapi/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Config } from "@/config/config"
import { Provider } from "@/provider/provider"
import * as InstanceState from "@/effect/instance-state"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
import { markInstanceForDisposal } from "./lifecycle"
Expand Down Expand Up @@ -57,7 +57,7 @@ export const ConfigApi = HttpApi.make("config")
}),
)

export const configHandlers = Layer.unwrap(
export const configHandlers = HttpApiBuilder.group(ConfigApi, "config", (handlers) =>
Effect.gen(function* () {
const providerSvc = yield* Provider.Service
const configSvc = yield* Config.Service
Expand All @@ -80,8 +80,6 @@ export const configHandlers = Layer.unwrap(
}
})

return HttpApiBuilder.group(ConfigApi, "config", (handlers) =>
handlers.handle("get", get).handle("update", update).handle("providers", providers),
)
return handlers.handle("get", get).handle("update", update).handle("providers", providers)
}),
).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer))
)
32 changes: 30 additions & 2 deletions packages/opencode/src/server/routes/instance/httpapi/control.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Auth } from "@/auth"
import { ProviderID } from "@/provider/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

const AuthParams = Schema.Struct({
providerID: ProviderID,
Expand Down Expand Up @@ -69,3 +70,30 @@ export const ControlApi = HttpApi.make("control").add(
)
.annotateMerge(OpenApi.annotations({ title: "control", description: "Control plane routes." })),
)

export const controlHandlers = HttpApiBuilder.group(ControlApi, "control", (handlers) =>
Effect.gen(function* () {
const auth = yield* Auth.Service

const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: {
params: { providerID: ProviderID }
payload: Auth.Info
}) {
yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie)
return true
})

const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) {
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
return true
})

const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
const logger = Log.create({ service: ctx.payload.service })
logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
return true
})

return handlers.handle("authSet", authSet).handle("authRemove", authRemove).handle("log", log)
}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Session } from "@/session/session"
import { ToolRegistry } from "@/tool/registry"
import * as EffectZod from "@/util/effect-zod"
import { Worktree } from "@/worktree"
import { Effect, Layer, Option, Schema, SchemaGetter } from "effect"
import { Effect, Option, Schema, SchemaGetter } from "effect"
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
Expand Down Expand Up @@ -210,7 +210,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
)

export const experimentalHandlers = Layer.unwrap(
export const experimentalHandlers = HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) =>
Effect.gen(function* () {
const account = yield* Account.Service
const agents = yield* Agent.Service
Expand Down Expand Up @@ -335,27 +335,17 @@ export const experimentalHandlers = Layer.unwrap(
return yield* mcp.resources()
})

return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) =>
handlers
.handle("console", getConsole)
.handle("consoleOrgs", listConsoleOrgs)
.handle("consoleSwitch", switchConsole)
.handle("tool", tool)
.handle("toolIDs", toolIDs)
.handle("worktree", worktree)
.handle("worktreeCreate", worktreeCreate)
.handle("worktreeRemove", worktreeRemove)
.handle("worktreeReset", worktreeReset)
.handle("session", session)
.handle("resource", resource),
)
return handlers
.handle("console", getConsole)
.handle("consoleOrgs", listConsoleOrgs)
.handle("consoleSwitch", switchConsole)
.handle("tool", tool)
.handle("toolIDs", toolIDs)
.handle("worktree", worktree)
.handle("worktreeCreate", worktreeCreate)
.handle("worktreeRemove", worktreeRemove)
.handle("worktreeReset", worktreeReset)
.handle("session", session)
.handle("resource", resource)
}),
).pipe(
Layer.provide(Account.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(ToolRegistry.defaultLayer),
Layer.provide(Worktree.defaultLayer),
)
22 changes: 10 additions & 12 deletions packages/opencode/src/server/routes/instance/httpapi/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { File } from "@/file"
import { Ripgrep } from "@/file/ripgrep"
import * as InstanceState from "@/effect/instance-state"
import { LSP } from "@/lsp/lsp"
import { Effect, Layer, Schema } from "effect"
import { Effect, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"

Expand Down Expand Up @@ -116,7 +116,7 @@ export const FileApi = HttpApi.make("file")
}),
)

export const fileHandlers = Layer.unwrap(
export const fileHandlers = HttpApiBuilder.group(FileApi, "file", (handlers) =>
Effect.gen(function* () {
const svc = yield* File.Service
const ripgrep = yield* Ripgrep.Service
Expand Down Expand Up @@ -154,14 +154,12 @@ export const fileHandlers = Layer.unwrap(
return yield* svc.status()
})

return HttpApiBuilder.group(FileApi, "file", (handlers) =>
handlers
.handle("findText", findText)
.handle("findFile", findFile)
.handle("findSymbol", findSymbol)
.handle("list", list)
.handle("content", content)
.handle("status", status),
)
return handlers
.handle("findText", findText)
.handle("findFile", findFile)
.handle("findSymbol", findSymbol)
.handle("list", list)
.handle("content", content)
.handle("status", status)
}),
).pipe(Layer.provide(File.defaultLayer), Layer.provide(Ripgrep.defaultLayer))
)
166 changes: 162 additions & 4 deletions packages/opencode/src/server/routes/instance/httpapi/global.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Config } from "@/config/config"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
import { Installation } from "@/installation"
import { Instance } from "@/project/instance"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Schema } from "effect"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

const log = Log.create({ service: "server" })

const GlobalHealth = Schema.Struct({
healthy: Schema.Literal(true),
version: Schema.String,
}).annotate({ identifier: "GlobalHealth" })

const GlobalEvent = Schema.Struct({
const GlobalEventSchema = Schema.Struct({
directory: Schema.String,
project: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
Expand Down Expand Up @@ -50,7 +58,7 @@ export const GlobalApi = HttpApi.make("global").add(
}),
),
HttpApiEndpoint.get("event", GlobalPaths.event, {
success: GlobalEvent,
success: GlobalEventSchema,
}).annotateMerge(
OpenApi.annotations({
identifier: "global.event",
Expand Down Expand Up @@ -99,3 +107,153 @@ export const GlobalApi = HttpApi.make("global").add(
)
.annotateMerge(OpenApi.annotations({ title: "global", description: "Global server routes." })),
)

function eventData(data: unknown) {
return `data: ${JSON.stringify(data)}\n\n`
}

function parseBody(body: string) {
try {
return JSON.parse(body || "{}") as unknown
} catch {
return undefined
}
}

function eventResponse() {
const encoder = new TextEncoder()
let heartbeat: ReturnType<typeof setInterval> | undefined
let unsubscribe = () => {}
let done = false

const cleanup = () => {
if (done) return
done = true
if (heartbeat) clearInterval(heartbeat)
unsubscribe()
log.info("global event disconnected")
}

log.info("global event connected")
return HttpServerResponse.raw(
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
const write = (data: unknown) => {
if (done) return
try {
controller.enqueue(encoder.encode(eventData(data)))
} catch {
cleanup()
}
}
const handler = (event: GlobalBusEvent) => write(event)
unsubscribe = () => GlobalBus.off("event", handler)
GlobalBus.on("event", handler)
write({ payload: { type: "server.connected", properties: {} } })
heartbeat = setInterval(() => write({ payload: { type: "server.heartbeat", properties: {} } }), 10_000)
},
cancel: cleanup,
}),
{
headers: {
"Cache-Control": "no-cache, no-transform",
"Content-Type": "text/event-stream",
"X-Accel-Buffering": "no",
"X-Content-Type-Options": "nosniff",
},
},
),
)
}

export const globalHandlers = HttpApiBuilder.group(GlobalApi, "global", (handlers) =>
Effect.gen(function* () {
const config = yield* Config.Service
const installation = yield* Installation.Service

const health = Effect.fn("GlobalHttpApi.health")(function* () {
return { healthy: true as const, version: InstallationVersion }
})

const event = Effect.fn("GlobalHttpApi.event")(function* () {
return eventResponse()
})

const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
return yield* config.getGlobal()
})

const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
return yield* config.updateGlobal(ctx.payload)
})

const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
yield* Effect.promise(() => Instance.disposeAll())
GlobalBus.emit("event", {
directory: "global",
payload: { type: "global.disposed", properties: {} },
})
return true
})

const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) {
const method = yield* installation.method()
if (method === "unknown") {
return {
status: 400,
body: { success: false as const, error: "Unknown installation method" },
}
}
const target = ctx.payload.target || (yield* installation.latest(method))
const result = yield* installation.upgrade(method, target).pipe(
Effect.as({ status: 200, body: { success: true as const, version: target } }),
Effect.catch((err) =>
Effect.succeed({
status: 500,
body: {
success: false as const,
error: err instanceof Error ? err.message : String(err),
},
}),
),
)
if (!result.body.success) return result
GlobalBus.emit("event", {
directory: "global",
payload: {
type: Installation.Event.Updated.type,
properties: { version: target },
},
})
return result
})

const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: {
request: HttpServerRequest.HttpServerRequest
}) {
const body = yield* Effect.orDie(ctx.request.text)
const json = parseBody(body)
if (json === undefined) {
return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
}
const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe(
Effect.map((payload) => ({ valid: true as const, payload })),
Effect.catch(() => Effect.succeed({ valid: false as const })),
)
if (!payload.valid) {
return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
}
const result = yield* upgrade({ payload: payload.payload })
return HttpServerResponse.jsonUnsafe(result.body, { status: result.status })
})

return handlers
.handle("health", health)
.handleRaw("event", event)
.handle("configGet", configGet)
.handle("configUpdate", configUpdate)
.handle("dispose", dispose)
.handleRaw("upgrade", upgradeRaw)
}),
)
Loading
Loading