refactor(client): split package into promise and effect entrypoints
This commit is contained in:
parent
0087dd56d9
commit
af5eabcb26
46 changed files with 5211 additions and 2332 deletions
|
|
@ -2,8 +2,9 @@ import { EOL } from "node:os"
|
|||
import { Effect, Option } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Service } from "../../services/service"
|
||||
import type { Transport } from "@opencode-ai/client/service"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import type { Transport } from "@opencode-ai/client/effect"
|
||||
|
||||
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
|
||||
|
||||
|
|
@ -18,7 +19,9 @@ type OpenApi = {
|
|||
export default Runtime.handler(
|
||||
Commands.commands.api,
|
||||
Effect.fn("cli.api")(function* (input) {
|
||||
const transport = yield* Service.connect()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const params = Option.getOrElse(input.param, () => ({}))
|
||||
const request = yield* resolveRequest(transport, input.request, params)
|
||||
const headers = new Headers(transport.headers)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import * as Effect from "effect/Effect"
|
|||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.debug.commands.agents,
|
||||
Effect.fn("cli.debug.agents")(function* () {
|
||||
const transport = yield* Service.connect()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } }))
|
||||
process.stdout.write(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Effect, FileSystem, Option } from "effect"
|
||||
import { Service } from "../../services/service"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import type { Transport } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import { Standalone } from "../../services/standalone"
|
||||
import { Updater } from "../../services/updater"
|
||||
import { basicAuth } from "@opencode-ai/client/service"
|
||||
import type { Transport } from "@opencode-ai/client/service"
|
||||
|
||||
export default Runtime.handler(Commands, (input) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -17,29 +17,34 @@ export default Runtime.handler(Commands, (input) =>
|
|||
const server = Option.getOrUndefined(input.server)
|
||||
if (server !== undefined && input.standalone)
|
||||
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
|
||||
const transport = yield* resolveTransport(server, input.standalone)
|
||||
const transport = yield* Effect.gen(function* () {
|
||||
if (server !== undefined) {
|
||||
const password = process.env["OPENCODE_SERVER_PASSWORD"]
|
||||
return {
|
||||
url: server,
|
||||
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
|
||||
} satisfies Transport
|
||||
}
|
||||
if (input.standalone) return yield* Standalone.transport()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
return found ?? (yield* Service.start(options))
|
||||
})
|
||||
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||
// The TUI re-runs discover whenever its event stream drops. For an explicit
|
||||
// --server or a standalone child the transport is fixed, so reconnects
|
||||
// retry the same address; for the managed service discovery re-reads the
|
||||
// registration and may start a replacement.
|
||||
const context = yield* Effect.context<FileSystem.FileSystem | Global.Service>()
|
||||
const discover =
|
||||
server !== undefined || input.standalone
|
||||
? () => Promise.resolve(transport)
|
||||
: () => Effect.runPromise(Service.connect().pipe(Effect.provide(context)))
|
||||
const serviceOptions = server === undefined && !input.standalone ? yield* ServiceConfig.options() : undefined
|
||||
const discover = serviceOptions
|
||||
? () =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const found = yield* Service.discover(serviceOptions)
|
||||
return found ?? (yield* Service.start(serviceOptions))
|
||||
}).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||
)
|
||||
: () => Promise.resolve(transport)
|
||||
yield* runTui(transport, { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, discover)
|
||||
}),
|
||||
)
|
||||
|
||||
function resolveTransport(server: string | undefined, standalone: boolean) {
|
||||
if (server !== undefined) {
|
||||
const password = process.env["OPENCODE_SERVER_PASSWORD"]
|
||||
return Effect.succeed({
|
||||
url: server,
|
||||
headers: password ? basicAuth(password) : undefined,
|
||||
} satisfies Transport)
|
||||
}
|
||||
if (standalone) return Standalone.transport()
|
||||
return Service.connect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import {
|
|||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
import { resolveIntegration } from "./resolve"
|
||||
|
||||
const location = { directory: process.cwd() }
|
||||
|
|
@ -16,7 +17,9 @@ const location = { directory: process.cwd() }
|
|||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.auth,
|
||||
Effect.fn("cli.mcp.auth")(function* (input) {
|
||||
const transport = yield* Service.connect()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
|
||||
const integration = yield* resolveIntegration(client, input.name, location)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import * as Effect from "effect/Effect"
|
|||
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.list,
|
||||
Effect.fn("cli.mcp.list")(function* () {
|
||||
const transport = yield* Service.connect()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } }))
|
||||
const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { Effect } from "effect"
|
|||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
import { resolveIntegration } from "./resolve"
|
||||
|
||||
const location = { directory: process.cwd() }
|
||||
|
|
@ -11,7 +12,9 @@ const location = { directory: process.cwd() }
|
|||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.logout,
|
||||
Effect.fn("cli.mcp.logout")(function* (input) {
|
||||
const transport = yield* Service.connect()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
|
||||
const integration = yield* resolveIntegration(client, input.name, location)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Context, Layer, Option, Schedule } from "effect"
|
||||
import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { createServer } from "node:http"
|
||||
import { createRoutes } from "@opencode-ai/server/routes"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Service } from "../../services/service"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import { Updater } from "../../services/updater"
|
||||
import { randomBytes } from "crypto"
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import path from "path"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.serve,
|
||||
|
|
@ -25,9 +27,9 @@ export default Runtime.handler(
|
|||
Effect.gen(function* () {
|
||||
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
|
||||
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||
const config = input.service ? yield* Service.config() : {}
|
||||
const config = input.service ? yield* ServiceConfig.read() : {}
|
||||
const password = input.service
|
||||
? yield* Service.password()
|
||||
? yield* ServiceConfig.password()
|
||||
: standalonePassword || randomBytes(32).toString("base64url")
|
||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
||||
|
|
@ -43,7 +45,7 @@ export default Runtime.handler(
|
|||
headers: ServerAuth.headers({ password }),
|
||||
}).v2.health.get({}),
|
||||
)
|
||||
if (input.service) yield* Service.register(address)
|
||||
if (input.service) yield* register(address)
|
||||
const url = HttpServer.formatAddress(address)
|
||||
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
||||
|
|
@ -55,6 +57,56 @@ export default Runtime.handler(
|
|||
}),
|
||||
)
|
||||
|
||||
// Server-side half of the registration protocol. The registration embeds the
|
||||
// password so the file alone is enough for any client to discover and
|
||||
// authenticate. The file arbitrates ownership after concurrent starts; it is
|
||||
// not a startup lock: the atomic rename elects the latest writer, the watcher
|
||||
// self-evicts losers, and the finalizer id-guard keeps an exiting server from
|
||||
// deleting its successor's registration.
|
||||
const RegistrationId = Schema.Struct({ id: Schema.optional(Schema.String) })
|
||||
const decodeRegistrationId = Schema.decodeUnknownEffect(Schema.fromJsonString(RegistrationId))
|
||||
|
||||
const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const { file } = yield* ServiceConfig.options()
|
||||
const id = randomUUID()
|
||||
const secret = yield* ServiceConfig.password()
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
temp,
|
||||
JSON.stringify({
|
||||
id,
|
||||
version: InstallationVersion,
|
||||
url: HttpServer.formatAddress(address),
|
||||
pid: process.pid,
|
||||
password: secret,
|
||||
}),
|
||||
{ mode: 0o600 },
|
||||
)
|
||||
yield* fs.rename(temp, file)
|
||||
const currentID = fs.readFileString(file).pipe(
|
||||
Effect.flatMap(decodeRegistrationId),
|
||||
Effect.map((info) => info.id),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
yield* currentID.pipe(
|
||||
Effect.flatMap((current) =>
|
||||
current === id
|
||||
? Effect.void
|
||||
: Effect.try({ try: () => process.kill(process.pid, "SIGTERM"), catch: (cause) => cause }).pipe(Effect.ignore),
|
||||
),
|
||||
Effect.repeat(Schedule.spaced("10 seconds")),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
currentID.pipe(
|
||||
Effect.flatMap((current) => (current === id ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
function waitForStdinClose() {
|
||||
return Effect.callback<void>((resume) => {
|
||||
const close = () => resume(Effect.void)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { Option } from "effect"
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.get,
|
||||
Effect.fn("cli.service.get")(function* (input) {
|
||||
process.stdout.write((yield* Service.get(Option.getOrUndefined(input.key))) + EOL)
|
||||
process.stdout.write((yield* ServiceConfig.get(Option.getOrUndefined(input.key))) + EOL)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.restart,
|
||||
Effect.fn("cli.service.restart")(function* () {
|
||||
yield* Service.stop()
|
||||
process.stdout.write((yield* Service.start()).url + EOL)
|
||||
const options = yield* ServiceConfig.options()
|
||||
yield* Service.stop(options)
|
||||
const transport = yield* Service.start(options)
|
||||
process.stdout.write(transport.url + EOL)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.set,
|
||||
Effect.fn("cli.service.set")(function* (input) {
|
||||
yield* Service.set(input.key, input.value)
|
||||
yield* ServiceConfig.set(input.key, input.value)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.start,
|
||||
Effect.fn("cli.service.start")(function* () {
|
||||
process.stdout.write((yield* Service.start()).url + EOL)
|
||||
const transport = yield* Service.start(yield* ServiceConfig.options())
|
||||
process.stdout.write(transport.url + EOL)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.status,
|
||||
Effect.fn("cli.service.status")(function* () {
|
||||
const found = yield* Service.discover()
|
||||
const found = yield* Service.discover(yield* ServiceConfig.options())
|
||||
process.stdout.write((found ? found.url : "stopped") + EOL)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.stop,
|
||||
Effect.fn("cli.service.stop")(function* () {
|
||||
yield* Service.stop()
|
||||
yield* Service.stop(yield* ServiceConfig.options())
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "../../../services/service"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.unset,
|
||||
Effect.fn("cli.service.unset")(function* (input) {
|
||||
yield* Service.unset(input.key)
|
||||
yield* ServiceConfig.unset(input.key)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue