takt/src/utils/debug.ts
2026-01-25 15:16:27 +09:00

160 lines
4.4 KiB
TypeScript

/**
* Debug logging utilities for takt
* Writes debug logs to file when enabled in config
*/
import { existsSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import type { DebugConfig } from '../models/types.js';
/** Debug logger state */
let debugEnabled = false;
let debugLogFile: string | null = null;
let initialized = false;
/** Get default debug log file path */
function getDefaultLogFile(): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
return join(homedir(), '.takt', 'logs', `debug-${timestamp}.log`);
}
/** Initialize debug logger from config */
export function initDebugLogger(config?: DebugConfig, projectDir?: string): void {
if (initialized) {
return;
}
debugEnabled = config?.enabled ?? false;
if (debugEnabled) {
if (config?.logFile) {
debugLogFile = config.logFile;
} else {
debugLogFile = getDefaultLogFile();
}
// Ensure log directory exists
const logDir = dirname(debugLogFile);
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
// Write initial log header
const header = [
'='.repeat(60),
`TAKT Debug Log`,
`Started: ${new Date().toISOString()}`,
`Project: ${projectDir || 'N/A'}`,
'='.repeat(60),
'',
].join('\n');
writeFileSync(debugLogFile, header, 'utf-8');
}
initialized = true;
}
/** Reset debug logger (for testing) */
export function resetDebugLogger(): void {
debugEnabled = false;
debugLogFile = null;
initialized = false;
}
/** Check if debug is enabled */
export function isDebugEnabled(): boolean {
return debugEnabled;
}
/** Get current debug log file path */
export function getDebugLogFile(): string | null {
return debugLogFile;
}
/** Format log message with timestamp and level */
function formatLogMessage(level: string, component: string, message: string, data?: unknown): string {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}] [${component}]`;
let logLine = `${prefix} ${message}`;
if (data !== undefined) {
try {
const dataStr = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
logLine += `\n${dataStr}`;
} catch {
logLine += `\n[Unable to serialize data]`;
}
}
return logLine;
}
/** Write a debug log entry */
export function debugLog(component: string, message: string, data?: unknown): void {
if (!debugEnabled || !debugLogFile) {
return;
}
const logLine = formatLogMessage('DEBUG', component, message, data);
try {
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
} catch {
// Silently fail - logging errors should not interrupt main flow
}
}
/** Write an info log entry */
export function infoLog(component: string, message: string, data?: unknown): void {
if (!debugEnabled || !debugLogFile) {
return;
}
const logLine = formatLogMessage('INFO', component, message, data);
try {
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
} catch {
// Silently fail
}
}
/** Write an error log entry */
export function errorLog(component: string, message: string, data?: unknown): void {
if (!debugEnabled || !debugLogFile) {
return;
}
const logLine = formatLogMessage('ERROR', component, message, data);
try {
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
} catch {
// Silently fail
}
}
/** Log function entry with arguments */
export function traceEnter(component: string, funcName: string, args?: Record<string, unknown>): void {
debugLog(component, `>> ${funcName}()`, args);
}
/** Log function exit with result */
export function traceExit(component: string, funcName: string, result?: unknown): void {
debugLog(component, `<< ${funcName}()`, result);
}
/** Create a scoped logger for a component */
export function createLogger(component: string) {
return {
debug: (message: string, data?: unknown) => debugLog(component, message, data),
info: (message: string, data?: unknown) => infoLog(component, message, data),
error: (message: string, data?: unknown) => errorLog(component, message, data),
enter: (funcName: string, args?: Record<string, unknown>) => traceEnter(component, funcName, args),
exit: (funcName: string, result?: unknown) => traceExit(component, funcName, result),
};
}