diff options
| author | Dax Raad <[email protected]> | 2026-04-25 13:29:52 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2026-04-25 13:30:37 -0400 |
| commit | 1a734adb4d1ce6071432bd68ac45fa4457f0dc2e (patch) | |
| tree | aed9acae5ae3da2fb93d3184ea2dc1f6a9412104 /packages/core/src/util | |
| parent | a9740b9133a8056f5992b17f1b3fde15cc039f8d (diff) | |
| download | opencode-1a734adb4d1ce6071432bd68ac45fa4457f0dc2e.tar.gz opencode-1a734adb4d1ce6071432bd68ac45fa4457f0dc2e.zip | |
core: consolidate shared infrastructure into core package
Moves effect logging, observability, runtime utilities, flags, installation
version info, and process utilities from opencode to core package. This
enables better code sharing across packages and establishes core as the
single source of truth for foundational utilities.
All internal imports updated to use @opencode-ai/core paths for consistency.
Diffstat (limited to 'packages/core/src/util')
| -rw-r--r-- | packages/core/src/util/log.ts | 185 | ||||
| -rw-r--r-- | packages/core/src/util/opencode-process.ts | 24 |
2 files changed, 209 insertions, 0 deletions
diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts new file mode 100644 index 000000000..a61c15f7a --- /dev/null +++ b/packages/core/src/util/log.ts @@ -0,0 +1,185 @@ +import path from "path" +import fs from "fs/promises" +import { createWriteStream } from "fs" +import * as Global from "../global" +import z from "zod" +import { Glob } from "./glob" + +export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) +export type Level = z.infer<typeof Level> + +const levelPriority: Record<Level, number> = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +} +const keep = 10 + +let level: Level = "INFO" + +function shouldLog(input: Level): boolean { + return levelPriority[input] >= levelPriority[level] +} + +export type Logger = { + debug(message?: any, extra?: Record<string, any>): void + info(message?: any, extra?: Record<string, any>): void + error(message?: any, extra?: Record<string, any>): void + warn(message?: any, extra?: Record<string, any>): void + tag(key: string, value: string): Logger + clone(): Logger + time( + message: string, + extra?: Record<string, any>, + ): { + stop(): void + [Symbol.dispose](): void + } +} + +const loggers = new Map<string, Logger>() + +export const Default = create({ service: "default" }) + +export interface Options { + print: boolean + dev?: boolean + level?: Level +} + +let logpath = "" +export function file() { + return logpath +} +let write = (msg: any) => { + process.stderr.write(msg) + return msg.length +} + +export async function init(options: Options) { + if (options.level) level = options.level + void cleanup(Global.Path.log) + if (options.print) return + logpath = path.join( + Global.Path.log, + options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log", + ) + await fs.truncate(logpath).catch(() => {}) + const stream = createWriteStream(logpath, { flags: "a" }) + write = async (msg: any) => { + return new Promise((resolve, reject) => { + stream.write(msg, (err) => { + if (err) reject(err) + else resolve(msg.length) + }) + }) + } +} + +async function cleanup(dir: string) { + const files = ( + await Glob.scan("????-??-??T??????.log", { + cwd: dir, + absolute: false, + include: "file", + }).catch(() => []) + ) + .filter((file) => path.basename(file) === file) + .sort() + if (files.length <= keep) return + + const doomed = files.slice(0, -keep) + await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {}))) +} + +function formatError(error: Error, depth = 0): string { + const result = error.message + return error.cause instanceof Error && depth < 10 + ? result + " Caused by: " + formatError(error.cause, depth + 1) + : result +} + +let last = Date.now() +export function create(tags?: Record<string, any>) { + tags = tags || {} + + const service = tags["service"] + if (service && typeof service === "string") { + const cached = loggers.get(service) + if (cached) { + return cached + } + } + + function build(message: any, extra?: Record<string, any>) { + const prefix = Object.entries({ + ...tags, + ...extra, + }) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + const prefix = `${key}=` + if (value instanceof Error) return prefix + formatError(value) + if (typeof value === "object") return prefix + JSON.stringify(value) + return prefix + value + }) + .join(" ") + const next = new Date() + const diff = next.getTime() - last + last = next.getTime() + return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n" + } + const result: Logger = { + debug(message?: any, extra?: Record<string, any>) { + if (shouldLog("DEBUG")) { + write("DEBUG " + build(message, extra)) + } + }, + info(message?: any, extra?: Record<string, any>) { + if (shouldLog("INFO")) { + write("INFO " + build(message, extra)) + } + }, + error(message?: any, extra?: Record<string, any>) { + if (shouldLog("ERROR")) { + write("ERROR " + build(message, extra)) + } + }, + warn(message?: any, extra?: Record<string, any>) { + if (shouldLog("WARN")) { + write("WARN " + build(message, extra)) + } + }, + tag(key: string, value: string) { + if (tags) tags[key] = value + return result + }, + clone() { + return create({ ...tags }) + }, + time(message: string, extra?: Record<string, any>) { + const now = Date.now() + result.info(message, { status: "started", ...extra }) + function stop() { + result.info(message, { + status: "completed", + duration: Date.now() - now, + ...extra, + }) + } + return { + stop, + [Symbol.dispose]() { + stop() + }, + } + }, + } + + if (service && typeof service === "string") { + loggers.set(service, result) + } + + return result +} diff --git a/packages/core/src/util/opencode-process.ts b/packages/core/src/util/opencode-process.ts new file mode 100644 index 000000000..f59270ad2 --- /dev/null +++ b/packages/core/src/util/opencode-process.ts @@ -0,0 +1,24 @@ +export const OPENCODE_RUN_ID = "OPENCODE_RUN_ID" +export const OPENCODE_PROCESS_ROLE = "OPENCODE_PROCESS_ROLE" + +export function ensureRunID() { + return (process.env[OPENCODE_RUN_ID] ??= crypto.randomUUID()) +} + +export function ensureProcessRole(fallback: "main" | "worker") { + return (process.env[OPENCODE_PROCESS_ROLE] ??= fallback) +} + +export function ensureProcessMetadata(fallback: "main" | "worker") { + return { + runID: ensureRunID(), + processRole: ensureProcessRole(fallback), + } +} + +export function sanitizedProcessEnv(overrides?: Record<string, string>) { + const env = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), + ) + return overrides ? Object.assign(env, overrides) : env +} |
