import { NamedError } from "@opencode-ai/core/util/error" import { errorFormat } from "@/util/error" import { isRecord } from "@/util/record" type ConfigIssue = { message: string; path: string[] } function isTaggedError(error: unknown, tag: string): error is Record { return isRecord(error) && error._tag === tag } function configData(input: unknown, tag: string): Record | 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, key: string): string | undefined { return typeof input[key] === "string" ? input[key] : undefined } function configIssues(input: Record): ConfigIssue[] { return Array.isArray(input.issues) ? input.issues.filter((issue): issue is ConfigIssue => { if (!isRecord(issue)) return false return ( typeof issue.message === "string" && Array.isArray(issue.path) && issue.path.every((x) => typeof x === "string") ) }) : [] } export function FormatError(input: unknown): string | undefined { if (input instanceof Error && isRecord(input.cause) && "body" in input.cause) { const formatted = FormatError(input.cause.body) if (formatted) return formatted } // CliError: domain failure surfaced from an effectCmd handler via fail("...") if (isTaggedError(input, "CliError")) { if (typeof input.exitCode === "number") process.exitCode = input.exitCode return stringField(input, "message") ?? "" } // MCPFailed: { name: string } if (NamedError.hasName(input, "MCPFailed")) { const data = isRecord(input) && isRecord(input.data) ? stringField(input.data, "name") : undefined return `MCP server "${data}" failed. Note, opencode does not support MCP authentication yet.` } // AccountServiceError, AccountTransportError: TaggedErrorClass if (isTaggedError(input, "AccountServiceError") || isTaggedError(input, "AccountTransportError")) { return stringField(input, "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: ${stringField(providerModelNotFound, "providerID")}/${stringField(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") } // ProviderInitError: { providerID: string } const providerInit = configData(input, "ProviderInitError") if (providerInit) { return `Failed to initialize provider "${stringField(providerInit, "providerID")}". Check credentials and configuration.` } // 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}` : "") } // 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.` } // ConfigFrontmatterError: { message: string } const configFrontmatter = configData(input, "ConfigFrontmatterError") if (configFrontmatter) { return stringField(configFrontmatter, "message") ?? "" } // ConfigRemoteAuthError: { url: string, remote: string } const remoteAuth = configData(input, "ConfigRemoteAuthError") if (remoteAuth) { const url = stringField(remoteAuth, "url") const remote = stringField(remoteAuth, "remote") return [ `Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`, `Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).`, ...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []), ].join("\n") } // 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) return [ `Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""), ...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")), ].join("\n") } // UICancelledError: user cancelled an interactive CLI prompt if (isTaggedError(input, "UICancelledError") || NamedError.hasName(input, "UICancelledError")) { return "" } return undefined } export function FormatUnknownError(input: unknown): string { return errorFormat(input) }