/** * Logging utilities that work well in both local and Gitea Actions environments */ import { AsyncLocalStorage } from "node:async_hooks"; import * as core from "@actions/core"; import { isGitHubActions, isInsideDocker } from "./globals.ts"; type LogContext = { prefix: string }; const logContext = new AsyncLocalStorage(); const MAGENTA = "\x1b[35m"; const RESET = "\x1b[0m"; export function withLogPrefix(prefix: string, fn: () => Promise): Promise { return logContext.run({ prefix }, fn); } function prefixLines(message: string): string { const ctx = logContext.getStore(); if (!ctx) return message; const colored = `${MAGENTA}${ctx.prefix}${RESET} `; return message .split("\n") .map((line) => `${colored}${line}`) .join("\n"); } function prefixPlain(name: string): string { const ctx = logContext.getStore(); if (!ctx) return name; return `${ctx.prefix} ${name}`; } const isRunnerDebugEnabled = () => core.isDebug(); const isLocalDebugEnabled = () => process.env.LOG_LEVEL === "debug" || process.env.ACTIONS_STEP_DEBUG === "true"; const isDebugEnabled = () => isLocalDebugEnabled() || isRunnerDebugEnabled(); function ts(): string { return isDebugEnabled() ? `[${new Date().toISOString()}] ` : ""; } function formatArgs(args: unknown[]): string { return args .map((arg) => { if (typeof arg === "string") return arg; if (arg instanceof Error) return `${arg.message}\n${arg.stack}`; return JSON.stringify(arg); }) .join(" "); } function startGroup(name: string): void { const prefixed = prefixPlain(name); if (isGitHubActions) { core.startGroup(prefixed); } else { console.group(prefixed); } } function endGroup(): void { if (isGitHubActions) { core.endGroup(); } else { console.groupEnd(); } } function group(name: string, fn: () => void): void { startGroup(name); fn(); endGroup(); } function box(text: string, options?: { title?: string; maxWidth?: number }): void { const { title, maxWidth = 80 } = options || {}; const lines = text.trim().split("\n"); const maxLen = Math.max(...lines.map((l) => l.length)); const width = Math.min(maxLen + 2, maxWidth); const titleLen = title ? ` ${title} `.length : 0; const boxWidth = Math.max(width, titleLen); let result = ""; if (title) { const padding = Math.max(0, boxWidth - ` ${title} `.length); result += `┌ ${title} ${"─".repeat(padding)}┐\n`; } else { result += `┌${"─".repeat(boxWidth)}┐\n`; } for (const line of lines) { result += `│ ${line.padEnd(boxWidth - 2)} │\n`; } result += `└${"─".repeat(boxWidth)}┘`; core.info(prefixLines(result)); } function printTable( rows: Array> ): void { const tableData = rows.map((row) => row.map((cell) => (typeof cell === "string" ? cell : cell.data)) ); core.info(prefixLines(tableData.map((r) => r.join(" | ")).join("\n"))); } export async function writeSummary(text: string): Promise { if (!isGitHubActions) return; if (isInsideDocker) return; if (!process.env.GITHUB_STEP_SUMMARY) return; await core.summary.addRaw(text).write({ overwrite: true }); } export const log = { info: (...args: unknown[]): void => { core.info(prefixLines(`${ts()}${formatArgs(args)}`)); }, warning: (...args: unknown[]): void => { core.warning(prefixLines(`${ts()}${formatArgs(args)}`)); }, error: (...args: unknown[]): void => { core.error(prefixLines(`${ts()}${formatArgs(args)}`)); }, success: (...args: unknown[]): void => { core.info(prefixLines(`${ts()}» ${formatArgs(args)}`)); }, debug: (...args: unknown[]): void => { if (isRunnerDebugEnabled()) { core.debug(prefixLines(formatArgs(args))); return; } if (isLocalDebugEnabled()) { core.info(prefixLines(`${ts()}[DEBUG] ${formatArgs(args)}`)); } }, box, table: printTable, startGroup, endGroup, group, toolCall: ({ toolName, input }: { toolName: string; input: unknown }): void => { const inputFormatted = formatJsonValue(input); const output = inputFormatted !== "{}" ? `» ${toolName}(${inputFormatted})` : `» ${toolName}()`; log.info(output.trimEnd()); }, }; export function formatJsonValue(value: unknown): string { const compact = JSON.stringify(value); return compact.length > 80 || compact.includes("\n") ? JSON.stringify(value, null, 2) : compact; } export function formatIndentedField(label: string, content: string): string { if (!content.includes("\n")) return ` ${label}: ${content}\n`; const lines = content.split("\n"); let formatted = ` ${label}: ${lines[0]}\n`; for (let i = 1; i < lines.length; i++) { formatted += ` ${lines[i]}\n`; } return formatted; } export function formatUsageSummary(_entries: unknown[]): string { return ""; }