refactor(server): canonicalize service API (#31049)
This commit is contained in:
@@ -1,88 +1 @@
|
||||
import { isRecord } from "./record"
|
||||
|
||||
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
|
||||
}
|
||||
export * from "@opencode-ai/tui/util/error"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export function formatDuration(secs: number) {
|
||||
if (secs <= 0) return ""
|
||||
if (secs < 60) return `${secs}s`
|
||||
if (secs < 3600) {
|
||||
const mins = Math.floor(secs / 60)
|
||||
const remaining = secs % 60
|
||||
return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m`
|
||||
}
|
||||
if (secs < 86400) {
|
||||
const hours = Math.floor(secs / 3600)
|
||||
const remaining = Math.floor((secs % 3600) / 60)
|
||||
return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h`
|
||||
}
|
||||
if (secs < 604800) {
|
||||
const days = Math.floor(secs / 86400)
|
||||
return days === 1 ? "~1 day" : `~${days} days`
|
||||
}
|
||||
const weeks = Math.floor(secs / 604800)
|
||||
return weeks === 1 ? "~1 week" : `~${weeks} weeks`
|
||||
}
|
||||
@@ -1,86 +1,2 @@
|
||||
export function titlecase(str: string) {
|
||||
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
export function time(input: number): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
||||
}
|
||||
|
||||
export function datetime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const localTime = time(input)
|
||||
const localDate = date.toLocaleDateString()
|
||||
return `${localTime} · ${localDate}`
|
||||
}
|
||||
|
||||
export function todayTimeOrDateTime(input: number): string {
|
||||
const date = new Date(input)
|
||||
const now = new Date()
|
||||
const isToday =
|
||||
date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
|
||||
|
||||
if (isToday) {
|
||||
return time(input)
|
||||
} else {
|
||||
return datetime(input)
|
||||
}
|
||||
}
|
||||
|
||||
export function number(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + "M"
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + "K"
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
export function duration(input: number) {
|
||||
if (input < 1000) {
|
||||
return `${input}ms`
|
||||
}
|
||||
if (input < 60000) {
|
||||
return `${(input / 1000).toFixed(1)}s`
|
||||
}
|
||||
if (input < 3600000) {
|
||||
const minutes = Math.floor(input / 60000)
|
||||
const seconds = Math.floor((input % 60000) / 1000)
|
||||
return `${minutes}m ${seconds}s`
|
||||
}
|
||||
if (input < 86400000) {
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const minutes = Math.floor((input % 3600000) / 60000)
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
const hours = Math.floor(input / 3600000)
|
||||
const days = Math.floor((input % 3600000) / 86400000)
|
||||
return `${days}d ${hours}h`
|
||||
}
|
||||
|
||||
export function truncate(str: string, len: number): string {
|
||||
if (str.length <= len) return str
|
||||
return str.slice(0, len - 1) + "…"
|
||||
}
|
||||
|
||||
export function truncateLeft(str: string, len: number): string {
|
||||
if (str.length <= len) return str
|
||||
return "…" + str.slice(-(len - 1))
|
||||
}
|
||||
|
||||
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
||||
if (str.length <= maxLength) return str
|
||||
|
||||
const ellipsis = "…"
|
||||
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
||||
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
||||
|
||||
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
||||
}
|
||||
|
||||
export function pluralize(count: number, singular: string, plural: string): string {
|
||||
const template = count === 1 ? singular : plural
|
||||
return template.replace("{}", count.toString())
|
||||
}
|
||||
|
||||
export * as Locale from "./locale"
|
||||
export * from "@opencode-ai/tui/util/locale"
|
||||
export { Locale } from "@opencode-ai/tui/util/locale"
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
export * from "@opencode-ai/tui/util/record"
|
||||
|
||||
Reference in New Issue
Block a user