cli/mini: fix run failure handling (#35539)
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run script/build.ts",
|
"build": "bun run script/build.ts",
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
"test": "bun test --timeout 30000 --only-failures",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ Effect.logInfo("cli starting", {
|
|||||||
Effect.provide(LoggingLayer),
|
Effect.provide(LoggingLayer),
|
||||||
Effect.provide(NodeServices.layer),
|
Effect.provide(NodeServices.layer),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
Effect.tap(() => Effect.sync(() => process.exit(0))),
|
Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),
|
||||||
NodeRuntime.runMain,
|
NodeRuntime.runMain,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ type CurrentSkill = SkillListOutput["data"][number]
|
|||||||
type CurrentProvider = ProviderListOutput["data"][number]
|
type CurrentProvider = ProviderListOutput["data"][number]
|
||||||
type CurrentModel = ModelListOutput["data"][number]
|
type CurrentModel = ModelListOutput["data"][number]
|
||||||
|
|
||||||
function location(directory: string) {
|
function location(directory: string, workspace?: string) {
|
||||||
return {
|
return {
|
||||||
location: {
|
location: {
|
||||||
directory,
|
directory,
|
||||||
|
workspace,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +99,14 @@ export function runProviders(providers: CurrentProvider[], models: CurrentModel[
|
|||||||
export async function waitForCatalogReady(input: {
|
export async function waitForCatalogReady(input: {
|
||||||
sdk: OpenCodeClient
|
sdk: OpenCodeClient
|
||||||
directory: string
|
directory: string
|
||||||
|
workspace?: string
|
||||||
model: { providerID: string; modelID: string }
|
model: { providerID: string; modelID: string }
|
||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
}) {
|
}) {
|
||||||
const deadline = Date.now() + (input.timeoutMs ?? 5_000)
|
const deadline = Date.now() + (input.timeoutMs ?? 5_000)
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const models = await input.sdk.model
|
const models = await input.sdk.model
|
||||||
.list(location(input.directory))
|
.list(location(input.directory, input.workspace))
|
||||||
.then((result) => result.data)
|
.then((result) => result.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return
|
if (models?.some((model) => model.providerID === input.model.providerID && model.id === input.model.modelID)) return
|
||||||
|
|||||||
@@ -132,8 +132,14 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||||||
|
|
||||||
const consume = async () => {
|
const consume = async () => {
|
||||||
while (!controller.signal.aborted) {
|
while (!controller.signal.aborted) {
|
||||||
const next = await stream.next()
|
const next = await stream.next().catch((error) => {
|
||||||
if (next.done) throw new Error("Event stream disconnected during prompt execution")
|
if (!emittedError) throw error
|
||||||
|
return { done: true as const, value: undefined }
|
||||||
|
})
|
||||||
|
if (next.done) {
|
||||||
|
if (emittedError) return
|
||||||
|
throw new Error("Event stream disconnected during prompt execution")
|
||||||
|
}
|
||||||
const event = next.value
|
const event = next.value
|
||||||
|
|
||||||
if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) {
|
if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) {
|
||||||
@@ -416,7 +422,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||||||
}
|
}
|
||||||
controller.abort()
|
controller.abort()
|
||||||
await completed?.catch(() => {})
|
await completed?.catch(() => {})
|
||||||
if (interrupted) return undefined
|
if (interrupted || emittedError) return undefined
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
admission = undefined
|
admission = undefined
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { open } from "node:fs/promises"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Daemon } from "../daemon"
|
import { Daemon } from "../daemon"
|
||||||
import { Standalone } from "../services/standalone"
|
import { Standalone } from "../services/standalone"
|
||||||
import { loadRunAgents, waitForCatalogReady, waitForDefaultModel } from "./catalog.shared"
|
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||||
import { runNonInteractivePrompt } from "./noninteractive"
|
import { runNonInteractivePrompt } from "./noninteractive"
|
||||||
import { toolInlineInfo } from "./tool"
|
import { toolInlineInfo } from "./tool"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
@@ -49,7 +49,11 @@ type Prepared = {
|
|||||||
|
|
||||||
const ATTACH_FILE_MAX_BYTES = 10 * 1024 * 1024
|
const ATTACH_FILE_MAX_BYTES = 10 * 1024 * 1024
|
||||||
|
|
||||||
export async function runNonInteractive(input: RunCommandInput) {
|
export function runNonInteractive(input: RunCommandInput) {
|
||||||
|
return run(input).catch((error) => reportError(input, error instanceof Error ? error.message : String(error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(input: RunCommandInput) {
|
||||||
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
||||||
const root = process.env.PWD ?? process.cwd()
|
const root = process.env.PWD ?? process.cwd()
|
||||||
const directory = input.server ? input.directory : localDirectory(input.directory, root)
|
const directory = input.server ? input.directory : localDirectory(input.directory, root)
|
||||||
@@ -78,17 +82,22 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
|
|||||||
if (!requestedDirectory) fail("Failed to resolve server directory")
|
if (!requestedDirectory) fail("Failed to resolve server directory")
|
||||||
const session = await selectSession(client, requestedDirectory, input)
|
const session = await selectSession(client, requestedDirectory, input)
|
||||||
const cwd = session?.location.directory ?? requestedDirectory
|
const cwd = session?.location.directory ?? requestedDirectory
|
||||||
|
const workspace = session?.location.workspaceID
|
||||||
const explicitModel = parseModel(input.model)
|
const explicitModel = parseModel(input.model)
|
||||||
const sessionModel = session?.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined
|
const sessionModel = session?.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined
|
||||||
const defaultModel =
|
const defaultModel =
|
||||||
input.variant && !explicitModel && !sessionModel
|
!explicitModel && !sessionModel
|
||||||
? await waitForDefaultModel({ sdk: client, directory: cwd })
|
? await client.model
|
||||||
|
.default({ location: { directory: cwd, workspace } })
|
||||||
|
.then((result) =>
|
||||||
|
result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined,
|
||||||
|
)
|
||||||
: undefined
|
: undefined
|
||||||
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
|
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
|
||||||
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
|
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
|
||||||
if (model) {
|
if (model) {
|
||||||
await waitForCatalogReady({ sdk: client, directory: cwd, model })
|
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
|
||||||
const available = await client.model.list({ location: { directory: cwd } })
|
const available = await client.model.list({ location: { directory: cwd, workspace } })
|
||||||
if (!available.data.some((item) => item.providerID === model.providerID && item.id === model.modelID))
|
if (!available.data.some((item) => item.providerID === model.providerID && item.id === model.modelID))
|
||||||
return reportError(input, `Model unavailable: ${model.providerID}/${model.modelID}`, session?.id)
|
return reportError(input, `Model unavailable: ${model.providerID}/${model.modelID}`, session?.id)
|
||||||
}
|
}
|
||||||
@@ -109,7 +118,6 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await runNonInteractivePrompt({
|
await runNonInteractivePrompt({
|
||||||
client,
|
client,
|
||||||
sessionID: selected.id,
|
sessionID: selected.id,
|
||||||
@@ -124,11 +132,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
|
|||||||
attached: !input.standaloneCommand,
|
attached: !input.standaloneCommand,
|
||||||
renderTool,
|
renderTool,
|
||||||
renderToolError,
|
renderToolError,
|
||||||
})
|
}).catch((error) => reportError(input, error instanceof Error ? error.message : String(error), selected.id))
|
||||||
} catch (error) {
|
|
||||||
UI.error(error instanceof Error ? error.message : String(error))
|
|
||||||
process.exitCode = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeInput(message: string | undefined, piped: string | undefined) {
|
export function mergeInput(message: string | undefined, piped: string | undefined) {
|
||||||
@@ -303,6 +307,5 @@ function reportError(input: RunCommandInput, message: string, sessionID?: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fail(message: string): never {
|
function fail(message: string): never {
|
||||||
UI.error(message)
|
throw new Error(message)
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (options.mode === "service") {
|
if (options.mode === "service") {
|
||||||
const service = yield* ServiceConfig.options()
|
const service = yield* ServiceConfig.options()
|
||||||
yield* Flock.effect("service-process", {
|
yield* Flock.effect(path.basename(service.file, ".json") + "-process", {
|
||||||
dir: path.dirname(service.file),
|
dir: path.dirname(service.file),
|
||||||
staleMs: 3_000,
|
staleMs: 3_000,
|
||||||
timeoutMs: 15_000,
|
timeoutMs: 15_000,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const makeTransport = Effect.fn("cli.standalone.transport")(
|
|||||||
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
|
||||||
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
|
||||||
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
const ready = yield* Effect.tryPromise(() => decodeReady(output))
|
||||||
return { url: ready.url, headers: ServerAuth.headers({ password }), pid: proc.pid }
|
return { url: ready.url, headers: ServerAuth.headers({ password, username: "opencode" }), pid: proc.pid }
|
||||||
},
|
},
|
||||||
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ await Effect.runPromise(
|
|||||||
Effect.scoped(
|
Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const transport = yield* Standalone.transport()
|
const transport = yield* Standalone.transport()
|
||||||
console.log(`${transport.pid} ${transport.url}`)
|
const response = yield* Effect.promise(() => fetch(new URL("/api/health", transport.url), { headers: transport.headers }))
|
||||||
|
console.log(`${transport.pid} ${transport.url} ${response.status}`)
|
||||||
return yield* Effect.never
|
return yield* Effect.never
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
|
import path from "node:path"
|
||||||
import { mergeInteractiveInput, mergeNonInteractiveInput, pickRunModel } from "../src/mini"
|
import { mergeInteractiveInput, mergeNonInteractiveInput, pickRunModel } from "../src/mini"
|
||||||
|
|
||||||
async function cli(args: string[]) {
|
async function cli(args: string[]) {
|
||||||
const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
|
const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
|
||||||
cwd: new URL("..", import.meta.url).pathname,
|
cwd: path.join(import.meta.dir, ".."),
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
@@ -61,6 +63,66 @@ describe("mini command", () => {
|
|||||||
expect(result.stderr).not.toContain("You must provide a message")
|
expect(result.stderr).not.toContain("You must provide a message")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves a run failure exit code", async () => {
|
||||||
|
let modelRequests = 0
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch(request) {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
if (url.pathname === "/api/health")
|
||||||
|
return Response.json({ healthy: true, version: InstallationVersion, pid: process.pid })
|
||||||
|
if (url.pathname === "/api/model") {
|
||||||
|
modelRequests++
|
||||||
|
return Response.json({
|
||||||
|
location: { directory: process.cwd(), project: { id: "global", directory: process.cwd() } },
|
||||||
|
data: modelRequests === 1 ? [{ id: "missing", providerID: "definitely" }] : [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return new Response(undefined, { status: 404 })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await cli([
|
||||||
|
"run",
|
||||||
|
"--server",
|
||||||
|
server.url.toString(),
|
||||||
|
"--dir",
|
||||||
|
process.cwd(),
|
||||||
|
"--model",
|
||||||
|
"definitely/missing",
|
||||||
|
"hi",
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1)
|
||||||
|
expect(result.stderr).toContain("Model unavailable: definitely/missing")
|
||||||
|
} finally {
|
||||||
|
server.stop(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reports pre-admission errors as JSON", async () => {
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch() {
|
||||||
|
return Response.json({ healthy: true, version: "incompatible", pid: process.pid })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await cli(["run", "--format", "json", "--server", server.url.toString(), "hi"])
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1)
|
||||||
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||||
|
type: "error",
|
||||||
|
sessionID: "",
|
||||||
|
error: { type: "unknown", message: expect.stringContaining("requires") },
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
server.stop(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("uses the shared V2 server option instead of an attach command", async () => {
|
test("uses the shared V2 server option instead of an attach command", async () => {
|
||||||
const result = await cli(["mini", "--help"])
|
const result = await cli(["mini", "--help"])
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import path from "node:path"
|
|||||||
test("standalone server exits when its owner is killed", async () => {
|
test("standalone server exits when its owner is killed", async () => {
|
||||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], {
|
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "fixture/standalone-owner.ts")], {
|
||||||
cwd: path.join(import.meta.dir, ".."),
|
cwd: path.join(import.meta.dir, ".."),
|
||||||
env: process.env,
|
env: { ...process.env, OPENCODE_SERVER_USERNAME: "custom" },
|
||||||
stdin: "ignore",
|
stdin: "ignore",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
})
|
})
|
||||||
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
|
const line = await Promise.race([readLine(owner.stdout), Bun.sleep(10_000).then(() => undefined)])
|
||||||
const [rawPID, url] = line?.split(" ") ?? []
|
const [rawPID, url, status] = line?.split(" ") ?? []
|
||||||
const pid = Number(rawPID)
|
const pid = Number(rawPID)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
expect(pid).toBeGreaterThan(0)
|
expect(pid).toBeGreaterThan(0)
|
||||||
expect(url).toStartWith("http://127.0.0.1:")
|
expect(url).toStartWith("http://127.0.0.1:")
|
||||||
|
expect(status).toBe("200")
|
||||||
expect(running(pid)).toBe(true)
|
expect(running(pid)).toBe(true)
|
||||||
|
|
||||||
owner.kill("SIGKILL")
|
owner.kill("SIGKILL")
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ export const Info = Schema.Struct({
|
|||||||
export type Info = typeof Info.Type
|
export type Info = typeof Info.Type
|
||||||
|
|
||||||
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
|
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
|
||||||
const decodeHealth = Schema.decodeUnknownEffect(
|
const decodeHealth = Schema.decodeUnknownOption(
|
||||||
Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, pid: Schema.Int }),
|
Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, pid: Schema.Int }),
|
||||||
)
|
)
|
||||||
|
const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) }))
|
||||||
|
|
||||||
// A missing or corrupt file means no valid info; callers treat both
|
// A missing or corrupt file means no valid info; callers treat both
|
||||||
// the same (the registering server self-evicts, clients rediscover).
|
// the same (the registering server self-evicts, clients rediscover).
|
||||||
@@ -122,7 +123,7 @@ type LocalService = {
|
|||||||
readonly transport: Transport
|
readonly transport: Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
const probe = Effect.fnUntraced(function* (info: Info, version?: string) {
|
const probe = Effect.fnUntraced(function* (info: Info, version?: string, allowLegacy = false) {
|
||||||
const headers = info.password === undefined ? undefined : auth(info.password)
|
const headers = info.password === undefined ? undefined : auth(info.password)
|
||||||
const response = yield* Effect.tryPromise(() =>
|
const response = yield* Effect.tryPromise(() =>
|
||||||
fetch(new URL("/api/health", info.url), {
|
fetch(new URL("/api/health", info.url), {
|
||||||
@@ -131,14 +132,20 @@ const probe = Effect.fnUntraced(function* (info: Info, version?: string) {
|
|||||||
}),
|
}),
|
||||||
).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
||||||
if (response === undefined || !response.ok) return undefined
|
if (response === undefined || !response.ok) return undefined
|
||||||
const health = yield* Effect.tryPromise(() => response.json()).pipe(
|
const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
||||||
Effect.flatMap(decodeHealth),
|
const health = decodeHealth(body)
|
||||||
Effect.option,
|
if (Option.isSome(health)) {
|
||||||
Effect.map(Option.getOrUndefined),
|
if (health.value.pid !== info.pid) return undefined
|
||||||
|
if (info.version !== undefined && health.value.version !== info.version) return undefined
|
||||||
|
if (version !== undefined && health.value.version !== version) return undefined
|
||||||
|
return { info, transport: { url: info.url, headers } } satisfies LocalService
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!allowLegacy ||
|
||||||
|
Option.isNone(decodeLegacyHealth(body)) ||
|
||||||
|
(typeof body === "object" && body !== null && ("version" in body || "pid" in body))
|
||||||
)
|
)
|
||||||
if (health?.pid !== info.pid) return undefined
|
return undefined
|
||||||
if (info.version !== undefined && health.version !== info.version) return undefined
|
|
||||||
if (version !== undefined && health.version !== version) return undefined
|
|
||||||
return { info, transport: { url: info.url, headers } } satisfies LocalService
|
return { info, transport: { url: info.url, headers } } satisfies LocalService
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,7 +154,7 @@ const probe = Effect.fnUntraced(function* (info: Info, version?: string) {
|
|||||||
const find = Effect.fnUntraced(function* (options: Options) {
|
const find = Effect.fnUntraced(function* (options: Options) {
|
||||||
const info = yield* read(options.file)
|
const info = yield* read(options.file)
|
||||||
if (info === undefined) return undefined
|
if (info === undefined) return undefined
|
||||||
return yield* probe(info)
|
return yield* probe(info, undefined, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
|
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
|
import { ServiceUnavailableError } from "@opencode-ai/protocol/errors"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
@@ -17,6 +19,19 @@ export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers)
|
|||||||
.handle(
|
.handle(
|
||||||
"model.default",
|
"model.default",
|
||||||
Effect.fn(function* () {
|
Effect.fn(function* () {
|
||||||
|
const plugins = yield* PluginSupervisor.Service
|
||||||
|
yield* plugins.ready.pipe(
|
||||||
|
Effect.timeoutOrElse({
|
||||||
|
duration: "5 seconds",
|
||||||
|
orElse: () =>
|
||||||
|
Effect.fail(
|
||||||
|
new ServiceUnavailableError({
|
||||||
|
message: "Model catalog initialization timed out",
|
||||||
|
service: "model.catalog",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
return yield* response(catalog.model.default())
|
return yield* response(catalog.model.default())
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
|
"@opencode-ai/cli#test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
"@opencode-ai/tui#test": {
|
"@opencode-ai/tui#test": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
|
|||||||
Reference in New Issue
Block a user