182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
import { isRecord } from "./record"
|
|
|
|
type ConfigIssue = { message: string; path: string[] }
|
|
|
|
export function cliErrorMessage(input: unknown): string | undefined {
|
|
if (input instanceof Error && isRecord(input.cause) && "body" in input.cause) {
|
|
const formatted = cliErrorMessage(input.cause.body)
|
|
if (formatted) return formatted
|
|
}
|
|
|
|
if (tagged(input, "CliError")) {
|
|
if (typeof input.exitCode === "number") process.exitCode = input.exitCode
|
|
return field(input, "message") ?? ""
|
|
}
|
|
if (tagged(input, "AccountServiceError") || tagged(input, "AccountTransportError")) {
|
|
return field(input, "message") ?? ""
|
|
}
|
|
|
|
const model = configData(input, "ProviderModelNotFoundError")
|
|
if (model) {
|
|
const suggestions = Array.isArray(model.suggestions)
|
|
? model.suggestions.filter((item): item is string => typeof item === "string")
|
|
: []
|
|
return [
|
|
`Model not found: ${field(model, "providerID")}/${field(model, "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")
|
|
}
|
|
|
|
const provider = configData(input, "ProviderInitError")
|
|
if (provider)
|
|
return `Failed to initialize provider "${field(provider, "providerID")}". Check credentials and configuration.`
|
|
|
|
const json = configData(input, "ConfigJsonError")
|
|
if (json) {
|
|
const message = field(json, "message")
|
|
return `Config file at ${field(json, "path")} is not valid JSON(C)` + (message ? `: ${message}` : "")
|
|
}
|
|
|
|
const directory = configData(input, "ConfigDirectoryTypoError")
|
|
if (directory) {
|
|
return `Directory "${field(directory, "dir")}" in ${field(directory, "path")} is not valid. Rename the directory to "${field(directory, "suggestion")}" or remove it. This is a common typo.`
|
|
}
|
|
|
|
const frontmatter = configData(input, "ConfigFrontmatterError")
|
|
if (frontmatter) return field(frontmatter, "message") ?? ""
|
|
|
|
const invalid = configData(input, "ConfigInvalidError")
|
|
if (invalid) {
|
|
const path = field(invalid, "path")
|
|
const message = field(invalid, "message")
|
|
const issues = Array.isArray(invalid.issues)
|
|
? invalid.issues.filter((issue): issue is ConfigIssue => {
|
|
return (
|
|
isRecord(issue) &&
|
|
typeof issue.message === "string" &&
|
|
Array.isArray(issue.path) &&
|
|
issue.path.every((item) => typeof item === "string")
|
|
)
|
|
})
|
|
: []
|
|
return [
|
|
`Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""),
|
|
...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")),
|
|
].join("\n")
|
|
}
|
|
|
|
if (tagged(input, "UICancelledError") || named(input, "UICancelledError")) return ""
|
|
if (isRecord(input) && named(input, "MCPFailed")) {
|
|
const name = isRecord(input.data) ? field(input.data, "name") : undefined
|
|
return `MCP server "${name}" failed. Note, opencode does not support MCP authentication yet.`
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function tagged(input: unknown, tag: string): input is Record<string, unknown> {
|
|
return isRecord(input) && input._tag === tag
|
|
}
|
|
|
|
function named(input: unknown, name: string) {
|
|
return isRecord(input) && (input.name === name || input._tag === name)
|
|
}
|
|
|
|
function configData(input: unknown, tag: string) {
|
|
if (!isRecord(input)) return undefined
|
|
if (input.name === tag && isRecord(input.data)) return input.data
|
|
if (input._tag === tag) return input
|
|
return undefined
|
|
}
|
|
|
|
function field(input: Record<string, unknown>, key: string) {
|
|
return typeof input[key] === "string" ? input[key] : undefined
|
|
}
|
|
|
|
export function errorFormat(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.stack ?? `${error.name}: ${error.message}`
|
|
}
|
|
|
|
if (typeof error === "object" && error !== null) {
|
|
try {
|
|
const json = JSON.stringify(error, null, 2)
|
|
// Plain objects whose own properties are all non-enumerable (or empty)
|
|
// serialize to "{}", which prints as a useless bare `{}` on stderr.
|
|
// Fall back to a custom toString first, then to ctor name + own prop names.
|
|
if (json === "{}") {
|
|
const str = String(error)
|
|
if (str && str !== "[object Object]") return str
|
|
const ctor = error.constructor?.name
|
|
const prefix = ctor && ctor !== "Object" ? ctor : "Error"
|
|
const names = Object.getOwnPropertyNames(error)
|
|
return names.length === 0 ? `${prefix} (no message)` : `${prefix} { ${names.join(", ")} }`
|
|
}
|
|
return json
|
|
} catch {
|
|
return "Unexpected error (unserializable)"
|
|
}
|
|
}
|
|
|
|
return String(error)
|
|
}
|
|
|
|
export function errorMessage(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
if (error.message) return error.message
|
|
if (error.name) return error.name
|
|
}
|
|
|
|
if (isRecord(error) && typeof error.message === "string" && error.message) {
|
|
return error.message
|
|
}
|
|
|
|
if (isRecord(error) && isRecord(error.data) && typeof error.data.message === "string" && error.data.message) {
|
|
return error.data.message
|
|
}
|
|
|
|
const text = String(error)
|
|
if (text && text !== "[object Object]") return text
|
|
|
|
const formatted = errorFormat(error)
|
|
if (formatted) return formatted
|
|
return "unknown error"
|
|
}
|
|
|
|
export function errorData(error: unknown) {
|
|
if (error instanceof Error) {
|
|
return {
|
|
type: error.name,
|
|
message: errorMessage(error),
|
|
stack: error.stack,
|
|
cause: error.cause === undefined ? undefined : errorFormat(error.cause),
|
|
formatted: errorFormat(error),
|
|
}
|
|
}
|
|
|
|
if (!isRecord(error)) {
|
|
return {
|
|
type: typeof error,
|
|
message: errorMessage(error),
|
|
formatted: errorFormat(error),
|
|
}
|
|
}
|
|
|
|
const data = Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => {
|
|
const value = error[key]
|
|
if (value === undefined) return acc
|
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
acc[key] = value
|
|
return acc
|
|
}
|
|
// oxlint-disable-next-line no-base-to-string -- intentional coercion of arbitrary error properties
|
|
acc[key] = value instanceof Error ? value.message : String(value)
|
|
return acc
|
|
}, {})
|
|
|
|
if (typeof data.message !== "string") data.message = errorMessage(error)
|
|
if (typeof data.type !== "string") data.type = error.constructor?.name
|
|
data.formatted = errorFormat(error)
|
|
return data
|
|
}
|