2026-04-25 14:59:17 +00:00
import { NamedError } from "@opencode-ai/core/util/error"
2026-03-27 14:00:26 +00:00
import { errorFormat } from "@/util/error"
2026-05-13 22:23:09 +00:00
import { isRecord } from "@/util/record"
2026-04-16 06:03:03 +00:00
2026-05-13 05:31:18 +00:00
type ConfigIssue = { message : string ; path : string [ ] }
2026-05-16 00:42:56 +00:00
function isTaggedError ( error : unknown , tag : string ) : error is Record < string , unknown > {
2026-05-13 05:31:18 +00:00
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-05-13 05:31:18 +00:00
} )
: [ ]
2026-04-16 06:03:03 +00:00
}
2025-06-20 04:57:28 +00:00
2026-05-16 00:42:56 +00:00
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
}
2026-05-02 16:08:04 +00:00
// CliError: domain failure surfaced from an effectCmd handler via fail("...")
if ( isTaggedError ( input , "CliError" ) ) {
2026-05-16 00:42:56 +00:00
if ( typeof input . exitCode === "number" ) process . exitCode = input . exitCode
return stringField ( input , "message" ) ? ? ""
2026-05-02 16:08:04 +00:00
}
2026-04-16 06:03:03 +00:00
// MCPFailed: { name: string }
if ( NamedError . hasName ( input , "MCPFailed" ) ) {
2026-05-16 00:42:56 +00:00
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. `
2026-04-07 18:31:53 +00:00
}
2026-04-16 06:03:03 +00:00
// AccountServiceError, AccountTransportError: TaggedErrorClass
if ( isTaggedError ( input , "AccountServiceError" ) || isTaggedError ( input , "AccountTransportError" ) ) {
2026-05-16 00:42:56 +00:00
return stringField ( input , "message" ) ? ? ""
2026-04-16 06:03:03 +00:00
}
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
2026-05-13 22:23:09 +00:00
const providerModelNotFound = configData ( input , "ProviderModelNotFoundError" )
if ( providerModelNotFound ) {
const suggestions = Array . isArray ( providerModelNotFound . suggestions )
? providerModelNotFound . suggestions . filter ( ( x ) = > typeof x === "string" )
: [ ]
2025-11-21 08:01:19 +00:00
return [
2026-05-16 00:42:56 +00:00
` Model not found: ${ stringField ( providerModelNotFound , "providerID" ) } / ${ stringField ( providerModelNotFound , "modelID" ) } ` ,
2026-04-16 23:52:04 +00:00
. . . ( suggestions . length ? [ "Did you mean: " + suggestions . join ( ", " ) ] : [ ] ) ,
2025-11-21 08:01:19 +00:00
` 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 }
2026-05-14 09:18:58 +00:00
const providerInit = configData ( input , "ProviderInitError" )
if ( providerInit ) {
return ` Failed to initialize provider " ${ stringField ( providerInit , "providerID" ) } ". Check credentials and configuration. `
2025-11-21 08:01:19 +00:00
}
2026-04-16 06:03:03 +00:00
// ConfigJsonError: { path: string, message?: string }
2026-05-13 05:31:18 +00:00
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 } ` : "" )
2025-07-31 11:25:26 +00:00
}
2026-04-16 06:03:03 +00:00
// ConfigDirectoryTypoError: { dir: string, path: string, suggestion: string }
2026-05-13 05:31:18 +00:00
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 }
2026-05-13 05:31:18 +00:00
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
2026-06-10 15:02:09 +00:00
// 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" )
}
2026-04-16 06:03:03 +00:00
// ConfigInvalidError: { path?: string, message?: string, issues?: Array<{ message: string, path: string[] }> }
2026-05-13 05:31:18 +00:00
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 } ` : "" ) ,
2026-04-16 23:52:04 +00:00
. . . 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
}
2025-06-25 02:09:43 +00:00
2026-05-12 18:09:00 +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 ""
}
2026-05-16 00:42:56 +00:00
return undefined
2025-06-20 04:57:28 +00:00
}
2025-11-26 16:49:55 +00:00
export function FormatUnknownError ( input : unknown ) : string {
2026-03-27 14:00:26 +00:00
return errorFormat ( input )
2025-11-26 16:49:55 +00:00
}