import { appendFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; const SECRET_KEYS = /api[_-]?key|authorization|token|secret|password/i; export interface LoggerOptions { extensionRoot: string; debug: boolean; } export function redactSecrets(value: unknown): unknown { if (Array.isArray(value)) { return value.map((entry) => redactSecrets(entry)); } if (value && typeof value === "object") { const output: Record = {}; for (const [key, nestedValue] of Object.entries(value)) { output[key] = SECRET_KEYS.test(key) ? "[REDACTED]" : redactSecrets(nestedValue); } return output; } return value; } function stringifyDetails(details: unknown): string { if (details === undefined) return ""; try { return ` ${JSON.stringify(redactSecrets(details))}`; } catch { return " [unserializable-details]"; } } export class DebugLogger { private readonly debugDir: string; private readonly logPath: string; private debugDirEnsured = false; private writeQueue: Promise = Promise.resolve(); constructor(private readonly options: LoggerOptions) { this.debugDir = join(options.extensionRoot, "debug"); this.logPath = join(this.debugDir, "debug.log"); } debug(event: string, details?: unknown): void { this.write("debug", event, details); } warn(event: string, details?: unknown): void { this.write("warn", event, details); } error(event: string, details?: unknown): void { this.write("error", event, details); } flush(): Promise { return this.writeQueue.catch(() => undefined); } private async ensureDebugDir(): Promise { if (this.debugDirEnsured) return; await mkdir(this.debugDir, { recursive: true }); this.debugDirEnsured = true; } private write(level: "debug" | "warn" | "error", event: string, details?: unknown): void { if (!this.options.debug) return; const line = `${JSON.stringify({ timestamp: new Date().toISOString(), level, extension: "pi-model-discovery", event })}${stringifyDetails(details)}\n`; this.writeQueue = this.writeQueue.then( () => this.appendLine(line), () => this.appendLine(line), ); void this.writeQueue.catch(() => { // Debug logging must never affect extension behavior or terminal output. }); } private async appendLine(line: string): Promise { await this.ensureDebugDir(); await appendFile(this.logPath, line, "utf-8"); } }