File size: 1,579 Bytes
513a1ec
 
 
 
 
 
 
 
 
 
fce80d1
513a1ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Structured logger with event emission for TUI consumption.
// Levels: debug < info < warn < error
// In headless mode, writes to stderr (keeps stdout clean for piping).
// In TUI mode, emits events consumed by the dashboard.

import { EventEmitter } from 'node:events';

const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
const COLORS = { debug: '\x1b[90m', info: '\x1b[36m', warn: '\x1b[33m', error: '\x1b[31m' };
const RESET = '\x1b[0m';
const ICONS = { debug: '·', info: '●', warn: '', error: '' };

class Logger extends EventEmitter {
  #level = LEVELS.info;
  #quiet = false;

  setLevel(level) {
    this.#level = LEVELS[level] ?? LEVELS.info;
  }

  /** Suppress console output (TUI mode — events only). */
  quiet(on = true) {
    this.#quiet = on;
  }

  #log(level, msg, meta) {
    if (LEVELS[level] < this.#level) return;
    const ts = new Date();
    const entry = {
      ts,
      time: ts.toLocaleTimeString('en-GB', { hour12: false }),
      level,
      icon: ICONS[level],
      msg,
      meta,
    };
    this.emit('entry', entry);
    if (!this.#quiet) {
      const prefix = `${COLORS[level]}${ICONS[level]}${RESET}`;
      const metaStr = meta ? ` ${COLORS.debug}${JSON.stringify(meta)}${RESET}` : '';
      process.stderr.write(`  ${entry.time}  ${prefix} ${msg}${metaStr}\n`);
    }
  }

  debug(msg, meta) { this.#log('debug', msg, meta); }
  info(msg, meta)  { this.#log('info', msg, meta); }
  warn(msg, meta)  { this.#log('warn', msg, meta); }
  error(msg, meta) { this.#log('error', msg, meta); }
}

export const log = new Logger();