/** * Verbose REPL/fish diagnostics when `DEBUG=1`. * Writes to the **session TTY** when bound (see `bindReplDebugStream`) so Pear/bare-stdio * logs show in the same terminal pane as the shell. Falls back to `process.stdout`, then * `stderr`, then `console.log`. Lines are prefixed with reset + newline so they stay * readable if the prompt left SGR/cursor state; expect the screen to scroll while debugging. */ /** @type {import('stream').Writable | null} */ let _debugStream = null /** Bind REPL/fish stdout (e.g. bare-stdio `out`) so DEBUG lines appear in the visible terminal. */ export function bindReplDebugStream(stream) { _debugStream = stream && typeof stream.write === 'function' ? stream : null } export function unbindReplDebugStream() { _debugStream = null } /** @returns {boolean} */ export function isReplDebug() { const p = globalThis.process?.env return p?.DEBUG === '1' } function ts() { if (typeof globalThis.performance?.now === 'function') { return `${globalThis.performance.now().toFixed(2)}ms` } return `${Date.now()}` } function serialize(part) { if (typeof part === 'string') return part if (part === undefined) return 'undefined' if (part === null) return 'null' if (typeof part === 'number' || typeof part === 'boolean') return String(part) try { return JSON.stringify(part) } catch { return String(part) } } /** * @param {string} scope Short tag (e.g. fish-render, repl, booter) * @param {...unknown} parts Message pieces */ function writeDebugLine(text) { const payload = `\x1b[0m\n${text}\n` try { if (_debugStream && typeof _debugStream.write === 'function') { _debugStream.write(payload) return } const p = globalThis.process if (p?.stdout && typeof p.stdout.write === 'function') { p.stdout.write(payload) return } if (p?.stderr && typeof p.stderr.write === 'function') { p.stderr.write(payload) return } globalThis.console?.log?.(text) } catch { /* ignore */ } } export function replDbg(scope, ...parts) { if (!isReplDebug()) return const line = `[${ts()}] [bare-os DEBUG:${scope}] ${parts.map(serialize).join(' ')}` writeDebugLine(line) } /** * Human-readable key / escape sequence for logs. * @param {string} key */ export function replDbgKeyRepr(key) { if (key === '\n') return '\\n' if (key === '\r') return '\\r' if (key === '\t') return '\\t' if (key === '\u007f') return 'DEL' if (key === '\u0003') return '^C' if (key === '\u0004') return '^D' if (key === '\u000c') return '^L' if (key === '\u0012' || (key.length === 1 && key.charCodeAt(0) === 18)) return '^R' if (key.startsWith('\u001b')) { return `CSI(${key.length}ch)=${JSON.stringify(key)}` } if (key.length === 1) { const c = key.charCodeAt(0) if (c < 32) return `^${String.fromCharCode(c + 64)}(u${c})` return key } return JSON.stringify(key.length > 120 ? key.slice(0, 120) + '…' : key) } /** * @param {string} label * @param {string} chunk * @param {number} [maxLen] */ export function replDbgChunk(label, chunk, maxLen = 256) { if (!isReplDebug()) return const s = typeof chunk === 'string' ? chunk : String(chunk) const t = s.length > maxLen ? s.slice(0, maxLen) + `…(+${s.length - maxLen})` : s const codes = [...t].slice(0, 80).map((ch) => { const u = ch.charCodeAt(0) if (u < 32 || u === 127) return `\\x${u.toString(16).padStart(2, '0')}` return ch }) replDbg('fish-stdin', label, `len=${s.length}`, `head=${JSON.stringify(codes.join(''))}`) }