refactor(client): rename service start to ensure
This commit is contained in:
@@ -35,7 +35,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
|
|||||||
yield* request(() => timeline.intro("Log in"))
|
yield* request(() => timeline.intro("Log in"))
|
||||||
yield* request(() => timeline.pending("Connecting to OpenCode..."))
|
yield* request(() => timeline.pending("Connecting to OpenCode..."))
|
||||||
|
|
||||||
const endpoint = yield* Service.start(yield* ServiceConfig.options())
|
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
|
||||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal }))
|
const found = yield* request((signal) => client.integration.get({ integrationID, location }, { signal }))
|
||||||
const integration = yield* required(found.data, "OpenCode Console integration is unavailable")
|
const integration = yield* required(found.data, "OpenCode Console integration is unavailable")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default Runtime.handler(
|
|||||||
Effect.fn("cli.debug.agents")(function* () {
|
Effect.fn("cli.debug.agents")(function* () {
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.ensure(options))
|
||||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
|
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default Runtime.handler(
|
|||||||
Effect.fn("cli.mcp.auth")(function* (input) {
|
Effect.fn("cli.mcp.auth")(function* (input) {
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.ensure(options))
|
||||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
const integration = yield* resolveIntegration(client, input.name, location)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default Runtime.handler(
|
|||||||
Effect.fn("cli.mcp.list")(function* () {
|
Effect.fn("cli.mcp.list")(function* () {
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.ensure(options))
|
||||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
|
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
|
||||||
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
|
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default Runtime.handler(
|
|||||||
Effect.fn("cli.mcp.logout")(function* (input) {
|
Effect.fn("cli.mcp.logout")(function* (input) {
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.ensure(options))
|
||||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
const integration = yield* resolveIntegration(client, input.name, location)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ServiceConfig } from "../../services/service-config"
|
|||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.pair,
|
Commands.commands.pair,
|
||||||
Effect.fn("cli.pair")(function* () {
|
Effect.fn("cli.pair")(function* () {
|
||||||
const endpoint = yield* Service.start(yield* ServiceConfig.options())
|
const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
|
||||||
const password = yield* ServiceConfig.password()
|
const password = yield* ServiceConfig.password()
|
||||||
const server = yield* Effect.tryPromise(() =>
|
const server = yield* Effect.tryPromise(() =>
|
||||||
OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }).server.get(),
|
OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) }).server.get(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default Runtime.handler(
|
|||||||
Effect.fn("cli.service.restart")(function* () {
|
Effect.fn("cli.service.restart")(function* () {
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
yield* Service.stop(options)
|
yield* Service.stop(options)
|
||||||
const transport = yield* Service.start(options)
|
const transport = yield* Service.ensure(options)
|
||||||
process.stdout.write(transport.url + EOL)
|
process.stdout.write(transport.url + EOL)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ServiceConfig } from "../../../services/service-config"
|
|||||||
export default Runtime.handler(
|
export default Runtime.handler(
|
||||||
Commands.commands.service.commands.start,
|
Commands.commands.service.commands.start,
|
||||||
Effect.fn("cli.service.start")(function* () {
|
Effect.fn("cli.service.start")(function* () {
|
||||||
const transport = yield* Service.start(yield* ServiceConfig.options())
|
const transport = yield* Service.ensure(yield* ServiceConfig.options())
|
||||||
process.stdout.write(transport.url + EOL)
|
process.stdout.write(transport.url + EOL)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Service, type Endpoint, type StartOptions } from "@opencode-ai/client/effect/service"
|
import { Service, type Endpoint, type EnsureOptions } from "@opencode-ai/client/effect/service"
|
||||||
import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise"
|
import { ClientError, isUnauthorizedError, OpenCode } from "@opencode-ai/client/promise"
|
||||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { Effect, Redacted } from "effect"
|
import { Effect, Redacted } from "effect"
|
||||||
@@ -10,7 +10,7 @@ export type Args = {
|
|||||||
readonly server?: string
|
readonly server?: string
|
||||||
readonly standalone?: boolean
|
readonly standalone?: boolean
|
||||||
readonly mismatch?: "replace" | "ignore" | "error"
|
readonly mismatch?: "replace" | "ignore" | "error"
|
||||||
readonly onStart?: StartOptions["onStart"]
|
readonly onStart?: EnsureOptions["onStart"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Resolved = {
|
export type Resolved = {
|
||||||
@@ -49,31 +49,31 @@ export const resolve = Effect.fn("cli.server-connection.resolve")(function* (arg
|
|||||||
} satisfies Resolved
|
} satisfies Resolved
|
||||||
})
|
})
|
||||||
|
|
||||||
function managedService(options: StartOptions) {
|
function managedService(options: EnsureOptions) {
|
||||||
const reconnectOptions = { ...options, version: undefined }
|
const reconnectOptions = { ...options, version: undefined }
|
||||||
return {
|
return {
|
||||||
reconnect: () => Service.start(reconnectOptions),
|
reconnect: () => Service.ensure(reconnectOptions),
|
||||||
restart: () =>
|
restart: () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* Service.stop(options)
|
yield* Service.stop(options)
|
||||||
yield* Service.start(options)
|
yield* Service.ensure(options)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveManaged = Effect.fnUntraced(function* (
|
const resolveManaged = Effect.fnUntraced(function* (
|
||||||
options: StartOptions,
|
options: EnsureOptions,
|
||||||
mismatch: NonNullable<Args["mismatch"]>,
|
mismatch: NonNullable<Args["mismatch"]>,
|
||||||
) {
|
) {
|
||||||
if (mismatch === "replace") return yield* Service.start(options)
|
if (mismatch === "replace") return yield* Service.ensure(options)
|
||||||
if (mismatch === "ignore") return yield* Service.start({ ...options, version: undefined })
|
if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined })
|
||||||
|
|
||||||
const compatible = yield* Service.discover(options)
|
const compatible = yield* Service.discover(options)
|
||||||
if (compatible !== undefined) return compatible
|
if (compatible !== undefined) return compatible
|
||||||
const existing = yield* Service.discover({ ...options, version: undefined })
|
const existing = yield* Service.discover({ ...options, version: undefined })
|
||||||
if (existing !== undefined)
|
if (existing !== undefined)
|
||||||
return yield* Effect.fail(new Error("Background server version does not match this client"))
|
return yield* Effect.fail(new Error("Background server version does not match this client"))
|
||||||
return yield* Service.start(options)
|
return yield* Service.ensure(options)
|
||||||
})
|
})
|
||||||
|
|
||||||
function connectError(endpoint: Endpoint, cause: unknown) {
|
function connectError(endpoint: Endpoint, cause: unknown) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Effect, FileSystem, Option, Schema } from "effect"
|
|||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
// The CLI's service configuration file, plus the Service.StartOptions binding that
|
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
||||||
// points the client package's service operations at this CLI: which
|
// points the client package's service operations at this CLI: which
|
||||||
// registration file (by channel), which version, and how to spawn opencode.
|
// registration file (by channel), which version, and how to spawn opencode.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { join } from "node:path"
|
|||||||
import type {
|
import type {
|
||||||
DiscoverOptions,
|
DiscoverOptions,
|
||||||
Endpoint,
|
Endpoint,
|
||||||
StartOptions,
|
EnsureOptions,
|
||||||
StopOptions,
|
StopOptions,
|
||||||
} from "../service.js"
|
} from "../service.js"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ type Contender = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read-only lookup: registration file plus health check and version gate.
|
// Read-only lookup: registration file plus health check and version gate.
|
||||||
// Never spawns; escalation to start() is the caller's policy.
|
// Never spawns; escalation to ensure() is the caller's policy.
|
||||||
/** Discover a healthy, compatible local service without starting one. */
|
/** Discover a healthy, compatible local service without starting one. */
|
||||||
export const discover = Effect.fn("service.discover")(function* (options: DiscoverOptions = {}) {
|
export const discover = Effect.fn("service.discover")(function* (options: DiscoverOptions = {}) {
|
||||||
return (yield* discoverLocal(options))?.endpoint
|
return (yield* discoverLocal(options))?.endpoint
|
||||||
@@ -45,7 +45,7 @@ const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
|
|||||||
// version-mismatched one, and otherwise spawns small contenders until a server
|
// version-mismatched one, and otherwise spawns small contenders until a server
|
||||||
// becomes discoverable. A contender is never killed merely for slow startup.
|
// becomes discoverable. A contender is never killed merely for slow startup.
|
||||||
/** Ensure a healthy, compatible local service is running. */
|
/** Ensure a healthy, compatible local service is running. */
|
||||||
export const start = Effect.fn("service.start")(function* (options: StartOptions = {}) {
|
export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) {
|
||||||
const contenders = new Set<Contender>()
|
const contenders = new Set<Contender>()
|
||||||
let announced = false
|
let announced = false
|
||||||
let lastSpawn = 0
|
let lastSpawn = 0
|
||||||
@@ -219,7 +219,7 @@ const find = Effect.fnUntraced(function* (options: { readonly file?: string }) {
|
|||||||
return (yield* registered(options.file, true)).service
|
return (yield* registered(options.file, true)).service
|
||||||
})
|
})
|
||||||
|
|
||||||
// 50ms cadence bounded at ~5s, shared by stop escalation and each start
|
// 50ms cadence bounded at ~5s, shared by stop escalation and each ensure
|
||||||
// discovery window.
|
// discovery window.
|
||||||
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
|
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
|
||||||
|
|
||||||
@@ -280,4 +280,4 @@ const requestStop = Effect.fnUntraced(function* (service: LocalService) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/** Effect-based local service lifecycle operations. */
|
/** Effect-based local service lifecycle operations. */
|
||||||
export const Service = { discover, start, stop, headers, Info }
|
export const Service = { discover, ensure, stop, headers, Info }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
DiscoverOptions,
|
DiscoverOptions,
|
||||||
Endpoint,
|
Endpoint,
|
||||||
Info,
|
Info,
|
||||||
StartOptions,
|
EnsureOptions,
|
||||||
StopOptions,
|
StopOptions,
|
||||||
} from "../service.js"
|
} from "../service.js"
|
||||||
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
||||||
@@ -37,7 +37,7 @@ async function discoverLocal(options: DiscoverOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure a healthy, compatible local service is running. */
|
/** Ensure a healthy, compatible local service is running. */
|
||||||
export async function start(options: StartOptions = {}): Promise<Endpoint> {
|
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
|
||||||
const contenders = new Set<Contender>()
|
const contenders = new Set<Contender>()
|
||||||
let announced = false
|
let announced = false
|
||||||
let lastSpawn = 0
|
let lastSpawn = 0
|
||||||
@@ -249,4 +249,4 @@ function delay(milliseconds: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Promise-based local service lifecycle operations. */
|
/** Promise-based local service lifecycle operations. */
|
||||||
export const Service = { discover, start, stop, headers }
|
export const Service = { discover, ensure, stop, headers }
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ export type DiscoverOptions = {
|
|||||||
readonly version?: string
|
readonly version?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reason a new service process must be started. */
|
/** Reason ensuring the service requires a new process. */
|
||||||
export type StartReason = "missing" | "version-mismatch"
|
export type EnsureReason = "missing" | "version-mismatch"
|
||||||
|
|
||||||
/** Options used to ensure the local OpenCode service is running. */
|
/** Options used to ensure the local OpenCode service is running. */
|
||||||
export type StartOptions = DiscoverOptions & {
|
export type EnsureOptions = DiscoverOptions & {
|
||||||
/** Service command and arguments. Defaults to `opencode serve --service`. */
|
/** Service command and arguments. Defaults to `opencode serve --service`. */
|
||||||
readonly command?: ReadonlyArray<string>
|
readonly command?: ReadonlyArray<string>
|
||||||
/** Called once before spawning a new service process. */
|
/** Called once before spawning a new service process. */
|
||||||
readonly onStart?: (reason: StartReason, previousVersion?: string) => void
|
readonly onStart?: (reason: EnsureReason, previousVersion?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options used to stop the local OpenCode service. */
|
/** Options used to stop the local OpenCode service. */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { afterEach, expect, test } from "bun:test"
|
|||||||
import { mkdtemp, rm } from "node:fs/promises"
|
import { mkdtemp, rm } from "node:fs/promises"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { Service, type StartReason } from "../src/promise/service"
|
import { Service, type EnsureReason } from "../src/promise/service"
|
||||||
|
|
||||||
const fixture = join(import.meta.dir, "fixture/service.ts")
|
const fixture = join(import.meta.dir, "fixture/service.ts")
|
||||||
const processes: Bun.Subprocess[] = []
|
const processes: Bun.Subprocess[] = []
|
||||||
@@ -23,12 +23,12 @@ test("discovers a registered service", async () => {
|
|||||||
expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
|
expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("starts a missing service with native promises", async () => {
|
test("ensures a missing service with native promises", async () => {
|
||||||
const directory = await temp()
|
const directory = await temp()
|
||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
const starts: StartReason[] = []
|
const starts: EnsureReason[] = []
|
||||||
|
|
||||||
const endpoint = await Service.start({
|
const endpoint = await Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "coordinated"],
|
command: [process.execPath, fixture, registration, "coordinated"],
|
||||||
@@ -47,7 +47,7 @@ test("starts a missing service with native promises", async () => {
|
|||||||
test("reports a failed registered service", async () => {
|
test("reports a failed registered service", async () => {
|
||||||
const registration = await setup("failed-owner")
|
const registration = await setup("failed-owner")
|
||||||
|
|
||||||
await expect(Service.start({ file: registration, version: "test", command: [] })).rejects.toThrow(
|
await expect(Service.ensure({ file: registration, version: "test", command: [] })).rejects.toThrow(
|
||||||
"Background service failed to start",
|
"Background service failed to start",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Effect } from "effect"
|
|||||||
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import { Service, type StartReason } from "../src/effect/service"
|
import { Service, type EnsureReason } from "../src/effect/service"
|
||||||
|
|
||||||
const fixture = join(import.meta.dir, "fixture/service.ts")
|
const fixture = join(import.meta.dir, "fixture/service.ts")
|
||||||
const processes: Bun.Subprocess[] = []
|
const processes: Bun.Subprocess[] = []
|
||||||
@@ -23,9 +23,9 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
|
|||||||
await waitForFile(registration)
|
await waitForFile(registration)
|
||||||
const original = await Bun.file(registration).json()
|
const original = await Bun.file(registration).json()
|
||||||
|
|
||||||
const starts: StartReason[] = []
|
const starts: EnsureReason[] = []
|
||||||
const first = run(
|
const first = run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [],
|
command: [],
|
||||||
@@ -34,7 +34,7 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
|
|||||||
)
|
)
|
||||||
await waitForFile(registration + ".first-request")
|
await waitForFile(registration + ".first-request")
|
||||||
|
|
||||||
const resolved = await run(Service.start({ file: registration, version: "test" }))
|
const resolved = await run(Service.ensure({ file: registration, version: "test" }))
|
||||||
expect(resolved.url).toBe(original.url)
|
expect(resolved.url).toBe(original.url)
|
||||||
|
|
||||||
await writeFile(registration + ".release", "")
|
await writeFile(registration + ".release", "")
|
||||||
@@ -50,7 +50,7 @@ test("waits for a registered service to finish starting", async () => {
|
|||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
const process = spawn(registration, "starting")
|
const process = spawn(registration, "starting")
|
||||||
await waitForFile(registration)
|
await waitForFile(registration)
|
||||||
const result = run(Service.start({ file: registration, version: "test", command: [] }))
|
const result = run(Service.ensure({ file: registration, version: "test", command: [] }))
|
||||||
|
|
||||||
await Bun.sleep(500)
|
await Bun.sleep(500)
|
||||||
expect(process.exitCode).toBe(null)
|
expect(process.exitCode).toBe(null)
|
||||||
@@ -64,7 +64,7 @@ test("reports a failed registered service without spawning", async () => {
|
|||||||
const process = spawn(registration, "failed-owner")
|
const process = spawn(registration, "failed-owner")
|
||||||
await waitForFile(registration)
|
await waitForFile(registration)
|
||||||
|
|
||||||
await expect(run(Service.start({ file: registration, version: "test", command: [] }))).rejects.toThrow(
|
await expect(run(Service.ensure({ file: registration, version: "test", command: [] }))).rejects.toThrow(
|
||||||
"Background service failed to start",
|
"Background service failed to start",
|
||||||
)
|
)
|
||||||
expect(process.exitCode).toBe(null)
|
expect(process.exitCode).toBe(null)
|
||||||
@@ -90,7 +90,7 @@ test("does not spawn contenders while an incompatible service rejects replacemen
|
|||||||
await waitForFile(registration)
|
await waitForFile(registration)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const starting = Effect.runPromise(
|
const starting = Effect.runPromise(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, contender, "record-start"],
|
command: [process.execPath, fixture, contender, "record-start"],
|
||||||
@@ -113,8 +113,8 @@ test("a legacy health response is still replaced", async () => {
|
|||||||
const existing = spawn(registration, "legacy")
|
const existing = spawn(registration, "legacy")
|
||||||
await waitForFile(registration)
|
await waitForFile(registration)
|
||||||
|
|
||||||
const starts: StartReason[] = []
|
const starts: EnsureReason[] = []
|
||||||
const result = run(Service.start({ file: registration, command: [], onStart: (reason) => starts.push(reason) }))
|
const result = run(Service.ensure({ file: registration, command: [], onStart: (reason) => starts.push(reason) }))
|
||||||
|
|
||||||
await expect(result).rejects.toThrow("Missing service command")
|
await expect(result).rejects.toThrow("Missing service command")
|
||||||
expect(starts).toEqual(["version-mismatch"])
|
expect(starts).toEqual(["version-mismatch"])
|
||||||
@@ -125,7 +125,7 @@ test("waits for a slow winner while bounding lock probes", async () => {
|
|||||||
const directory = await temp()
|
const directory = await temp()
|
||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
const endpoint = await run(
|
const endpoint = await run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "coordinated"],
|
command: [process.execPath, fixture, registration, "coordinated"],
|
||||||
@@ -146,7 +146,7 @@ test("reports a contender that fails to start", async () => {
|
|||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
await expect(
|
await expect(
|
||||||
run(
|
run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "failed"],
|
command: [process.execPath, fixture, registration, "failed"],
|
||||||
@@ -160,7 +160,7 @@ test("reports a contender terminated by a signal", async () => {
|
|||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
await expect(
|
await expect(
|
||||||
run(
|
run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "signal"],
|
command: [process.execPath, fixture, registration, "signal"],
|
||||||
@@ -174,7 +174,7 @@ test("reports a slow contender that eventually fails", async () => {
|
|||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
await expect(
|
await expect(
|
||||||
run(
|
run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "delayed-failed", "8000"],
|
command: [process.execPath, fixture, registration, "delayed-failed", "8000"],
|
||||||
@@ -187,7 +187,7 @@ test("replaces an incompatible owner that appears during startup", async () => {
|
|||||||
const directory = await temp()
|
const directory = await temp()
|
||||||
const registration = join(directory, "service.json")
|
const registration = join(directory, "service.json")
|
||||||
const starting = run(
|
const starting = run(
|
||||||
Service.start({
|
Service.ensure({
|
||||||
file: registration,
|
file: registration,
|
||||||
version: "test",
|
version: "test",
|
||||||
command: [process.execPath, fixture, registration, "delayed", "8000"],
|
command: [process.execPath, fixture, registration, "delayed", "8000"],
|
||||||
|
|||||||
Vendored
+28
-13
@@ -70,23 +70,23 @@ for await (const event of client.event.subscribe()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local service
|
## Local background service
|
||||||
|
|
||||||
`Service` discovers and manages the local OpenCode background service from a
|
The main client entrypoints are browser-compatible and do not include local
|
||||||
Node application. The Promise API uses Node APIs directly and does not require
|
process management. In a Node application, import the native Promise service
|
||||||
Effect or `@effect/platform-node`.
|
API from `@opencode-ai/client/service`.
|
||||||
|
|
||||||
- `Service.discover()` returns a healthy registered endpoint without starting
|
- `Service.discover()` returns a healthy registered endpoint without starting
|
||||||
a process.
|
a process.
|
||||||
- `Service.start()` reuses a compatible service or starts one when needed.
|
- `Service.ensure()` returns a compatible service, starting one when needed.
|
||||||
- `Service.stop()` stops the registered service.
|
- `Service.stop()` stops the exact registered service instance.
|
||||||
- `Service.headers(endpoint)` creates the authentication headers for a client.
|
- `Service.headers(endpoint)` creates the authentication headers for a client.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { OpenCode } from "@opencode-ai/client"
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { Service } from "@opencode-ai/client/service"
|
import { Service } from "@opencode-ai/client/service"
|
||||||
|
|
||||||
const endpoint = await Service.start()
|
const endpoint = await Service.ensure()
|
||||||
const client = OpenCode.make({
|
const client = OpenCode.make({
|
||||||
baseUrl: endpoint.url,
|
baseUrl: endpoint.url,
|
||||||
headers: Service.headers(endpoint),
|
headers: Service.headers(endpoint),
|
||||||
@@ -95,7 +95,22 @@ const client = OpenCode.make({
|
|||||||
const health = await client.health.get()
|
const health = await client.health.get()
|
||||||
```
|
```
|
||||||
|
|
||||||
Import the native Promise service API from `@opencode-ai/client/service`.
|
`Service.ensure()` accepts an optional registration file, required version,
|
||||||
|
service command, and `onStart` callback:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const endpoint = await Service.ensure({
|
||||||
|
file: "/var/run/opencode/service.json",
|
||||||
|
version: "2.0.0",
|
||||||
|
command: ["opencode", "serve", "--service"],
|
||||||
|
onStart(reason, previousVersion) {
|
||||||
|
console.log(reason, previousVersion)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit these options to use the standard registration path and
|
||||||
|
`opencode serve --service` command.
|
||||||
|
|
||||||
## Effect
|
## Effect
|
||||||
|
|
||||||
@@ -133,11 +148,11 @@ const session = await Effect.runPromise(
|
|||||||
Streaming operations, including `client.event.subscribe()` and
|
Streaming operations, including `client.event.subscribe()` and
|
||||||
`client.session.log(...)`, return Effect `Stream` values.
|
`client.session.log(...)`, return Effect `Stream` values.
|
||||||
|
|
||||||
### Local service
|
### Local background service
|
||||||
|
|
||||||
The Effect entrypoint exposes the same service lifecycle operations as Effect
|
The Node-only `@opencode-ai/client/effect/service` entrypoint exposes the same
|
||||||
values. Add `@effect/platform-node` and provide its filesystem layer when
|
operations as Effect values. Add `@effect/platform-node` and provide its
|
||||||
running service operations.
|
filesystem layer when running them.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bun add @effect/platform-node
|
bun add @effect/platform-node
|
||||||
@@ -151,7 +166,7 @@ import { Effect } from "effect"
|
|||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
const endpoint = yield* Service.start()
|
const endpoint = yield* Service.ensure()
|
||||||
const client = yield* OpenCode.make({
|
const client = yield* OpenCode.make({
|
||||||
baseUrl: endpoint.url,
|
baseUrl: endpoint.url,
|
||||||
headers: Service.headers(endpoint),
|
headers: Service.headers(endpoint),
|
||||||
|
|||||||
+2
-2
@@ -113,7 +113,7 @@ Node application:
|
|||||||
|
|
||||||
- `Service.discover()` returns a healthy registered endpoint without starting
|
- `Service.discover()` returns a healthy registered endpoint without starting
|
||||||
a process.
|
a process.
|
||||||
- `Service.start()` reuses a compatible service or starts one when needed.
|
- `Service.ensure()` returns a compatible service, starting one when needed.
|
||||||
- `Service.stop()` stops the registered service.
|
- `Service.stop()` stops the registered service.
|
||||||
- `Service.headers(endpoint)` creates the authentication headers for a client.
|
- `Service.headers(endpoint)` creates the authentication headers for a client.
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ import { Effect } from "effect"
|
|||||||
import { FetchHttpClient } from "effect/unstable/http"
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
const program = Effect.gen(function* () {
|
||||||
const endpoint = yield* Service.start()
|
const endpoint = yield* Service.ensure()
|
||||||
const client = yield* OpenCode.make({
|
const client = yield* OpenCode.make({
|
||||||
baseUrl: endpoint.url,
|
baseUrl: endpoint.url,
|
||||||
headers: Service.headers(endpoint),
|
headers: Service.headers(endpoint),
|
||||||
|
|||||||
Reference in New Issue
Block a user