cloudaxe-opencode/packages/opencode/src/cli/error.ts

120 lines
4.7 KiB
TypeScript
Raw Normal View History

import { NamedError } from "@opencode-ai/core/util/error"
2026-03-27 14:00:26 +00:00
import { errorFormat } from "@/util/error"
import { isRecord } from "@/util/record"
2026-04-16 06:03:03 +00:00
interface ErrorLike {
name?: string
_tag?: string
message?: string
data?: Record<string, unknown>
}
type ConfigIssue = { message: string; path: string[] }
2026-04-16 06:03:03 +00:00
function isTaggedError(error: unknown, tag: string): boolean {
return isRecord(error) && error._tag === tag
}
function configData(input: unknown, tag: string): Record<string, unknown> | undefined {
if (!isRecord(input)) return undefined
if (input.name === tag && isRecord(input.data)) return input.data
if (input._tag === tag) return input
return undefined
}
function stringField(input: Record<string, unknown>, key: string): string | undefined {
return typeof input[key] === "string" ? input[key] : undefined
}
function configIssues(input: Record<string, unknown>): ConfigIssue[] {
return Array.isArray(input.issues)
? input.issues.filter((issue): issue is ConfigIssue => {
if (!isRecord(issue)) return false
2026-05-13 05:32:32 +00:00
return (
typeof issue.message === "string" &&
Array.isArray(issue.path) &&
issue.path.every((x) => typeof x === "string")
)
})
: []
2026-04-16 06:03:03 +00:00
}
2025-06-20 04:57:28 +00:00
export function FormatError(input: unknown) {
// CliError: domain failure surfaced from an effectCmd handler via fail("...")
if (isTaggedError(input, "CliError")) {
const data = input as ErrorLike & { exitCode?: number }
if (data.exitCode != null) process.exitCode = data.exitCode
return data.message ?? ""
}
2026-04-16 06:03:03 +00:00
// MCPFailed: { name: string }
if (NamedError.hasName(input, "MCPFailed")) {
return `MCP server "${(input as ErrorLike).data?.name}" failed. Note, opencode does not support MCP authentication yet.`
}
2026-04-16 06:03:03 +00:00
// AccountServiceError, AccountTransportError: TaggedErrorClass
if (isTaggedError(input, "AccountServiceError") || isTaggedError(input, "AccountTransportError")) {
return (input as ErrorLike).message ?? ""
}
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
const providerModelNotFound = configData(input, "ProviderModelNotFoundError")
if (providerModelNotFound) {
const suggestions = Array.isArray(providerModelNotFound.suggestions)
? providerModelNotFound.suggestions.filter((x) => typeof x === "string")
: []
return [
`Model not found: ${providerModelNotFound.providerID}/${providerModelNotFound.modelID}`,
...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
`Try: \`opencode models\` to list available models`,
`Or check your config (opencode.json) provider/model names`,
].join("\n")
}
2026-04-16 06:03:03 +00:00
// ProviderInitError: { providerID: string }
const providerInit = configData(input, "ProviderInitError")
if (providerInit) {
return `Failed to initialize provider "${stringField(providerInit, "providerID")}". Check credentials and configuration.`
}
2026-04-16 06:03:03 +00:00
// ConfigJsonError: { path: string, message?: string }
const configJson = configData(input, "ConfigJsonError")
if (configJson) {
const message = stringField(configJson, "message")
return `Config file at ${stringField(configJson, "path")} is not valid JSON(C)` + (message ? `: ${message}` : "")
}
2026-04-16 06:03:03 +00:00
// ConfigDirectoryTypoError: { dir: string, path: string, suggestion: string }
const configDirectoryTypo = configData(input, "ConfigDirectoryTypoError")
if (configDirectoryTypo) {
return `Directory "${stringField(configDirectoryTypo, "dir")}" in ${stringField(configDirectoryTypo, "path")} is not valid. Rename the directory to "${stringField(configDirectoryTypo, "suggestion")}" or remove it. This is a common typo.`
2025-10-17 19:34:37 +00:00
}
2026-04-16 06:03:03 +00:00
// ConfigFrontmatterError: { message: string }
const configFrontmatter = configData(input, "ConfigFrontmatterError")
if (configFrontmatter) {
return stringField(configFrontmatter, "message") ?? ""
2025-10-30 15:56:30 +00:00
}
2026-04-16 06:03:03 +00:00
// ConfigInvalidError: { path?: string, message?: string, issues?: Array<{ message: string, path: string[] }> }
const configInvalid = configData(input, "ConfigInvalidError")
if (configInvalid) {
const path = stringField(configInvalid, "path")
const message = stringField(configInvalid, "message")
const issues = configIssues(configInvalid)
2025-06-20 04:57:28 +00:00
return [
2026-04-16 06:03:03 +00:00
`Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""),
...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")),
2025-06-20 04:57:28 +00:00
].join("\n")
2026-04-16 06:03:03 +00:00
}
// UICancelledError: user cancelled an interactive CLI prompt
if (isTaggedError(input, "UICancelledError") || NamedError.hasName(input, "UICancelledError")) {
2026-04-16 06:03:03 +00:00
return ""
}
2025-06-20 04:57:28 +00:00
}
export function FormatUnknownError(input: unknown): string {
2026-03-27 14:00:26 +00:00
return errorFormat(input)
}