@loglayer/transport-pretty-terminal 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1525 @@
1
+ // src/PrettyTerminalTransport.ts
2
+ import { LoggerlessTransport } from "@loglayer/transport";
3
+ import chalk5 from "chalk";
4
+
5
+ // src/LogRenderer.ts
6
+ import chalk2 from "chalk";
7
+ import truncate from "cli-truncate";
8
+ import wrap from "wrap-ansi";
9
+
10
+ // src/vendor/prettyjson.js
11
+ import chalk from "chalk";
12
+
13
+ // src/vendor/utils.js
14
+ function indent(numSpaces) {
15
+ return new Array(numSpaces + 1).join(" ");
16
+ }
17
+ function getMaxIndexLength(input) {
18
+ var maxWidth = 0;
19
+ Object.getOwnPropertyNames(input).forEach(function(key) {
20
+ if (input[key] === void 0) {
21
+ return;
22
+ }
23
+ maxWidth = Math.max(maxWidth, key.length);
24
+ });
25
+ return maxWidth;
26
+ }
27
+ function isIsoStringDate(isoString) {
28
+ if (typeof isoString !== "string") {
29
+ return false;
30
+ }
31
+ if (!/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/.test(isoString)) {
32
+ return false;
33
+ }
34
+ const testDate = new Date(isoString);
35
+ return !Number.isNaN(testDate.getTime());
36
+ }
37
+
38
+ // src/vendor/prettyjson.js
39
+ var conflictChars = /[^\w\s\n\r\v\t\.,]/i;
40
+ var isPrintable = (input, options) => input !== void 0 || options.renderUndefined;
41
+ var isSerializable = (input, onlyPrimitives, options) => {
42
+ if (typeof input === "boolean" || typeof input === "number" || typeof input === "function" || input === null || input === void 0 || input instanceof Date) {
43
+ return true;
44
+ }
45
+ if (typeof input === "string" && input.indexOf("\n") === -1) {
46
+ return true;
47
+ }
48
+ if (options.inlineArrays && !onlyPrimitives) {
49
+ if (Array.isArray(input) && isSerializable(input[0], true, options)) {
50
+ return true;
51
+ }
52
+ }
53
+ return false;
54
+ };
55
+ var getColorRenderer = (colorFn, options) => {
56
+ if (options.noColor) {
57
+ return (text) => text;
58
+ }
59
+ if (typeof colorFn !== "function") {
60
+ return (text) => text;
61
+ }
62
+ return colorFn;
63
+ };
64
+ var addColorToData = (input, options) => {
65
+ if (options.noColor) {
66
+ return input;
67
+ }
68
+ if (input instanceof Date) {
69
+ return getColorRenderer(options.dateColor, options)(input.toISOString());
70
+ }
71
+ if (typeof input === "string") {
72
+ if (isIsoStringDate(input)) {
73
+ return getColorRenderer(options.dateColor, options)(input);
74
+ }
75
+ return getColorRenderer(options.stringColor, options)(input);
76
+ }
77
+ const sInput = `${input}`;
78
+ if (typeof input === "boolean") {
79
+ return getColorRenderer(options.booleanColor, options)(sInput);
80
+ }
81
+ if (input === null || input === void 0) {
82
+ return getColorRenderer(options.nullUndefinedColor, options)(sInput);
83
+ }
84
+ if (typeof input === "number") {
85
+ if (input >= 0) {
86
+ return getColorRenderer(options.positiveNumberColor, options)(sInput);
87
+ }
88
+ return getColorRenderer(options.negativeNumberColor, options)(sInput);
89
+ }
90
+ if (typeof input === "function") {
91
+ return "function() {}";
92
+ }
93
+ if (Array.isArray(input)) {
94
+ return input.join(", ");
95
+ }
96
+ return sInput;
97
+ };
98
+ var colorMultilineString = (options, line) => getColorRenderer(options.multilineStringColor, options)(line);
99
+ var indentLines = (string, spaces, options) => {
100
+ let lines = string.split("\n");
101
+ lines = lines.map((line) => indent(spaces) + colorMultilineString(options, line));
102
+ return lines.join("\n");
103
+ };
104
+ var renderToArray = (data, options, indentation) => {
105
+ if (typeof data === "string" && data.match(conflictChars) && options.escape) {
106
+ data = JSON.stringify(data);
107
+ }
108
+ if (!isPrintable(data, options)) {
109
+ return [];
110
+ }
111
+ if (isSerializable(data, false, options)) {
112
+ return [indent(indentation) + addColorToData(data, options)];
113
+ }
114
+ if (typeof data === "string") {
115
+ return [
116
+ indent(indentation) + colorMultilineString(options, '"""'),
117
+ indentLines(data, indentation + options.defaultIndentation, options),
118
+ indent(indentation) + colorMultilineString(options, '"""')
119
+ ];
120
+ }
121
+ if (Array.isArray(data)) {
122
+ if (data.length === 0) {
123
+ return [indent(indentation) + options.emptyArrayMsg];
124
+ }
125
+ if (options.collapseArrays && data.length > 0) {
126
+ return [indent(indentation) + `[... ${data.length} items]`];
127
+ }
128
+ const outputArray = [];
129
+ data.forEach((element) => {
130
+ if (!isPrintable(element, options)) {
131
+ return;
132
+ }
133
+ let line = "- ";
134
+ line = getColorRenderer(options.dashColor, options)(line);
135
+ line = indent(indentation) + line;
136
+ if (isSerializable(element, false, options)) {
137
+ line += renderToArray(element, options, 0)[0];
138
+ outputArray.push(line);
139
+ } else {
140
+ outputArray.push(line);
141
+ outputArray.push.apply(outputArray, renderToArray(element, options, indentation + options.defaultIndentation));
142
+ }
143
+ });
144
+ return outputArray;
145
+ }
146
+ if (data instanceof Error) {
147
+ return renderToArray(
148
+ {
149
+ message: data.message,
150
+ stack: data.stack.split("\n")
151
+ },
152
+ options,
153
+ indentation
154
+ );
155
+ }
156
+ const maxIndexLength = options.noAlign ? 0 : getMaxIndexLength(data);
157
+ let key;
158
+ const output = [];
159
+ Object.getOwnPropertyNames(data).forEach((i) => {
160
+ if (!isPrintable(data[i], options)) {
161
+ return;
162
+ }
163
+ key = `${i}: `;
164
+ key = getColorRenderer(options.keysColor, options)(key);
165
+ key = indent(indentation) + key;
166
+ if (isSerializable(data[i], false, options)) {
167
+ const nextIndentation = options.noAlign ? 0 : maxIndexLength - i.length;
168
+ key += renderToArray(data[i], options, nextIndentation)[0];
169
+ output.push(key);
170
+ } else {
171
+ output.push(key);
172
+ output.push.apply(output, renderToArray(data[i], options, indentation + options.defaultIndentation));
173
+ }
174
+ });
175
+ return output;
176
+ };
177
+ var validateOptionsAndSetDefaults = (options) => {
178
+ options = options || {};
179
+ options.emptyArrayMsg = options.emptyArrayMsg || "(empty array)";
180
+ options.keysColor = options.keysColor || chalk.green;
181
+ options.dashColor = options.dashColor || chalk.green;
182
+ options.booleanColor = options.booleanColor || chalk.cyan;
183
+ options.nullUndefinedColor = options.nullUndefinedColor || chalk.grey;
184
+ options.numberColor = options.numberColor || chalk.blue;
185
+ options.positiveNumberColor = options.positiveNumberColor || options.numberColor;
186
+ options.negativeNumberColor = options.negativeNumberColor || options.numberColor;
187
+ options.dateColor = options.dateColor || chalk.magenta;
188
+ options.defaultIndentation = options.defaultIndentation || 2;
189
+ options.noColor = !!options.noColor;
190
+ options.noAlign = !!options.noAlign;
191
+ options.escape = !!options.escape;
192
+ options.renderUndefined = !!options.renderUndefined;
193
+ options.collapseArrays = !!options.collapseArrays;
194
+ options.stringColor = options.stringColor || null;
195
+ options.multilineStringColor = options.multilineStringColor || options.stringColor || null;
196
+ return options;
197
+ };
198
+ function render(data, options, indentation) {
199
+ indentation = indentation || 0;
200
+ options = validateOptionsAndSetDefaults(options);
201
+ return renderToArray(data, options, indentation).join("\n");
202
+ }
203
+
204
+ // src/LogRenderer.ts
205
+ var LogRenderer = class {
206
+ /** Current terminal width in characters */
207
+ termWidth;
208
+ /** Maximum depth for displaying nested objects inline */
209
+ maxInlineDepth;
210
+ /** Maximum length for inline data before truncating */
211
+ maxInlineLength;
212
+ /** Whether arrays are currently collapsed in detail view */
213
+ isArraysCollapsed = false;
214
+ /** Color and formatting config for simple log view */
215
+ simpleViewConfig;
216
+ /** Color and formatting config for detailed view */
217
+ detailedViewConfig;
218
+ /** Current scroll position in detailed view */
219
+ detailViewScrollPos = 0;
220
+ /** Cached content lines for detailed view */
221
+ detailViewContent = [];
222
+ /**
223
+ * Creates a new LogRenderer instance.
224
+ *
225
+ * @param simpleViewConfig - Configuration for simple log view
226
+ * @param detailedViewConfig - Configuration for detailed view
227
+ * @param maxInlineDepth - Maximum depth for inline object display
228
+ * @param maxInlineLength - Maximum length before truncation
229
+ */
230
+ constructor(simpleViewConfig, detailedViewConfig, maxInlineDepth, maxInlineLength) {
231
+ this.simpleViewConfig = simpleViewConfig;
232
+ this.detailedViewConfig = detailedViewConfig;
233
+ this.maxInlineDepth = maxInlineDepth;
234
+ this.maxInlineLength = maxInlineLength;
235
+ this.termWidth = process.stdout.columns || 80;
236
+ process.stdout.on("resize", () => {
237
+ this.termWidth = process.stdout.columns || 80;
238
+ });
239
+ }
240
+ /**
241
+ * Gets the appropriate color for a log level.
242
+ * Handles both simple and detailed view color schemes.
243
+ *
244
+ * @param level - Log level (trace, debug, info, etc.)
245
+ * @param isDetailView - Whether to use detailed view colors
246
+ * @returns Chalk instance for the color
247
+ */
248
+ getLevelColor(level, isDetailView = false) {
249
+ const colors = isDetailView ? this.detailedViewConfig.colors : this.simpleViewConfig.colors;
250
+ return colors[level] || chalk2.white;
251
+ }
252
+ /**
253
+ * Formats a timestamp into a human-readable string.
254
+ * Format: HH:MM:SS.mmm
255
+ *
256
+ * @param timestamp - Unix timestamp in milliseconds
257
+ * @returns Formatted and colored timestamp string
258
+ */
259
+ formatTimestamp(timestamp) {
260
+ const date = new Date(timestamp);
261
+ return this.simpleViewConfig.logIdColor(
262
+ `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}.${date.getMilliseconds().toString().padStart(3, "0")}`
263
+ );
264
+ }
265
+ /**
266
+ * Formats structured data for inline display.
267
+ * Handles nested objects, arrays, and primitive values.
268
+ * Truncates output to maintain readability.
269
+ *
270
+ * @param data - Data to format
271
+ * @returns Formatted string with color coding
272
+ */
273
+ formatInlineData(data) {
274
+ if (!data) return "";
275
+ const formatValue = (value) => {
276
+ if (typeof value === "string") return value;
277
+ if (typeof value === "number" || typeof value === "boolean") return value.toString();
278
+ if (value === null) return "null";
279
+ if (value === void 0) return "undefined";
280
+ if (Array.isArray(value)) return "[...]";
281
+ if (typeof value === "object") return "{...}";
282
+ return value.toString();
283
+ };
284
+ const pairs = [];
285
+ const traverse = (obj, prefix = "", depth = 0) => {
286
+ if (depth >= this.maxInlineDepth) return;
287
+ for (const [key, value] of Object.entries(obj)) {
288
+ const fullKey = prefix ? `${prefix}.${key}` : key;
289
+ if (value && typeof value === "object" && !Array.isArray(value)) {
290
+ traverse(value, fullKey, depth + 1);
291
+ } else {
292
+ pairs.push(
293
+ `${this.simpleViewConfig.dataKeyColor(fullKey)}=${this.simpleViewConfig.dataValueColor(formatValue(value))}`
294
+ );
295
+ }
296
+ }
297
+ };
298
+ traverse(data);
299
+ const result = pairs.join(" ");
300
+ return truncate(result, this.maxInlineLength);
301
+ }
302
+ /**
303
+ * Renders a single log line in simple view format.
304
+ * Format: [timestamp] LEVEL [id] message data
305
+ *
306
+ * @param entry - Log entry to render
307
+ */
308
+ renderLogLine(entry) {
309
+ const levelColor = this.getLevelColor(entry.level);
310
+ const timestamp = this.formatTimestamp(entry.timestamp);
311
+ const chevron = levelColor(`\u25B6 ${entry.level.toUpperCase()} `);
312
+ const logId = this.simpleViewConfig.logIdColor(`[${entry.id}]`);
313
+ const message = entry.message;
314
+ const data = entry.data ? this.formatInlineData(JSON.parse(entry.data)) : "";
315
+ const line = `${timestamp} ${chevron}${logId} ${message}${data ? ` ${data}` : ""}`;
316
+ console.log(wrap(line, this.termWidth, { hard: true }));
317
+ }
318
+ /**
319
+ * Formats a compact single-line version of a log entry.
320
+ * Used for showing context in detailed view.
321
+ *
322
+ * @param entry - Log entry to render
323
+ * @param prefix - Optional prefix (e.g., "←" for previous entry)
324
+ * @returns Formatted log line
325
+ */
326
+ formatCompactLogLine(entry, prefix = "") {
327
+ if (!entry) return "";
328
+ const levelColor = this.getLevelColor(entry.level, true);
329
+ const logId = this.detailedViewConfig.logIdColor(`[${entry.id}]`);
330
+ const line = `${prefix}${levelColor(entry.level.toUpperCase())} ${logId} ${entry.message}`;
331
+ return this.detailedViewConfig.separatorColor(wrap(line, this.termWidth, { hard: true }));
332
+ }
333
+ /**
334
+ * Renders the detailed view of a log entry.
335
+ * Shows full entry details with context and formatted data.
336
+ *
337
+ * @param entry - Log entry to show in detail
338
+ * @param prevEntry - Previous entry for context
339
+ * @param nextEntry - Next entry for context
340
+ * @param scrollPos - Current scroll position
341
+ */
342
+ renderDetailView(entry, prevEntry, nextEntry, scrollPos = 0) {
343
+ console.clear();
344
+ if (this.isJsonView) {
345
+ if (entry.data) {
346
+ console.log(JSON.stringify(JSON.parse(entry.data)));
347
+ } else {
348
+ console.log("{}");
349
+ }
350
+ console.log(`
351
+ ${this.detailedViewConfig.separatorColor("TAB to return to detailed view")}`);
352
+ return;
353
+ }
354
+ const headerContent = [];
355
+ if (prevEntry) {
356
+ const prevLine = this.formatCompactLogLine(prevEntry, "\u2190 ");
357
+ headerContent.push(prevLine);
358
+ headerContent.push(this.detailedViewConfig.separatorColor("\u2500".repeat(this.termWidth)));
359
+ }
360
+ const footerContent = [];
361
+ if (nextEntry) {
362
+ footerContent.push(this.detailedViewConfig.separatorColor("\u2500".repeat(this.termWidth)));
363
+ const nextLine = this.formatCompactLogLine(nextEntry, "\u2192 ");
364
+ footerContent.push(nextLine);
365
+ }
366
+ footerContent.push("");
367
+ footerContent.push(
368
+ this.detailedViewConfig.separatorColor("TAB to exit \u2022 \u2191/\u2193 scroll \u2022 Q/W page up/down \u2022 J raw JSON")
369
+ );
370
+ footerContent.push(this.detailedViewConfig.separatorColor("\u2190/\u2192 navigate \u2022 A/S first/last log \u2022 C toggle arrays"));
371
+ const mainContent = [];
372
+ const levelColor = this.getLevelColor(entry.level, true);
373
+ const header = levelColor(`=== Log Detail [${entry.id}] ===`);
374
+ mainContent.push(header);
375
+ const timestamp = new Date(entry.timestamp);
376
+ const isoTime = timestamp.toISOString();
377
+ const relativeTime = this.formatRelativeTime(timestamp);
378
+ mainContent.push(
379
+ `${this.detailedViewConfig.labelColor("Timestamp:")} ${this.detailedViewConfig.dataValueColor(isoTime)} (${relativeTime})`
380
+ );
381
+ mainContent.push(`${this.detailedViewConfig.labelColor("Level:")} ${levelColor.bold(entry.level.toUpperCase())}`);
382
+ mainContent.push(
383
+ `${this.detailedViewConfig.labelColor("Message:")} ${this.detailedViewConfig.dataValueColor(entry.message)}`
384
+ );
385
+ if (entry.data) {
386
+ mainContent.push(this.detailedViewConfig.labelColor("\nData:"));
387
+ const jsonLines = render(JSON.parse(entry.data), {
388
+ ...this.detailedViewConfig.jsonColors,
389
+ defaultIndentation: 2,
390
+ collapseArrays: this.isArraysCollapsed
391
+ }).split("\n");
392
+ mainContent.push(...jsonLines);
393
+ }
394
+ this.detailViewContent = mainContent;
395
+ const headerHeight = headerContent.length;
396
+ const footerHeight = footerContent.length;
397
+ const scrollIndicatorLines = 2;
398
+ const bufferSpace = 2;
399
+ const availableHeight = Math.max(
400
+ 0,
401
+ process.stdout.rows - headerHeight - footerHeight - scrollIndicatorLines - bufferSpace
402
+ );
403
+ this.detailViewScrollPos = Math.max(0, Math.min(scrollPos, mainContent.length - availableHeight));
404
+ for (const line of headerContent) {
405
+ console.log(line);
406
+ }
407
+ if (this.detailViewScrollPos > 0) {
408
+ console.log(chalk2.dim("\u2191 More content above"));
409
+ }
410
+ const startIndex = this.detailViewScrollPos;
411
+ const visibleContent = mainContent.slice(startIndex, startIndex + availableHeight);
412
+ for (const line of visibleContent) {
413
+ console.log(line);
414
+ }
415
+ if (this.detailViewScrollPos + availableHeight < mainContent.length) {
416
+ console.log(chalk2.dim("\u2193 More content below"));
417
+ }
418
+ for (const line of footerContent) {
419
+ console.log(line);
420
+ }
421
+ }
422
+ /**
423
+ * Updates the scroll position in detailed view
424
+ * @param delta - Number of lines to scroll (positive for down, negative for up)
425
+ */
426
+ scrollDetailView(delta) {
427
+ const headerHeight = 2;
428
+ const footerHeight = 5;
429
+ const scrollIndicatorLines = 2;
430
+ const bufferSpace = 2;
431
+ const availableHeight = Math.max(
432
+ 0,
433
+ process.stdout.rows - headerHeight - footerHeight - scrollIndicatorLines - bufferSpace
434
+ );
435
+ const maxScroll = Math.max(0, this.detailViewContent.length - availableHeight);
436
+ this.detailViewScrollPos = Math.max(0, Math.min(maxScroll, this.detailViewScrollPos + delta));
437
+ }
438
+ /**
439
+ * Gets the current scroll position
440
+ */
441
+ getDetailViewScrollPos() {
442
+ return this.detailViewScrollPos;
443
+ }
444
+ /**
445
+ * Renders the interactive selection view.
446
+ * Shows a scrollable list of logs with current selection.
447
+ *
448
+ * Features:
449
+ * - Pagination for large log sets
450
+ * - Active filter display
451
+ * - Scroll indicators
452
+ * - Selected item highlighting
453
+ *
454
+ * @param logs - Array of log entries to display
455
+ * @param selectedIndex - Index of currently selected log
456
+ * @param filterText - Current filter text (if any)
457
+ * @param newLogCount - Number of new logs since last render (optional)
458
+ */
459
+ renderSelectionView(logs, selectedIndex, filterText, newLogCount = 0) {
460
+ console.clear();
461
+ const headerHeight = filterText ? 2 : 0;
462
+ const footerHeight = 2;
463
+ const availableHeight = process.stdout.rows - headerHeight - footerHeight;
464
+ if (filterText) {
465
+ console.log(chalk2.cyan("Filter:"), chalk2.white(filterText));
466
+ console.log(chalk2.dim("\u2500".repeat(this.termWidth)));
467
+ }
468
+ if (logs.length === 0) {
469
+ console.log(chalk2.yellow("No matching logs found"));
470
+ } else {
471
+ const visibleLines = Math.min(availableHeight, 20);
472
+ const bottomPadding = 2;
473
+ let startIdx = Math.max(0, selectedIndex - (visibleLines - bottomPadding - 1));
474
+ const endIdx = Math.min(logs.length, startIdx + visibleLines);
475
+ if (endIdx - startIdx < visibleLines && endIdx < logs.length) {
476
+ startIdx = Math.max(0, endIdx - visibleLines);
477
+ }
478
+ if (startIdx > 0) {
479
+ console.log(chalk2.dim(" \u2191 More logs above"));
480
+ }
481
+ logs.slice(startIdx, endIdx).forEach((entry, index) => {
482
+ const actualIndex = startIdx + index;
483
+ const isSelected = actualIndex === selectedIndex;
484
+ const prefix = isSelected ? this.simpleViewConfig.selectorColor("\u25BA ") : " ";
485
+ const timestamp = this.formatTimestamp(entry.timestamp);
486
+ const levelColor = this.getLevelColor(entry.level);
487
+ const chevron = levelColor(`${entry.level.toUpperCase()} `);
488
+ const logId = chalk2.dim(`[${entry.id}]`);
489
+ const message = entry.message;
490
+ const data = entry.data ? this.formatInlineData(JSON.parse(entry.data)) : "";
491
+ const mainLine = `${prefix}${timestamp} ${chevron}${logId} ${message}`;
492
+ console.log(wrap(mainLine, this.termWidth - 2, { hard: true }));
493
+ if (data) {
494
+ const wrappedData = wrap(data, this.termWidth - (isSelected ? 6 : 2), { hard: true });
495
+ const indentedLines = wrappedData.split("\n").map((line) => isSelected ? ` ${line}` : line);
496
+ console.log(indentedLines.join("\n"));
497
+ }
498
+ });
499
+ if (endIdx < logs.length || newLogCount > 0) {
500
+ const moreLogsText = newLogCount > 0 ? this.simpleViewConfig.selectorColor(
501
+ ` \u2193 ${newLogCount} new log${newLogCount === 1 ? "" : "s"} available (press \u2193 to view)`
502
+ ) : chalk2.dim(" \u2193 More logs below");
503
+ console.log(moreLogsText);
504
+ }
505
+ }
506
+ console.log(chalk2.dim("\nType to filter \u2022 Enter to view details \u2022 TAB to exit"));
507
+ }
508
+ /**
509
+ * Formats a timestamp into a human-readable relative time
510
+ * @param timestamp - Date to format
511
+ * @returns Formatted relative time string
512
+ */
513
+ formatRelativeTime(timestamp) {
514
+ const now = /* @__PURE__ */ new Date();
515
+ const diffMs = now.getTime() - timestamp.getTime();
516
+ const diffSecs = Math.floor(diffMs / 1e3);
517
+ const diffMins = Math.floor(diffSecs / 60);
518
+ const diffHours = Math.floor(diffMins / 60);
519
+ const diffDays = Math.floor(diffHours / 24);
520
+ if (diffSecs < 60) return `${diffSecs}s ago`;
521
+ if (diffMins < 60) return `${diffMins}m ago`;
522
+ if (diffHours < 24) return `${diffHours}h ago`;
523
+ if (diffDays === 1) return "yesterday";
524
+ if (diffDays < 30) return `${diffDays}d ago`;
525
+ return timestamp.toLocaleDateString();
526
+ }
527
+ /** Whether we're showing raw JSON view */
528
+ isJsonView = false;
529
+ /**
530
+ * Toggles JSON view state
531
+ */
532
+ toggleJsonView() {
533
+ this.isJsonView = !this.isJsonView;
534
+ }
535
+ /**
536
+ * Gets current JSON view state
537
+ */
538
+ isInJsonView() {
539
+ return this.isJsonView;
540
+ }
541
+ /**
542
+ * Toggles array collapse state in detail view
543
+ */
544
+ toggleArrayCollapse() {
545
+ this.isArraysCollapsed = !this.isArraysCollapsed;
546
+ }
547
+ };
548
+
549
+ // src/LogStorage.ts
550
+ import { resolve } from "node:path";
551
+ import Database from "better-sqlite3";
552
+ var LogStorage = class {
553
+ /** SQLite database instance */
554
+ db;
555
+ /**
556
+ * Creates a new LogStorage instance.
557
+ * Initializes a SQLite database and creates the logs table.
558
+ *
559
+ * @param logFile - Optional path to SQLite file for persistent storage.
560
+ * If not provided, uses in-memory database.
561
+ * Relative paths are resolved from the current working directory.
562
+ */
563
+ constructor(logFile) {
564
+ const dbPath = logFile ? logFile.startsWith("/") ? logFile : resolve(process.cwd(), logFile) : ":memory:";
565
+ this.db = new Database(dbPath);
566
+ this.initializeDatabase();
567
+ }
568
+ /**
569
+ * Initializes the database schema.
570
+ * Creates the logs table with appropriate columns and indexes.
571
+ * If the table already exists, it will be dropped to ensure a clean state.
572
+ * @private
573
+ */
574
+ initializeDatabase() {
575
+ this.db.exec(`
576
+ DROP TABLE IF EXISTS logs;
577
+ CREATE TABLE logs (
578
+ id TEXT PRIMARY KEY,
579
+ timestamp INTEGER,
580
+ level TEXT,
581
+ message TEXT,
582
+ data TEXT
583
+ )
584
+ `);
585
+ }
586
+ /**
587
+ * Stores a new log entry in the database.
588
+ * Uses prepared statements for efficient and safe insertion.
589
+ *
590
+ * @param entry - The log entry to store
591
+ * @example
592
+ * storage.store({
593
+ * id: "abc123",
594
+ * timestamp: Date.now(),
595
+ * level: "info",
596
+ * message: "User logged in",
597
+ * data: JSON.stringify({ userId: 123 })
598
+ * });
599
+ */
600
+ store(entry) {
601
+ this.db.prepare(`
602
+ INSERT INTO logs (id, timestamp, level, message, data)
603
+ VALUES (?, ?, ?, ?, ?)
604
+ `).run(entry.id, entry.timestamp, entry.level, entry.message, entry.data);
605
+ }
606
+ /**
607
+ * Retrieves all logs from the database in chronological order.
608
+ * Used when displaying the full log history or clearing filters.
609
+ *
610
+ * @returns Array of log entries sorted by timestamp
611
+ */
612
+ getAllLogs() {
613
+ return this.db.prepare("SELECT * FROM logs ORDER BY timestamp ASC").all();
614
+ }
615
+ /**
616
+ * Searches logs based on a text query.
617
+ * Performs a case-insensitive search across multiple fields:
618
+ * - Log ID
619
+ * - Message content
620
+ * - Structured data
621
+ *
622
+ * @param searchText - Text to search for
623
+ * @returns Array of matching log entries sorted by timestamp
624
+ * @example
625
+ * const errorLogs = storage.searchLogs("error");
626
+ * const userLogs = storage.searchLogs("userId:123");
627
+ */
628
+ searchLogs(searchText) {
629
+ const query = `
630
+ SELECT * FROM logs
631
+ WHERE id LIKE ?
632
+ OR message LIKE ?
633
+ OR data LIKE ?
634
+ ORDER BY timestamp ASC
635
+ `;
636
+ const pattern = `%${searchText}%`;
637
+ return this.db.prepare(query).all(pattern, pattern, pattern);
638
+ }
639
+ /**
640
+ * Closes the database connection and performs cleanup.
641
+ * Should be called when the application exits or the transport is destroyed.
642
+ */
643
+ close() {
644
+ this.db.close();
645
+ }
646
+ };
647
+
648
+ // src/UIManager.ts
649
+ import chalk3 from "chalk";
650
+ import keypress from "keypress";
651
+ var UIManager = class {
652
+ /**
653
+ * Creates a new UIManager instance.
654
+ * Sets up keyboard handling and cleanup handlers.
655
+ *
656
+ * @param renderer - Instance for handling log display
657
+ * @param storage - Instance for log persistence
658
+ */
659
+ constructor(renderer, storage) {
660
+ this.renderer = renderer;
661
+ this.storage = storage;
662
+ this.setupKeyboardHandling();
663
+ process.stdout.on("resize", () => {
664
+ this.termWidth = process.stdout.columns || 80;
665
+ if (this.isDetailView) {
666
+ const entry = this.logs[this.selectedIndex];
667
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
668
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
669
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
670
+ } else if (this.isSelectionMode) {
671
+ this.renderer.renderSelectionView(this.logs, this.selectedIndex, this.filterText, this.selectionBuffer.length);
672
+ }
673
+ });
674
+ }
675
+ /** Whether the UI is in log selection mode */
676
+ isSelectionMode = false;
677
+ /** Whether the UI is showing detailed log view */
678
+ isDetailView = false;
679
+ /** Whether log streaming is paused */
680
+ isPaused = false;
681
+ /** Buffer for logs received while paused */
682
+ pauseBuffer = [];
683
+ /** Buffer for new logs in selection mode */
684
+ selectionBuffer = [];
685
+ /** Index of currently selected log entry */
686
+ selectedIndex = 0;
687
+ /** Current filter text for log searching */
688
+ filterText = "";
689
+ /** Cached array of filtered log entries */
690
+ logs = [];
691
+ /** Polling interval for checking new logs in detail view */
692
+ detailViewPollInterval = null;
693
+ /** Polling interval for checking new logs in selection view */
694
+ selectionViewPollInterval = null;
695
+ /** Current terminal width in characters */
696
+ termWidth = process.stdout.columns || 80;
697
+ /**
698
+ * Sets up keyboard input handling and cleanup.
699
+ * Configures raw mode for immediate key processing.
700
+ * Sets up process exit handlers for cleanup.
701
+ * @private
702
+ */
703
+ setupKeyboardHandling() {
704
+ keypress(process.stdin);
705
+ process.stdin.setRawMode(true);
706
+ process.stdin.on("keypress", this.handleKeypress.bind(this));
707
+ const cleanup = () => {
708
+ if (this.detailViewPollInterval) {
709
+ clearInterval(this.detailViewPollInterval);
710
+ }
711
+ if (this.selectionViewPollInterval) {
712
+ clearInterval(this.selectionViewPollInterval);
713
+ }
714
+ process.stdin.setRawMode(false);
715
+ process.stdin.removeAllListeners("keypress");
716
+ console.clear();
717
+ this.storage.close();
718
+ process.exit(0);
719
+ };
720
+ process.on("SIGINT", cleanup);
721
+ process.on("SIGTERM", cleanup);
722
+ }
723
+ /**
724
+ * Starts polling for new logs in selection view
725
+ * @private
726
+ */
727
+ startSelectionViewPolling() {
728
+ if (this.selectionViewPollInterval) {
729
+ clearInterval(this.selectionViewPollInterval);
730
+ }
731
+ this.selectionViewPollInterval = setInterval(() => {
732
+ if (!this.isSelectionMode) {
733
+ clearInterval(this.selectionViewPollInterval);
734
+ this.selectionViewPollInterval = null;
735
+ return;
736
+ }
737
+ if (this.selectionBuffer.length > 0) {
738
+ this.renderer.renderSelectionView(this.logs, this.selectedIndex, this.filterText, this.selectionBuffer.length);
739
+ }
740
+ }, 2e3);
741
+ }
742
+ /**
743
+ * Starts polling for new logs in detail view
744
+ * @private
745
+ */
746
+ startDetailViewPolling() {
747
+ if (this.detailViewPollInterval) {
748
+ clearInterval(this.detailViewPollInterval);
749
+ }
750
+ this.detailViewPollInterval = setInterval(() => {
751
+ if (!this.isDetailView) {
752
+ clearInterval(this.detailViewPollInterval);
753
+ this.detailViewPollInterval = null;
754
+ return;
755
+ }
756
+ const currentLogs = this.storage.getAllLogs();
757
+ if (currentLogs.length > this.logs.length && this.selectedIndex === this.logs.length - 1) {
758
+ this.logs = currentLogs;
759
+ const entry = this.logs[this.selectedIndex];
760
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
761
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
762
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
763
+ }
764
+ }, 2e3);
765
+ }
766
+ /**
767
+ * Updates the filtered list of logs based on search text.
768
+ * Handles both full list and filtered views.
769
+ * Maintains selection state when filter changes.
770
+ * @private
771
+ */
772
+ updateFilteredLogs() {
773
+ const storedLogs = this.filterText ? this.storage.searchLogs(this.filterText) : this.storage.getAllLogs();
774
+ this.logs = storedLogs.slice(0, storedLogs.length - this.selectionBuffer.length);
775
+ if (this.logs.length > 0) {
776
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.logs.length - 1));
777
+ } else {
778
+ this.selectedIndex = 0;
779
+ }
780
+ }
781
+ updateNewLogsNotification() {
782
+ if (!this.isSelectionMode || !this.selectionBuffer.length) return;
783
+ process.stdout.write(`\x1B[2A\r${" ".repeat(this.termWidth)}\r`);
784
+ const notification = chalk3.dim(
785
+ `${this.selectionBuffer.length} new log${this.selectionBuffer.length === 1 ? "" : "s"} available (press \u2193 to view)`
786
+ );
787
+ process.stdout.write(`${notification}
788
+
789
+ `);
790
+ }
791
+ /**
792
+ * Handles new log entries from the transport.
793
+ * Stores the log and updates the display if needed.
794
+ *
795
+ * @param entry - New log entry to process
796
+ */
797
+ handleNewLog(entry) {
798
+ this.storage.store(entry);
799
+ if (this.isSelectionMode) {
800
+ this.selectionBuffer.push(entry);
801
+ this.renderer.renderSelectionView(this.logs, this.selectedIndex, this.filterText, this.selectionBuffer.length);
802
+ return;
803
+ }
804
+ if (!this.isDetailView) {
805
+ if (this.isPaused) {
806
+ this.pauseBuffer.push(entry);
807
+ process.stdout.write(`\r${chalk3.yellow(`\u23F8 Paused (${this.pauseBuffer.length} new logs)`)}${" ".repeat(20)}`);
808
+ } else {
809
+ this.renderer.renderLogLine(entry);
810
+ }
811
+ }
812
+ }
813
+ /**
814
+ * Processes keyboard input and manages UI state.
815
+ * Handles navigation, mode switching, and filtering.
816
+ *
817
+ * Key mappings:
818
+ * - CTRL+C: Exit application
819
+ * - TAB: Toggle selection mode
820
+ * - Up/Down: Navigate logs
821
+ * - Enter: View log details
822
+ * - Backspace: Edit filter
823
+ * - P: Toggle pause in simple view
824
+ * - Other keys: Add to filter
825
+ *
826
+ * @param ch - Character input if available
827
+ * @param key - Key event information
828
+ * @private
829
+ */
830
+ handleKeypress(ch, key) {
831
+ if (key?.ctrl && key?.name === "c") {
832
+ process.stdin.setRawMode(false);
833
+ process.stdin.removeAllListeners("keypress");
834
+ console.clear();
835
+ this.storage.close();
836
+ process.exit(0);
837
+ return;
838
+ }
839
+ if (!this.isSelectionMode && !this.isDetailView && ch === "p") {
840
+ this.isPaused = !this.isPaused;
841
+ if (!this.isPaused && this.pauseBuffer.length > 0) {
842
+ process.stdout.write(`\r${" ".repeat(50)}\r`);
843
+ for (const entry of this.pauseBuffer) {
844
+ this.renderer.renderLogLine(entry);
845
+ }
846
+ this.pauseBuffer = [];
847
+ } else if (this.isPaused) {
848
+ process.stdout.write(`\r${chalk3.yellow("\u23F8 Paused (0 new logs)")}${" ".repeat(20)}`);
849
+ }
850
+ return;
851
+ }
852
+ if (!this.isSelectionMode && !this.isDetailView && key?.name === "tab") {
853
+ if (this.isPaused) {
854
+ this.isPaused = false;
855
+ this.pauseBuffer = [];
856
+ process.stdout.write(`\r${" ".repeat(50)}\r`);
857
+ }
858
+ this.isSelectionMode = true;
859
+ this.filterText = "";
860
+ this.updateFilteredLogs();
861
+ this.selectedIndex = Math.max(0, this.logs.length - 1);
862
+ this.renderer.renderSelectionView(this.logs, this.selectedIndex, this.filterText, this.selectionBuffer.length);
863
+ this.startSelectionViewPolling();
864
+ return;
865
+ }
866
+ if (this.isSelectionMode) {
867
+ if (key) {
868
+ switch (key.name) {
869
+ case "up": {
870
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
871
+ this.renderer.renderSelectionView(
872
+ this.logs,
873
+ this.selectedIndex,
874
+ this.filterText,
875
+ this.selectionBuffer.length
876
+ );
877
+ return;
878
+ }
879
+ case "down": {
880
+ if (this.selectedIndex === this.logs.length - 1 && this.selectionBuffer.length > 0) {
881
+ this.logs.push(...this.selectionBuffer);
882
+ this.selectionBuffer = [];
883
+ }
884
+ this.selectedIndex = Math.min(this.logs.length - 1, this.selectedIndex + 1);
885
+ this.renderer.renderSelectionView(
886
+ this.logs,
887
+ this.selectedIndex,
888
+ this.filterText,
889
+ this.selectionBuffer.length
890
+ );
891
+ return;
892
+ }
893
+ case "return": {
894
+ if (this.logs.length > 0) {
895
+ this.isSelectionMode = false;
896
+ this.isDetailView = true;
897
+ console.clear();
898
+ const selectedEntry = this.logs[this.selectedIndex];
899
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
900
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
901
+ this.renderer.renderDetailView(selectedEntry, prevEntry, nextEntry);
902
+ this.startDetailViewPolling();
903
+ }
904
+ return;
905
+ }
906
+ case "backspace": {
907
+ if (this.filterText.length > 0) {
908
+ this.filterText = this.filterText.slice(0, -1);
909
+ this.updateFilteredLogs();
910
+ this.renderer.renderSelectionView(
911
+ this.logs,
912
+ this.selectedIndex,
913
+ this.filterText,
914
+ this.selectionBuffer.length
915
+ );
916
+ }
917
+ return;
918
+ }
919
+ case "tab": {
920
+ this.isSelectionMode = false;
921
+ if (this.selectionViewPollInterval) {
922
+ clearInterval(this.selectionViewPollInterval);
923
+ this.selectionViewPollInterval = null;
924
+ }
925
+ this.filterText = "";
926
+ console.clear();
927
+ const logs = [...this.storage.getAllLogs()].reverse();
928
+ for (const entry of logs) {
929
+ this.renderer.renderLogLine(entry);
930
+ }
931
+ return;
932
+ }
933
+ }
934
+ }
935
+ if (ch && (!key || !key.ctrl && !key.meta) && ch.length === 1) {
936
+ this.filterText += ch;
937
+ this.updateFilteredLogs();
938
+ this.renderer.renderSelectionView(this.logs, this.selectedIndex, this.filterText, this.selectionBuffer.length);
939
+ return;
940
+ }
941
+ }
942
+ if (this.isDetailView) {
943
+ if (key?.name) {
944
+ switch (key.name) {
945
+ case "up": {
946
+ this.renderer.scrollDetailView(-1);
947
+ const entry = this.logs[this.selectedIndex];
948
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
949
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
950
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
951
+ break;
952
+ }
953
+ case "down": {
954
+ this.renderer.scrollDetailView(1);
955
+ const entry = this.logs[this.selectedIndex];
956
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
957
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
958
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
959
+ break;
960
+ }
961
+ case "left": {
962
+ if (this.selectedIndex > 0) {
963
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
964
+ const entry = this.logs[this.selectedIndex];
965
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
966
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
967
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry);
968
+ }
969
+ break;
970
+ }
971
+ case "right": {
972
+ if (this.selectedIndex < this.logs.length - 1) {
973
+ this.selectedIndex = Math.min(this.logs.length - 1, this.selectedIndex + 1);
974
+ const entry = this.logs[this.selectedIndex];
975
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
976
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
977
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry);
978
+ }
979
+ break;
980
+ }
981
+ case "tab": {
982
+ if (this.renderer.isInJsonView()) {
983
+ this.renderer.toggleJsonView();
984
+ const entry = this.logs[this.selectedIndex];
985
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
986
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
987
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
988
+ break;
989
+ }
990
+ this.isDetailView = false;
991
+ if (this.detailViewPollInterval) {
992
+ clearInterval(this.detailViewPollInterval);
993
+ this.detailViewPollInterval = null;
994
+ }
995
+ this.isSelectionMode = true;
996
+ this.updateFilteredLogs();
997
+ this.renderer.renderSelectionView(
998
+ this.logs,
999
+ this.selectedIndex,
1000
+ this.filterText,
1001
+ this.selectionBuffer.length
1002
+ );
1003
+ break;
1004
+ }
1005
+ }
1006
+ }
1007
+ if (ch) {
1008
+ switch (ch.toLowerCase()) {
1009
+ case "w": {
1010
+ this.renderer.scrollDetailView(process.stdout.rows - 8);
1011
+ const entry = this.logs[this.selectedIndex];
1012
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
1013
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
1014
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
1015
+ break;
1016
+ }
1017
+ case "q": {
1018
+ this.renderer.scrollDetailView(-process.stdout.rows + 8);
1019
+ const entry = this.logs[this.selectedIndex];
1020
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
1021
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
1022
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
1023
+ break;
1024
+ }
1025
+ case "a": {
1026
+ if (this.selectedIndex !== 0) {
1027
+ this.selectedIndex = 0;
1028
+ const entry = this.logs[this.selectedIndex];
1029
+ const prevEntry = null;
1030
+ const nextEntry = this.logs.length > 1 ? this.logs[1] : null;
1031
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry);
1032
+ }
1033
+ break;
1034
+ }
1035
+ case "s": {
1036
+ const lastIndex = this.logs.length - 1;
1037
+ if (this.selectedIndex !== lastIndex) {
1038
+ this.selectedIndex = lastIndex;
1039
+ const entry = this.logs[this.selectedIndex];
1040
+ const prevEntry = lastIndex > 0 ? this.logs[lastIndex - 1] : null;
1041
+ const nextEntry = null;
1042
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry);
1043
+ }
1044
+ break;
1045
+ }
1046
+ case "c": {
1047
+ this.renderer.toggleArrayCollapse();
1048
+ const entry = this.logs[this.selectedIndex];
1049
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
1050
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
1051
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry, this.renderer.getDetailViewScrollPos());
1052
+ break;
1053
+ }
1054
+ case "j": {
1055
+ this.renderer.toggleJsonView();
1056
+ const entry = this.logs[this.selectedIndex];
1057
+ const prevEntry = this.selectedIndex > 0 ? this.logs[this.selectedIndex - 1] : null;
1058
+ const nextEntry = this.selectedIndex < this.logs.length - 1 ? this.logs[this.selectedIndex + 1] : null;
1059
+ this.renderer.renderDetailView(entry, prevEntry, nextEntry);
1060
+ break;
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ };
1067
+
1068
+ // src/themes.ts
1069
+ import chalk4 from "chalk";
1070
+ var moonlight = {
1071
+ simpleView: {
1072
+ colors: {
1073
+ trace: chalk4.rgb(114, 135, 153),
1074
+ // Muted blue-grey for less important info
1075
+ debug: chalk4.rgb(130, 170, 255),
1076
+ // Soft blue that pops but doesn't strain
1077
+ info: chalk4.rgb(195, 232, 141),
1078
+ // Sage green for good readability
1079
+ warn: chalk4.rgb(255, 203, 107),
1080
+ // Warm yellow, less harsh than pure yellow
1081
+ error: chalk4.rgb(247, 118, 142),
1082
+ // Soft red that stands out without being aggressive
1083
+ fatal: chalk4.bgRgb(247, 118, 142).white
1084
+ // Inverted soft red for maximum visibility
1085
+ },
1086
+ logIdColor: chalk4.rgb(84, 98, 117),
1087
+ // Darker blue-grey for secondary information
1088
+ dataValueColor: chalk4.rgb(209, 219, 231),
1089
+ // Light grey-blue for primary content
1090
+ dataKeyColor: chalk4.rgb(130, 170, 255),
1091
+ // Matching debug blue for consistency
1092
+ selectorColor: chalk4.rgb(137, 221, 255)
1093
+ // Ice blue for selection indicators
1094
+ },
1095
+ detailedView: {
1096
+ colors: {
1097
+ trace: chalk4.rgb(114, 135, 153),
1098
+ debug: chalk4.rgb(130, 170, 255),
1099
+ info: chalk4.rgb(195, 232, 141),
1100
+ warn: chalk4.rgb(255, 203, 107),
1101
+ error: chalk4.rgb(247, 118, 142),
1102
+ fatal: chalk4.bgRgb(247, 118, 142).white
1103
+ },
1104
+ logIdColor: chalk4.rgb(84, 98, 117),
1105
+ dataValueColor: chalk4.rgb(209, 219, 231),
1106
+ dataKeyColor: chalk4.rgb(130, 170, 255),
1107
+ selectorColor: chalk4.rgb(137, 221, 255),
1108
+ // Ice blue for selection indicators
1109
+ headerColor: chalk4.rgb(137, 221, 255),
1110
+ labelColor: chalk4.rgb(137, 221, 255).bold,
1111
+ separatorColor: chalk4.rgb(84, 98, 117),
1112
+ jsonColors: {
1113
+ keysColor: chalk4.rgb(130, 170, 255),
1114
+ dashColor: chalk4.rgb(209, 219, 231),
1115
+ numberColor: chalk4.rgb(247, 140, 108),
1116
+ stringColor: chalk4.rgb(195, 232, 141),
1117
+ multilineStringColor: chalk4.rgb(195, 232, 141),
1118
+ positiveNumberColor: chalk4.rgb(195, 232, 141),
1119
+ negativeNumberColor: chalk4.rgb(247, 118, 142),
1120
+ booleanColor: chalk4.rgb(255, 203, 107),
1121
+ nullUndefinedColor: chalk4.rgb(114, 135, 153),
1122
+ dateColor: chalk4.rgb(199, 146, 234)
1123
+ }
1124
+ }
1125
+ };
1126
+ var sunlight = {
1127
+ simpleView: {
1128
+ colors: {
1129
+ trace: chalk4.rgb(110, 110, 110),
1130
+ // Dark grey for subtle information
1131
+ debug: chalk4.rgb(32, 96, 159),
1132
+ // Deep blue for strong contrast on white
1133
+ info: chalk4.rgb(35, 134, 54),
1134
+ // Forest green, easier on the eyes than bright green
1135
+ warn: chalk4.rgb(176, 95, 0),
1136
+ // Brown-orange for natural warning color
1137
+ error: chalk4.rgb(191, 0, 0),
1138
+ // Deep red for clear visibility on light backgrounds
1139
+ fatal: chalk4.bgRgb(191, 0, 0).white
1140
+ // White on deep red for critical issues
1141
+ },
1142
+ logIdColor: chalk4.rgb(110, 110, 110),
1143
+ dataValueColor: chalk4.rgb(0, 0, 0),
1144
+ dataKeyColor: chalk4.rgb(32, 96, 159),
1145
+ selectorColor: chalk4.rgb(0, 91, 129)
1146
+ // Deep blue for strong contrast
1147
+ },
1148
+ detailedView: {
1149
+ colors: {
1150
+ trace: chalk4.rgb(110, 110, 110),
1151
+ debug: chalk4.rgb(32, 96, 159),
1152
+ info: chalk4.rgb(35, 134, 54),
1153
+ warn: chalk4.rgb(176, 95, 0),
1154
+ error: chalk4.rgb(191, 0, 0),
1155
+ fatal: chalk4.bgRgb(191, 0, 0).white
1156
+ },
1157
+ logIdColor: chalk4.rgb(110, 110, 110),
1158
+ dataValueColor: chalk4.rgb(0, 0, 0),
1159
+ dataKeyColor: chalk4.rgb(32, 96, 159),
1160
+ selectorColor: chalk4.rgb(0, 91, 129),
1161
+ // Deep blue for strong contrast
1162
+ headerColor: chalk4.rgb(0, 91, 129),
1163
+ labelColor: chalk4.rgb(0, 91, 129).bold,
1164
+ separatorColor: chalk4.rgb(110, 110, 110),
1165
+ jsonColors: {
1166
+ keysColor: chalk4.rgb(32, 96, 159),
1167
+ dashColor: chalk4.rgb(0, 0, 0),
1168
+ numberColor: chalk4.rgb(170, 55, 49),
1169
+ stringColor: chalk4.rgb(35, 134, 54),
1170
+ multilineStringColor: chalk4.rgb(35, 134, 54),
1171
+ positiveNumberColor: chalk4.rgb(46, 125, 50),
1172
+ negativeNumberColor: chalk4.rgb(183, 28, 28),
1173
+ booleanColor: chalk4.rgb(176, 95, 0),
1174
+ nullUndefinedColor: chalk4.rgb(110, 110, 110),
1175
+ dateColor: chalk4.rgb(123, 31, 162)
1176
+ }
1177
+ }
1178
+ };
1179
+ var neon = {
1180
+ simpleView: {
1181
+ colors: {
1182
+ trace: chalk4.rgb(108, 108, 255),
1183
+ // Electric blue
1184
+ debug: chalk4.rgb(255, 82, 246),
1185
+ // Hot pink
1186
+ info: chalk4.rgb(0, 255, 163),
1187
+ // Cyber green
1188
+ warn: chalk4.rgb(255, 231, 46),
1189
+ // Electric yellow
1190
+ error: chalk4.rgb(255, 53, 91),
1191
+ // Neon red
1192
+ fatal: chalk4.bgRgb(255, 53, 91).rgb(0, 255, 163)
1193
+ // Neon red bg with cyber green text
1194
+ },
1195
+ logIdColor: chalk4.rgb(187, 134, 252),
1196
+ // Bright purple
1197
+ dataValueColor: chalk4.rgb(255, 255, 255),
1198
+ // Pure white
1199
+ dataKeyColor: chalk4.rgb(0, 255, 240),
1200
+ // Cyan
1201
+ selectorColor: chalk4.rgb(0, 255, 240)
1202
+ // Electric cyan for cyberpunk feel
1203
+ },
1204
+ detailedView: {
1205
+ colors: {
1206
+ trace: chalk4.rgb(108, 108, 255),
1207
+ debug: chalk4.rgb(255, 82, 246),
1208
+ info: chalk4.rgb(0, 255, 163),
1209
+ warn: chalk4.rgb(255, 231, 46),
1210
+ error: chalk4.rgb(255, 53, 91),
1211
+ fatal: chalk4.bgRgb(255, 53, 91).rgb(0, 255, 163)
1212
+ },
1213
+ logIdColor: chalk4.rgb(187, 134, 252),
1214
+ dataValueColor: chalk4.rgb(255, 255, 255),
1215
+ dataKeyColor: chalk4.rgb(0, 255, 240),
1216
+ selectorColor: chalk4.rgb(0, 255, 240),
1217
+ // Electric cyan for cyberpunk feel
1218
+ headerColor: chalk4.rgb(255, 82, 246),
1219
+ labelColor: chalk4.rgb(255, 82, 246).bold,
1220
+ separatorColor: chalk4.rgb(108, 108, 255),
1221
+ jsonColors: {
1222
+ keysColor: chalk4.rgb(0, 255, 240),
1223
+ dashColor: chalk4.rgb(255, 255, 255),
1224
+ numberColor: chalk4.rgb(255, 82, 246),
1225
+ stringColor: chalk4.rgb(0, 255, 163),
1226
+ multilineStringColor: chalk4.rgb(0, 255, 163),
1227
+ positiveNumberColor: chalk4.rgb(0, 255, 163),
1228
+ negativeNumberColor: chalk4.rgb(255, 53, 91),
1229
+ booleanColor: chalk4.rgb(255, 231, 46),
1230
+ nullUndefinedColor: chalk4.rgb(108, 108, 255),
1231
+ dateColor: chalk4.rgb(187, 134, 252)
1232
+ }
1233
+ }
1234
+ };
1235
+ var nature = {
1236
+ simpleView: {
1237
+ colors: {
1238
+ trace: chalk4.rgb(121, 85, 72),
1239
+ // Wooden brown
1240
+ debug: chalk4.rgb(46, 125, 50),
1241
+ // Forest green
1242
+ info: chalk4.rgb(0, 105, 92),
1243
+ // Deep teal
1244
+ warn: chalk4.rgb(175, 115, 0),
1245
+ // Golden amber
1246
+ error: chalk4.rgb(183, 28, 28),
1247
+ // Autumn red
1248
+ fatal: chalk4.bgRgb(183, 28, 28).rgb(255, 250, 240)
1249
+ // Red bg with soft white
1250
+ },
1251
+ logIdColor: chalk4.rgb(93, 64, 55),
1252
+ // Deep bark brown
1253
+ dataValueColor: chalk4.rgb(27, 27, 27),
1254
+ // Near black
1255
+ dataKeyColor: chalk4.rgb(56, 142, 60),
1256
+ // Leaf green
1257
+ selectorColor: chalk4.rgb(0, 105, 92)
1258
+ // Deep teal for natural feel
1259
+ },
1260
+ detailedView: {
1261
+ colors: {
1262
+ trace: chalk4.rgb(121, 85, 72),
1263
+ debug: chalk4.rgb(46, 125, 50),
1264
+ info: chalk4.rgb(0, 105, 92),
1265
+ warn: chalk4.rgb(175, 115, 0),
1266
+ error: chalk4.rgb(183, 28, 28),
1267
+ fatal: chalk4.bgRgb(183, 28, 28).rgb(255, 250, 240)
1268
+ },
1269
+ logIdColor: chalk4.rgb(93, 64, 55),
1270
+ dataValueColor: chalk4.rgb(27, 27, 27),
1271
+ dataKeyColor: chalk4.rgb(56, 142, 60),
1272
+ selectorColor: chalk4.rgb(0, 105, 92),
1273
+ // Deep teal for natural feel
1274
+ headerColor: chalk4.rgb(0, 77, 64),
1275
+ labelColor: chalk4.rgb(0, 77, 64).bold,
1276
+ separatorColor: chalk4.rgb(121, 85, 72),
1277
+ jsonColors: {
1278
+ keysColor: chalk4.rgb(56, 142, 60),
1279
+ dashColor: chalk4.rgb(27, 27, 27),
1280
+ numberColor: chalk4.rgb(230, 81, 0),
1281
+ stringColor: chalk4.rgb(0, 105, 92),
1282
+ multilineStringColor: chalk4.rgb(0, 105, 92),
1283
+ positiveNumberColor: chalk4.rgb(46, 125, 50),
1284
+ negativeNumberColor: chalk4.rgb(183, 28, 28),
1285
+ booleanColor: chalk4.rgb(175, 115, 0),
1286
+ nullUndefinedColor: chalk4.rgb(121, 85, 72),
1287
+ dateColor: chalk4.rgb(123, 31, 162)
1288
+ }
1289
+ }
1290
+ };
1291
+ var pastel = {
1292
+ simpleView: {
1293
+ colors: {
1294
+ trace: chalk4.rgb(179, 189, 203),
1295
+ // Soft slate blue
1296
+ debug: chalk4.rgb(189, 178, 255),
1297
+ // Gentle lavender
1298
+ info: chalk4.rgb(170, 236, 205),
1299
+ // Mint green
1300
+ warn: chalk4.rgb(255, 223, 186),
1301
+ // Peach
1302
+ error: chalk4.rgb(255, 188, 188),
1303
+ // Soft coral
1304
+ fatal: chalk4.bgRgb(255, 188, 188).rgb(76, 40, 40)
1305
+ // Coral bg with deep brown
1306
+ },
1307
+ logIdColor: chalk4.rgb(203, 195, 227),
1308
+ // Dusty lavender
1309
+ dataValueColor: chalk4.rgb(236, 236, 236),
1310
+ // Soft white
1311
+ dataKeyColor: chalk4.rgb(186, 207, 255),
1312
+ // Baby blue
1313
+ selectorColor: chalk4.rgb(189, 178, 255)
1314
+ // Soft lavender for gentle highlighting
1315
+ },
1316
+ detailedView: {
1317
+ colors: {
1318
+ trace: chalk4.rgb(179, 189, 203),
1319
+ debug: chalk4.rgb(189, 178, 255),
1320
+ info: chalk4.rgb(170, 236, 205),
1321
+ warn: chalk4.rgb(255, 223, 186),
1322
+ error: chalk4.rgb(255, 188, 188),
1323
+ fatal: chalk4.bgRgb(255, 188, 188).rgb(76, 40, 40)
1324
+ },
1325
+ logIdColor: chalk4.rgb(203, 195, 227),
1326
+ dataValueColor: chalk4.rgb(236, 236, 236),
1327
+ dataKeyColor: chalk4.rgb(186, 207, 255),
1328
+ selectorColor: chalk4.rgb(189, 178, 255),
1329
+ // Soft lavender for gentle highlighting
1330
+ headerColor: chalk4.rgb(255, 198, 255),
1331
+ // Cotton candy pink
1332
+ labelColor: chalk4.rgb(255, 198, 255).bold,
1333
+ separatorColor: chalk4.rgb(179, 189, 203),
1334
+ jsonColors: {
1335
+ keysColor: chalk4.rgb(186, 207, 255),
1336
+ // Baby blue
1337
+ dashColor: chalk4.rgb(236, 236, 236),
1338
+ // Soft white
1339
+ numberColor: chalk4.rgb(255, 198, 255),
1340
+ // Cotton candy pink
1341
+ stringColor: chalk4.rgb(170, 236, 205),
1342
+ // Mint green
1343
+ multilineStringColor: chalk4.rgb(170, 236, 205),
1344
+ positiveNumberColor: chalk4.rgb(170, 236, 205),
1345
+ negativeNumberColor: chalk4.rgb(255, 188, 188),
1346
+ booleanColor: chalk4.rgb(255, 223, 186),
1347
+ // Peach
1348
+ nullUndefinedColor: chalk4.rgb(179, 189, 203),
1349
+ dateColor: chalk4.rgb(189, 178, 255)
1350
+ // Gentle lavender
1351
+ }
1352
+ }
1353
+ };
1354
+
1355
+ // src/PrettyTerminalTransport.ts
1356
+ var PrettyTerminalTransport = class _PrettyTerminalTransport extends LoggerlessTransport {
1357
+ /** Singleton instance of the transport */
1358
+ static instance = null;
1359
+ /** Handles all rendering and formatting of logs */
1360
+ renderer;
1361
+ /** Manages log persistence in SQLite database */
1362
+ storage;
1363
+ /** Manages user interaction and view state */
1364
+ uiManager;
1365
+ /** Configuration options */
1366
+ config;
1367
+ /**
1368
+ * Creates a new PrettyTerminalTransport instance.
1369
+ * This is a singleton class - only one instance can exist at a time.
1370
+ * Use getInstance() instead of constructor directly.
1371
+ *
1372
+ * @param config - Configuration options for the transport
1373
+ * @throws Error if attempting to create multiple instances
1374
+ */
1375
+ constructor(config = {}) {
1376
+ if (_PrettyTerminalTransport.instance) {
1377
+ throw new Error("PrettyTerminalTransport is a singleton. Use getPrettyTerminal() instead.");
1378
+ }
1379
+ super(config);
1380
+ this.config = config;
1381
+ if (config.enabled === false) {
1382
+ return;
1383
+ }
1384
+ const maxInlineDepth = config.maxInlineDepth || 4;
1385
+ const maxInlineLength = config.maxInlineLength || 120;
1386
+ const theme = config.theme || moonlight;
1387
+ const logFile = config.logFile;
1388
+ const simpleViewConfig = {
1389
+ colors: {
1390
+ trace: chalk5.gray,
1391
+ // Lowest level, used for verbose output
1392
+ debug: chalk5.blue,
1393
+ // Debug information
1394
+ info: chalk5.green,
1395
+ // Normal operation
1396
+ warn: chalk5.yellow,
1397
+ // Warning conditions
1398
+ error: chalk5.red,
1399
+ // Error conditions
1400
+ fatal: chalk5.bgRed.white,
1401
+ // Critical errors
1402
+ ...theme.simpleView.colors
1403
+ },
1404
+ logIdColor: theme.simpleView.logIdColor || chalk5.dim,
1405
+ dataValueColor: theme.simpleView.dataValueColor || chalk5.white,
1406
+ dataKeyColor: theme.simpleView.dataKeyColor || chalk5.dim,
1407
+ selectorColor: theme.simpleView.selectorColor || chalk5.cyan
1408
+ };
1409
+ const detailedViewConfig = {
1410
+ colors: {
1411
+ trace: chalk5.gray,
1412
+ debug: chalk5.blue,
1413
+ info: chalk5.green,
1414
+ warn: chalk5.yellow,
1415
+ error: chalk5.red,
1416
+ fatal: chalk5.bgRed.white,
1417
+ ...theme.detailedView?.colors
1418
+ },
1419
+ logIdColor: theme.detailedView?.logIdColor || chalk5.dim,
1420
+ dataValueColor: theme.detailedView?.dataValueColor || chalk5.white,
1421
+ dataKeyColor: theme.detailedView?.dataKeyColor || chalk5.dim,
1422
+ selectorColor: theme.detailedView?.selectorColor || chalk5.cyan,
1423
+ headerColor: theme.detailedView?.headerColor || chalk5.cyan,
1424
+ labelColor: theme.detailedView?.labelColor || chalk5.cyan.bold,
1425
+ separatorColor: theme.detailedView?.separatorColor || chalk5.dim,
1426
+ // JSON formatting colors for detailed data view
1427
+ jsonColors: {
1428
+ keysColor: chalk5.yellow,
1429
+ // Object property names
1430
+ dashColor: chalk5.white,
1431
+ // Array bullets
1432
+ numberColor: chalk5.yellow,
1433
+ // Default number color
1434
+ stringColor: chalk5.white,
1435
+ // Single-line strings
1436
+ multilineStringColor: chalk5.white,
1437
+ // Multi-line strings
1438
+ positiveNumberColor: chalk5.green,
1439
+ // Numbers > 0
1440
+ negativeNumberColor: chalk5.red,
1441
+ // Numbers < 0
1442
+ booleanColor: chalk5.cyan,
1443
+ // true/false values
1444
+ nullUndefinedColor: chalk5.grey,
1445
+ // null/undefined
1446
+ dateColor: chalk5.magenta,
1447
+ // Date objects
1448
+ ...theme.detailedView?.jsonColors
1449
+ }
1450
+ };
1451
+ this.storage = new LogStorage(logFile);
1452
+ this.renderer = new LogRenderer(simpleViewConfig, detailedViewConfig, maxInlineDepth, maxInlineLength);
1453
+ this.uiManager = new UIManager(this.renderer, this.storage);
1454
+ _PrettyTerminalTransport.instance = this;
1455
+ }
1456
+ /**
1457
+ * Gets or creates the singleton instance of PrettyTerminalTransport.
1458
+ * This is the recommended way to obtain a transport instance.
1459
+ *
1460
+ * @param config - Configuration options for the transport
1461
+ * @returns The singleton instance of PrettyTerminalTransport
1462
+ */
1463
+ static getInstance(config = {}) {
1464
+ if (!_PrettyTerminalTransport.instance) {
1465
+ _PrettyTerminalTransport.instance = new _PrettyTerminalTransport(config);
1466
+ }
1467
+ return _PrettyTerminalTransport.instance;
1468
+ }
1469
+ /**
1470
+ * Generates a random ID for each log entry.
1471
+ * Uses base36 encoding for compact, readable IDs.
1472
+ *
1473
+ * @returns A 6-character string ID
1474
+ * @example
1475
+ * "a1b2c3" // Example generated ID
1476
+ */
1477
+ generateId() {
1478
+ return Math.random().toString(36).substring(2, 8);
1479
+ }
1480
+ /**
1481
+ * Main transport method that receives logs from LogLayer.
1482
+ * This method is called for each log event and handles:
1483
+ * - Generating a unique ID for the log
1484
+ * - Converting the log data to a storable format
1485
+ * - Storing the log in the database
1486
+ * - Rendering the log if not in selection mode
1487
+ *
1488
+ * @param logLevel - The severity level of the log
1489
+ * @param messages - Array of message strings to be joined
1490
+ * @param data - Additional structured data to be logged
1491
+ * @param hasData - Whether the log includes additional data
1492
+ * @returns The original messages array
1493
+ */
1494
+ shipToLogger({ logLevel, messages, data, hasData }) {
1495
+ if (this.config.enabled === false) {
1496
+ return messages;
1497
+ }
1498
+ const entry = {
1499
+ id: this.generateId(),
1500
+ timestamp: Date.now(),
1501
+ level: logLevel,
1502
+ message: messages.join(" "),
1503
+ data: hasData ? JSON.stringify(data) : null
1504
+ };
1505
+ this.uiManager.handleNewLog(entry);
1506
+ return messages;
1507
+ }
1508
+ };
1509
+
1510
+ // src/index.ts
1511
+ import * as chalk6 from "chalk";
1512
+ function getPrettyTerminal(config = {}) {
1513
+ return PrettyTerminalTransport.getInstance(config);
1514
+ }
1515
+ export {
1516
+ PrettyTerminalTransport,
1517
+ chalk6 as chalk,
1518
+ getPrettyTerminal,
1519
+ moonlight,
1520
+ nature,
1521
+ neon,
1522
+ pastel,
1523
+ sunlight
1524
+ };
1525
+ //# sourceMappingURL=index.js.map