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
interface ErrorLike {
name? : string
_tag? : string
message? : string
2026-05-13 05:31:18 +00:00
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 {
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
export function FormatError ( input : unknown ) {
2026-05-02 16:08:04 +00:00
// 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-07 18:31:53 +00:00
}
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[] }
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-13 22:23:09 +00:00
` Model not found: ${ providerModelNotFound . providerID } / ${ 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
// 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 ""
}
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
}