为打造自有纯 CLI 产品 zmo(类 Claude Code),在保留上游同步能力的前提下做套壳改造: - 入口隐藏 9 个非 CLI 命令(console/web/github/pr/stats/export/import/db/debug) - 全面 rebrand:scriptName、Logo、配置目录(~/.config/zmo)、bin 名、构建产物、可见文案 - env 双读兼容(ZMO_* 优先,回退 OPENCODE_*),配置文件名新增 zmo.json - 刻意保留 @opencode-ai/* 内部包名与 opencode provider,避免破坏功能与上游同步 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
119 lines
4.9 KiB
TypeScript
119 lines
4.9 KiB
TypeScript
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<string, unknown> {
|
|
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
|
|
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, zmo 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: \`zmo models\` to list available models`,
|
|
`Or check your config (zmo.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") ?? ""
|
|
}
|
|
|
|
// 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)
|
|
}
|