logixia 1.12.2 → 1.12.4

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.
@@ -0,0 +1,1341 @@
1
+ #!/usr/bin/env node
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ let node_fs = require("node:fs");
26
+ node_fs = __toESM(node_fs);
27
+ let node_path = require("node:path");
28
+ node_path = __toESM(node_path);
29
+ let commander = require("commander");
30
+ commander = __toESM(commander);
31
+ let picocolors = require("picocolors");
32
+ picocolors = __toESM(picocolors);
33
+
34
+ //#region src/cli/utils.ts
35
+ /**
36
+ * Render a single cell value. Uses a null/undefined check rather than `||` so
37
+ * falsy-but-real values (0, false, '') still render — a `count=0` or
38
+ * `statusCode=0` field must not silently display as blank.
39
+ */
40
+ function cellText(value) {
41
+ return value === null || value === void 0 ? "" : String(value);
42
+ }
43
+ function formatAsTable(rows, columns) {
44
+ const colWidths = columns.map((c) => Math.max(c.length, ...rows.map((r) => cellText(r[c]).length)));
45
+ return [
46
+ columns.map((c, i) => c.padEnd(colWidths[i] ?? 0)).join(" | "),
47
+ colWidths.map((w) => "-".repeat(w)).join("-|-"),
48
+ rows.map((r) => columns.map((c, i) => cellText(r[c]).padEnd(colWidths[i] ?? 0)).join(" | ")).join("\n")
49
+ ].join("\n");
50
+ }
51
+ function safeParseLogs(raw) {
52
+ const lines = raw.split(/\r?\n/).filter(Boolean);
53
+ const parsed = [];
54
+ for (const l of lines) {
55
+ try {
56
+ const j = JSON.parse(l);
57
+ parsed.push(j);
58
+ continue;
59
+ } catch {}
60
+ parsed.push({ message: l });
61
+ }
62
+ return parsed;
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/cli/commands/analyze.ts
67
+ function parseDurationToMs(input) {
68
+ if (!input) return;
69
+ const m = input.match(/^(\d+)([smhd])$/i);
70
+ if (!m) return;
71
+ const n = Number.parseInt(m[1], 10);
72
+ switch (m[2].toLowerCase()) {
73
+ case "s": return n * 1e3;
74
+ case "m": return n * 60 * 1e3;
75
+ case "h": return n * 60 * 60 * 1e3;
76
+ case "d": return n * 24 * 60 * 60 * 1e3;
77
+ default: return;
78
+ }
79
+ }
80
+ async function analyzeFileContents(raw, opts = {}) {
81
+ let filtered = safeParseLogs(raw);
82
+ const durMs = parseDurationToMs(opts.last);
83
+ if (durMs) {
84
+ const cutoff = Date.now() - durMs;
85
+ filtered = filtered.filter((e) => {
86
+ const row = e;
87
+ const ts = row["timestamp"] ? new Date(row["timestamp"]).getTime() : void 0;
88
+ if (!ts) return false;
89
+ return ts >= cutoff;
90
+ });
91
+ }
92
+ if (opts.level) filtered = filtered.filter((e) => (e["level"] ?? "").toLowerCase() === opts.level.toLowerCase());
93
+ const byLevel = {};
94
+ for (const e of filtered) {
95
+ const l = (e.level || "info").toUpperCase();
96
+ byLevel[l] = (byLevel[l] || 0) + 1;
97
+ }
98
+ if (opts.format === "json") return {
99
+ total: filtered.length,
100
+ byLevel
101
+ };
102
+ return formatAsTable(Object.keys(byLevel).map((k) => ({
103
+ level: k,
104
+ count: byLevel[k]
105
+ })), ["level", "count"]);
106
+ }
107
+ const analyzeCommand = new commander.Command("analyze").description("Analyze log files for patterns and insights").argument("<file>", "Path to log file").option("--level <level>", "Filter by log level").option("--last <range>", "Time range (e.g., 24h, 7d)").option("--format <fmt>", "Output format (table,json,csv)", "table").action(async (file, opts) => {
108
+ const full = node_path.default.resolve(process.cwd(), file);
109
+ if (!node_fs.default.existsSync(full)) {
110
+ console.error(`File not found: ${full}`);
111
+ process.exit(2);
112
+ }
113
+ const result = await analyzeFileContents(node_fs.default.readFileSync(full, "utf8"), opts);
114
+ if (opts.format === "json") {
115
+ console.log(JSON.stringify(result, null, 2));
116
+ return;
117
+ }
118
+ console.log(result);
119
+ });
120
+
121
+ //#endregion
122
+ //#region src/cli/commands/explore.ts
123
+ const CSI = "\x1B[";
124
+ const clearScreen = CSI + "2J";
125
+ const clearLine = CSI + "K";
126
+ const hideCursor = CSI + "?25l";
127
+ const showCursor = CSI + "?25h";
128
+ const altScreen = CSI + "?1049h";
129
+ const mainScreen = CSI + "?1049l";
130
+ function moveTo(row, col) {
131
+ return `${CSI}${row};${col}H`;
132
+ }
133
+ function write(s) {
134
+ process.stdout.write(s);
135
+ }
136
+ const ANSI_STRIP_RE = /\x1b\[[0-9;]*m/g;
137
+ function stripAnsi(s) {
138
+ return s.replace(ANSI_STRIP_RE, "");
139
+ }
140
+ function compose(...fns) {
141
+ return (s) => fns.reduceRight((acc, fn) => fn(acc), s);
142
+ }
143
+ const bgGray = picocolors.default.bgBlackBright;
144
+ function normalizeLevel(level) {
145
+ if (typeof level === "number") {
146
+ if (level >= 50) return "error";
147
+ if (level >= 40) return "warn";
148
+ if (level >= 30) return "info";
149
+ if (level >= 20) return "debug";
150
+ return "trace";
151
+ }
152
+ return String(level ?? "info").toLowerCase();
153
+ }
154
+ function levelColor(level) {
155
+ switch (level) {
156
+ case "error": return compose(picocolors.default.bgRed, picocolors.default.white);
157
+ case "warn": return compose(picocolors.default.bgYellow, picocolors.default.black);
158
+ case "info": return compose(picocolors.default.bgBlue, picocolors.default.white);
159
+ case "debug": return compose(bgGray, picocolors.default.white);
160
+ case "trace": return compose(picocolors.default.bgMagenta, picocolors.default.white);
161
+ case "verbose": return compose(picocolors.default.bgCyan, picocolors.default.black);
162
+ default: return compose(bgGray, picocolors.default.white);
163
+ }
164
+ }
165
+ const LEVEL_SHORT = {
166
+ error: "ERR",
167
+ warn: "WRN",
168
+ info: "INF",
169
+ debug: "DBG",
170
+ trace: "TRC",
171
+ verbose: "VRB"
172
+ };
173
+ function levelBadge(level) {
174
+ const label = ` ${(LEVEL_SHORT[level] ?? level.slice(0, 3).toUpperCase()).padEnd(3)} `;
175
+ return levelColor(level)(label);
176
+ }
177
+ function formatTime(ts) {
178
+ if (!ts) return " ";
179
+ try {
180
+ const d = new Date(String(ts));
181
+ if (Number.isNaN(d.getTime())) return String(ts).slice(0, 12).padEnd(12);
182
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
183
+ } catch {
184
+ return String(ts).slice(0, 12).padEnd(12);
185
+ }
186
+ }
187
+ function syntaxColorJson(line) {
188
+ return line.replace(/"([^"]+)":/g, (_, k) => picocolors.default.cyan(`"${k}"`) + ":").replace(/: "([^"]*)"/g, (_, v) => ": " + picocolors.default.green(`"${v}"`)).replace(/: (-?\d+(?:\.\d+)?)/g, (_, v) => ": " + picocolors.default.yellow(v)).replace(/: (true|false)/g, (_, v) => ": " + picocolors.default.magenta(v)).replace(/: null/g, ": " + picocolors.default.dim("null"));
189
+ }
190
+ function coloriseStackFrame(frame) {
191
+ const trimmed = frame.trim();
192
+ if (!trimmed.startsWith("at ")) return picocolors.default.dim(" " + trimmed);
193
+ const withoutAt = trimmed.slice(3);
194
+ const parenOpen = withoutAt.lastIndexOf("(");
195
+ const parenClose = withoutAt.lastIndexOf(")");
196
+ if (parenOpen !== -1 && parenClose > parenOpen) {
197
+ const name = withoutAt.slice(0, parenOpen).trimEnd();
198
+ const loc = withoutAt.slice(parenOpen + 1, parenClose);
199
+ return " " + picocolors.default.dim("at ") + picocolors.default.cyan(name) + " " + picocolors.default.dim("(" + loc + ")");
200
+ }
201
+ return " " + picocolors.default.dim("at ") + picocolors.default.cyan(withoutAt);
202
+ }
203
+ function buildDetailLines(entry) {
204
+ if (!entry) return [picocolors.default.dim(" No entry selected")];
205
+ const lines = [];
206
+ const { stack,...rest } = entry;
207
+ const jsonText = JSON.stringify(rest, null, 2);
208
+ for (const l of jsonText.split("\n")) lines.push(" " + syntaxColorJson(l));
209
+ if (stack) {
210
+ lines.push("");
211
+ lines.push(picocolors.default.bold(picocolors.default.red(" ▼ STACK TRACE")));
212
+ for (const sl of String(stack).split("\n")) {
213
+ if (!sl.trim()) continue;
214
+ if (sl.trim().startsWith("at ")) lines.push(coloriseStackFrame(sl));
215
+ else lines.push(" " + picocolors.default.red(sl.trim()));
216
+ }
217
+ }
218
+ return lines;
219
+ }
220
+ var TUIExplorer = class {
221
+ get LIST_ROWS() {
222
+ return Math.max(4, this.height - this.HEADER_ROWS - this.STATUS_ROWS - this.DETAIL_ROWS);
223
+ }
224
+ constructor(filePath, opts) {
225
+ this.opts = opts;
226
+ this.allEntries = [];
227
+ this.filteredEntries = [];
228
+ this.selectedIndex = 0;
229
+ this.listScrollOffset = 0;
230
+ this.detailScrollOffset = 0;
231
+ this.searchQuery = "";
232
+ this.searchMode = false;
233
+ this.searchBuffer = "";
234
+ this.exportMode = false;
235
+ this.exportBuffer = "";
236
+ this.filePos = 0;
237
+ this.width = 80;
238
+ this.height = 24;
239
+ this.HEADER_ROWS = 3;
240
+ this.STATUS_ROWS = 1;
241
+ this.DETAIL_ROWS = 9;
242
+ this.filePath = node_path.default.resolve(process.cwd(), filePath);
243
+ this.levelFilters = new Set([
244
+ "error",
245
+ "warn",
246
+ "info",
247
+ "debug",
248
+ "trace",
249
+ "verbose"
250
+ ]);
251
+ if (opts.levels) this.levelFilters = new Set(opts.levels.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean));
252
+ if (opts.search) this.searchQuery = opts.search;
253
+ this.followMode = opts.follow;
254
+ }
255
+ run() {
256
+ if (!node_fs.default.existsSync(this.filePath)) {
257
+ console.error(picocolors.default.red(`File not found: ${this.filePath}`));
258
+ process.exit(2);
259
+ }
260
+ const raw = node_fs.default.readFileSync(this.filePath, "utf8");
261
+ this.filePos = node_fs.default.statSync(this.filePath).size;
262
+ this.allEntries = safeParseLogs(raw);
263
+ this.applyFilters();
264
+ write(altScreen + hideCursor);
265
+ this.getTerminalSize();
266
+ process.stdout.on("resize", () => {
267
+ this.getTerminalSize();
268
+ this.redraw();
269
+ });
270
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
271
+ process.stdin.resume();
272
+ process.stdin.on("data", (buf) => {
273
+ this.handleKey(buf);
274
+ });
275
+ if (this.followMode) this.startFollow();
276
+ this.redraw();
277
+ }
278
+ getTerminalSize() {
279
+ this.width = process.stdout.columns || 80;
280
+ this.height = process.stdout.rows || 24;
281
+ }
282
+ applyFilters() {
283
+ this.filteredEntries = this.allEntries.filter((entry) => {
284
+ const lvl = normalizeLevel(entry.level);
285
+ if (!this.levelFilters.has(lvl)) return false;
286
+ if (this.searchQuery) {
287
+ if (!JSON.stringify(entry).toLowerCase().includes(this.searchQuery.toLowerCase())) return false;
288
+ }
289
+ return true;
290
+ });
291
+ if (this.selectedIndex >= this.filteredEntries.length) this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
292
+ this.clampScroll();
293
+ }
294
+ clampScroll() {
295
+ const lr = this.LIST_ROWS;
296
+ if (this.selectedIndex < this.listScrollOffset) this.listScrollOffset = this.selectedIndex;
297
+ if (this.selectedIndex >= this.listScrollOffset + lr) this.listScrollOffset = this.selectedIndex - lr + 1;
298
+ this.listScrollOffset = Math.max(0, this.listScrollOffset);
299
+ }
300
+ redraw() {
301
+ write(clearScreen + moveTo(1, 1));
302
+ this.drawHeader();
303
+ this.drawFilterBar();
304
+ this.drawColHeader();
305
+ this.drawList();
306
+ this.drawDetailPanel();
307
+ this.drawStatusBar();
308
+ }
309
+ pad(s, len) {
310
+ if (s.length >= len) return s.slice(0, len);
311
+ return s + " ".repeat(len - s.length);
312
+ }
313
+ truncate(s, len) {
314
+ if (len <= 0) return "";
315
+ if (s.length <= len) return s;
316
+ return s.slice(0, len - 1) + "…";
317
+ }
318
+ drawHeader() {
319
+ const w = this.width;
320
+ const title = compose(picocolors.default.bgWhite, picocolors.default.black, picocolors.default.bold)(" LOGIXIA EXPLORE ");
321
+ const fname = picocolors.default.cyan(` ${node_path.default.basename(this.filePath)} `);
322
+ const count = picocolors.default.dim(`[${this.filteredEntries.length}/${this.allEntries.length}]`);
323
+ const follow = this.followMode ? picocolors.default.green(" ⟳ FOLLOW") : "";
324
+ const search = this.searchQuery ? picocolors.default.yellow(` /${this.searchQuery}`) : "";
325
+ const left = ` ${title}${fname}`;
326
+ const right = count + follow + search + " ";
327
+ const leftLen = stripAnsi(left).length;
328
+ const rightLen = stripAnsi(right).length;
329
+ const gap = Math.max(1, w - leftLen - rightLen);
330
+ write(moveTo(1, 1) + picocolors.default.bgBlack(left + " ".repeat(gap) + right) + clearLine + "\n");
331
+ }
332
+ drawFilterBar() {
333
+ const badges = [
334
+ ["error", "E"],
335
+ ["warn", "W"],
336
+ ["info", "I"],
337
+ ["debug", "D"],
338
+ ["trace", "T"],
339
+ ["verbose", "V"]
340
+ ].map(([lvl, key]) => {
341
+ return this.levelFilters.has(lvl) ? levelColor(lvl)(` ${key} `) : compose(picocolors.default.dim, picocolors.default.strikethrough)(` ${key} `);
342
+ }).join(" ");
343
+ const searchPart = this.searchMode ? compose(picocolors.default.bgYellow, picocolors.default.black)(` /${this.searchBuffer}█ `) : picocolors.default.dim(" /: search ");
344
+ const w = this.width;
345
+ const left = " " + badges;
346
+ const right = searchPart + " ";
347
+ const gap = Math.max(1, w - stripAnsi(left).length - stripAnsi(right).length);
348
+ write(moveTo(2, 1) + picocolors.default.bgBlack(left + " ".repeat(gap) + right) + clearLine + "\n");
349
+ }
350
+ drawColHeader() {
351
+ const w = this.width;
352
+ const timeCol = " TIME ";
353
+ const levelCol = " LVL ";
354
+ const msgWidth = Math.max(20, w - 14 - 5 - 2);
355
+ write(moveTo(3, 1) + compose(picocolors.default.bold, picocolors.default.bgBlack, picocolors.default.dim)(timeCol + levelCol + " " + this.pad("MESSAGE", msgWidth)) + clearLine + "\n");
356
+ }
357
+ drawList() {
358
+ const lr = this.LIST_ROWS;
359
+ const w = this.width;
360
+ const msgWidth = Math.max(10, w - 13 - 5 - 3);
361
+ for (let row = 0; row < lr; row++) {
362
+ const entryIdx = this.listScrollOffset + row;
363
+ write(moveTo(this.HEADER_ROWS + row + 1, 1));
364
+ if (entryIdx >= this.filteredEntries.length) {
365
+ write(picocolors.default.bgBlack(" ".repeat(w)) + clearLine);
366
+ continue;
367
+ }
368
+ const entry = this.filteredEntries[entryIdx];
369
+ const isSelected = entryIdx === this.selectedIndex;
370
+ const level = normalizeLevel(entry.level);
371
+ const time = formatTime(entry.timestamp);
372
+ const msg = String(entry.message ?? "");
373
+ const extraFields = Object.entries(entry).filter(([k]) => ![
374
+ "timestamp",
375
+ "level",
376
+ "message",
377
+ "stack"
378
+ ].includes(k)).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join(" ");
379
+ const msgSlot = Math.floor(msgWidth * .65);
380
+ const msgPart = this.truncate(msg, msgSlot);
381
+ const extraPart = this.truncate(extraFields, msgWidth - msgPart.length - 2);
382
+ const msgDisplay = msgPart + (extraPart ? picocolors.default.dim(" " + extraPart) : "");
383
+ const line = ` ${time} ` + levelBadge(level) + " " + this.pad(msgDisplay, msgWidth);
384
+ if (isSelected) {
385
+ const plain = this.pad(stripAnsi(line), w);
386
+ write(compose(picocolors.default.bgWhite, picocolors.default.black)(plain));
387
+ } else if (this.searchQuery) write(this.highlightSearch(this.truncate(line, w)));
388
+ else write(this.truncate(line, w));
389
+ write(clearLine);
390
+ }
391
+ }
392
+ drawDetailPanel() {
393
+ const w = this.width;
394
+ const sepRow = this.HEADER_ROWS + this.LIST_ROWS + 1;
395
+ const selected = this.filteredEntries[this.selectedIndex];
396
+ const label = selected ? ` ▼ DETAIL (${this.selectedIndex + 1}/${this.filteredEntries.length}) ` : " ▼ DETAIL ";
397
+ const labelLen = stripAnsi(label).length;
398
+ const sepLine = compose(picocolors.default.bgBlack, picocolors.default.cyan, picocolors.default.bold)(label) + picocolors.default.dim("─".repeat(Math.max(0, w - labelLen)));
399
+ write(moveTo(sepRow, 1) + sepLine + clearLine + "\n");
400
+ const lines = buildDetailLines(selected);
401
+ for (let row = 0; row < 8; row++) {
402
+ write(moveTo(sepRow + 1 + row, 1));
403
+ const lineIdx = this.detailScrollOffset + row;
404
+ if (lineIdx < lines.length) write(this.truncate(lines[lineIdx], w) + clearLine);
405
+ else write(picocolors.default.bgBlack(" ".repeat(w)) + clearLine);
406
+ }
407
+ }
408
+ drawStatusBar() {
409
+ const statusRow = this.height;
410
+ const bar = " " + [
411
+ picocolors.default.bold("j/k") + " move",
412
+ picocolors.default.bold("/") + " search",
413
+ picocolors.default.bold("x") + " export",
414
+ picocolors.default.bold("E/W/I/D/T/V") + " filter",
415
+ picocolors.default.bold("J/K") + " detail↕",
416
+ picocolors.default.bold("f") + " follow",
417
+ picocolors.default.bold("g/G") + " top/bot",
418
+ picocolors.default.bold("q") + " quit"
419
+ ].join(picocolors.default.dim(" │ "));
420
+ write(moveTo(statusRow, 1) + compose(picocolors.default.bgBlack, picocolors.default.dim)(this.truncate(bar, this.width)) + clearLine);
421
+ if (this.exportMode) {
422
+ const promptRow = this.height - 1;
423
+ const prompt = compose(picocolors.default.bgYellow, picocolors.default.black)(` Export path (.json/.csv/.ndjson, blank=cancel): ${this.exportBuffer}█ `);
424
+ write(moveTo(promptRow, 1) + prompt + clearLine);
425
+ }
426
+ }
427
+ handleKey(buf) {
428
+ const key = buf.toString("utf8");
429
+ if (key === "") {
430
+ this.cleanup();
431
+ return;
432
+ }
433
+ if (this.exportMode) {
434
+ this.handleExportKey(key);
435
+ return;
436
+ }
437
+ if (this.searchMode) {
438
+ this.handleSearchKey(key);
439
+ return;
440
+ }
441
+ switch (key) {
442
+ case "j":
443
+ case "\x1B[B":
444
+ this.moveSelection(1);
445
+ break;
446
+ case "k":
447
+ case "\x1B[A":
448
+ this.moveSelection(-1);
449
+ break;
450
+ case "g":
451
+ case "\x1B[1~":
452
+ this.selectedIndex = 0;
453
+ this.listScrollOffset = 0;
454
+ this.detailScrollOffset = 0;
455
+ break;
456
+ case "G":
457
+ case "\x1B[4~":
458
+ this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
459
+ this.clampScroll();
460
+ this.detailScrollOffset = 0;
461
+ break;
462
+ case "\x1B[5~":
463
+ this.moveSelection(-this.LIST_ROWS);
464
+ break;
465
+ case "\x1B[6~":
466
+ this.moveSelection(this.LIST_ROWS);
467
+ break;
468
+ case "J":
469
+ this.detailScrollOffset++;
470
+ break;
471
+ case "K":
472
+ this.detailScrollOffset = Math.max(0, this.detailScrollOffset - 1);
473
+ break;
474
+ case "/":
475
+ this.searchMode = true;
476
+ this.searchBuffer = this.searchQuery;
477
+ break;
478
+ case "E":
479
+ this.toggleLevel("error");
480
+ break;
481
+ case "W":
482
+ this.toggleLevel("warn");
483
+ break;
484
+ case "I":
485
+ this.toggleLevel("info");
486
+ break;
487
+ case "D":
488
+ this.toggleLevel("debug");
489
+ break;
490
+ case "T":
491
+ this.toggleLevel("trace");
492
+ break;
493
+ case "V":
494
+ this.toggleLevel("verbose");
495
+ break;
496
+ case "f":
497
+ this.followMode = !this.followMode;
498
+ if (this.followMode) this.startFollow();
499
+ else {
500
+ node_fs.default.unwatchFile(this.filePath);
501
+ this.fileWatcher = void 0;
502
+ }
503
+ break;
504
+ case "x":
505
+ this.exportMode = true;
506
+ this.exportBuffer = "";
507
+ break;
508
+ case "q":
509
+ this.cleanup();
510
+ return;
511
+ default: break;
512
+ }
513
+ this.redraw();
514
+ }
515
+ handleSearchKey(key) {
516
+ if (key === "\r" || key === "\n") {
517
+ this.searchQuery = this.searchBuffer;
518
+ this.searchMode = false;
519
+ this.applyFilters();
520
+ } else if (key === "\x1B" || key === "\x1B\x1B") {
521
+ this.searchMode = false;
522
+ this.searchBuffer = "";
523
+ this.searchQuery = "";
524
+ this.applyFilters();
525
+ } else if (key === "" || key === "\b") this.searchBuffer = this.searchBuffer.slice(0, -1);
526
+ else if (key.length === 1 && key >= " ") this.searchBuffer += key;
527
+ this.redraw();
528
+ }
529
+ handleExportKey(key) {
530
+ if (key === "\r" || key === "\n") {
531
+ this.exportMode = false;
532
+ if (this.exportBuffer.trim()) this.doExport(this.exportBuffer.trim());
533
+ this.redraw();
534
+ } else if (key === "\x1B") {
535
+ this.exportMode = false;
536
+ this.exportBuffer = "";
537
+ this.redraw();
538
+ } else if (key === "" || key === "\b") {
539
+ this.exportBuffer = this.exportBuffer.slice(0, -1);
540
+ this.redraw();
541
+ } else if (key.length === 1 && key >= " ") {
542
+ this.exportBuffer += key;
543
+ this.redraw();
544
+ }
545
+ }
546
+ doExport(outPath) {
547
+ const resolved = node_path.default.resolve(process.cwd(), outPath);
548
+ const ext = node_path.default.extname(outPath).toLowerCase();
549
+ try {
550
+ if (ext === ".csv") {
551
+ const entries = this.filteredEntries;
552
+ const allKeys = [...new Set(entries.flatMap((e) => Object.keys(e)))];
553
+ const header = allKeys.join(",");
554
+ const rows = entries.map((e) => allKeys.map((k) => {
555
+ const v = e[k];
556
+ return `"${(typeof v === "string" ? v : JSON.stringify(v ?? "")).replace(/"/g, "\"\"")}"`;
557
+ }).join(","));
558
+ node_fs.default.writeFileSync(resolved, [header, ...rows].join("\n"), "utf8");
559
+ } else if (ext === ".ndjson" || ext === ".jsonl") node_fs.default.writeFileSync(resolved, this.filteredEntries.map((e) => JSON.stringify(e)).join("\n"), "utf8");
560
+ else node_fs.default.writeFileSync(resolved, JSON.stringify(this.filteredEntries, null, 2), "utf8");
561
+ write(moveTo(this.height - 1, 1) + compose(picocolors.default.bgGreen, picocolors.default.black)(` ✓ Exported ${this.filteredEntries.length} entries → ${resolved} `) + clearLine);
562
+ setTimeout(() => {
563
+ this.redraw();
564
+ }, 2e3);
565
+ } catch (err) {
566
+ write(moveTo(this.height - 1, 1) + compose(picocolors.default.bgRed, picocolors.default.white)(` ✗ Export failed: ${String(err.message)} `) + clearLine);
567
+ setTimeout(() => {
568
+ this.redraw();
569
+ }, 2e3);
570
+ }
571
+ }
572
+ moveSelection(delta) {
573
+ const max = Math.max(0, this.filteredEntries.length - 1);
574
+ this.selectedIndex = Math.max(0, Math.min(max, this.selectedIndex + delta));
575
+ this.detailScrollOffset = 0;
576
+ this.clampScroll();
577
+ }
578
+ toggleLevel(level) {
579
+ if (this.levelFilters.has(level)) {
580
+ if (this.levelFilters.size > 1) this.levelFilters.delete(level);
581
+ } else this.levelFilters.add(level);
582
+ this.applyFilters();
583
+ }
584
+ startFollow() {
585
+ this.fileWatcher = node_fs.default.watchFile(this.filePath, { interval: 500 }, (curr) => {
586
+ if (curr.size <= this.filePos) return;
587
+ try {
588
+ const rs = node_fs.default.createReadStream(this.filePath, {
589
+ start: this.filePos,
590
+ end: curr.size,
591
+ encoding: "utf8"
592
+ });
593
+ let buf = "";
594
+ rs.on("data", (chunk) => {
595
+ buf += String(chunk);
596
+ });
597
+ rs.on("end", () => {
598
+ this.filePos = curr.size;
599
+ const newEntries = safeParseLogs(buf);
600
+ this.allEntries.push(...newEntries);
601
+ this.applyFilters();
602
+ this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
603
+ this.clampScroll();
604
+ this.redraw();
605
+ });
606
+ } catch {}
607
+ });
608
+ }
609
+ highlightSearch(line) {
610
+ if (!this.searchQuery) return line;
611
+ const plain = stripAnsi(line);
612
+ const lower = plain.toLowerCase();
613
+ const needle = this.searchQuery.toLowerCase();
614
+ let result = "";
615
+ let i = 0;
616
+ while (i < plain.length) {
617
+ const idx = lower.indexOf(needle, i);
618
+ if (idx === -1) {
619
+ result += plain.slice(i);
620
+ break;
621
+ }
622
+ result += plain.slice(i, idx) + compose(picocolors.default.bgYellow, picocolors.default.black)(plain.slice(idx, idx + needle.length));
623
+ i = idx + needle.length;
624
+ }
625
+ return result;
626
+ }
627
+ cleanup() {
628
+ if (this.fileWatcher) node_fs.default.unwatchFile(this.filePath);
629
+ write(showCursor + mainScreen);
630
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
631
+ process.stdin.pause();
632
+ process.exit(0);
633
+ }
634
+ };
635
+ const exploreCommand = new commander.Command("explore").description("Interactive TUI log explorer with real-time search, level filters, and export").argument("<file>", "Path to log file (NDJSON / JSON-lines)").option("--follow", "Follow file changes in real-time", false).option("--levels <levels>", "Comma-separated levels to show (e.g. error,warn,info)", "").option("--search <query>", "Pre-populate the search field", "").action((file, opts) => {
636
+ new TUIExplorer(file, {
637
+ follow: Boolean(opts.follow),
638
+ levels: String(opts.levels ?? ""),
639
+ search: String(opts.search ?? "")
640
+ }).run();
641
+ });
642
+
643
+ //#endregion
644
+ //#region src/cli/commands/export.ts
645
+ async function exportLogs(raw, opts = {}) {
646
+ const entries = safeParseLogs(raw);
647
+ const fields = opts.fields ? opts.fields.split(",").map((f) => f.trim()) : null;
648
+ if (opts.format === "csv") return exportAsCSV(entries, fields);
649
+ else if (opts.format === "json") return exportAsJSON(entries, fields);
650
+ return JSON.stringify(entries, null, 2);
651
+ }
652
+ function exportAsCSV(entries, fields) {
653
+ if (entries.length === 0) return "";
654
+ const columns = fields || Object.keys(entries[0] || {});
655
+ const lines = [];
656
+ lines.push(columns.join(","));
657
+ for (const entry of entries) {
658
+ const row = columns.map((col) => {
659
+ const value = entry[col];
660
+ if (value === void 0 || value === null) return "";
661
+ const str = String(value);
662
+ if (str.includes(",") || str.includes("\"") || str.includes("\n")) return `"${str.replace(/"/g, "\"\"")}"`;
663
+ return str;
664
+ });
665
+ lines.push(row.join(","));
666
+ }
667
+ return lines.join("\n");
668
+ }
669
+ function exportAsJSON(entries, fields) {
670
+ if (!fields) return JSON.stringify(entries, null, 2);
671
+ const filtered = entries.map((entry) => {
672
+ const obj = {};
673
+ for (const field of fields) if (entry[field] !== void 0) obj[field] = entry[field];
674
+ return obj;
675
+ });
676
+ return JSON.stringify(filtered, null, 2);
677
+ }
678
+ const exportCommand = new commander.Command("export").description("Export log files to different formats").argument("<file>", "Path to log file").option("--format <fmt>", "Output format (csv, json)", "json").option("--fields <fields>", "Comma-separated list of fields to export (e.g., timestamp,level,message)").option("--output <file>", "Output file path (default: stdout)").action(async (file, opts) => {
679
+ const full = node_path.default.resolve(process.cwd(), file);
680
+ if (!node_fs.default.existsSync(full)) {
681
+ console.error(`File not found: ${full}`);
682
+ process.exit(2);
683
+ }
684
+ const output = await exportLogs(node_fs.default.readFileSync(full, "utf8"), opts);
685
+ if (opts.output) {
686
+ const outPath = node_path.default.resolve(process.cwd(), opts.output);
687
+ node_fs.default.writeFileSync(outPath, output, "utf8");
688
+ console.log(`Exported to: ${outPath}`);
689
+ } else console.log(output);
690
+ });
691
+
692
+ //#endregion
693
+ //#region src/cli/commands/query.ts
694
+ /**
695
+ * Parse a human-friendly time range string into a `Date`.
696
+ * Returns `undefined` when the string is not recognised.
697
+ *
698
+ * Supported formats:
699
+ * - "last N minutes" / "last N hours" / "last N days"
700
+ * - "today" → start of today (00:00:00)
701
+ * - "yesterday" → start of yesterday
702
+ * - ISO 8601 or any string parseable by `new Date()`
703
+ */
704
+ function parseTimeRange(value) {
705
+ const norm = value.trim().toLowerCase();
706
+ if (norm === "today") {
707
+ const d = /* @__PURE__ */ new Date();
708
+ d.setHours(0, 0, 0, 0);
709
+ return d;
710
+ }
711
+ if (norm === "yesterday") {
712
+ const d = /* @__PURE__ */ new Date();
713
+ d.setDate(d.getDate() - 1);
714
+ d.setHours(0, 0, 0, 0);
715
+ return d;
716
+ }
717
+ const relMatch = norm.match(/^last\s+(\d+)\s+(minute|minutes|min|hour|hours|day|days|week|weeks)$/);
718
+ if (relMatch) {
719
+ const n = Number(relMatch[1]);
720
+ const unit = relMatch[2];
721
+ let ms;
722
+ if (unit.startsWith("min")) ms = n * 6e4;
723
+ else if (unit.startsWith("hour")) ms = n * 36e5;
724
+ else if (unit.startsWith("day")) ms = n * 864e5;
725
+ else ms = n * 7 * 864e5;
726
+ return new Date(Date.now() - ms);
727
+ }
728
+ const parsed = new Date(value);
729
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
730
+ }
731
+ function parseSQL(sql) {
732
+ const result = {
733
+ select: "*",
734
+ where: null,
735
+ aggregation: null,
736
+ orderBy: null,
737
+ limit: null
738
+ };
739
+ const upper = sql.trim().toUpperCase();
740
+ {
741
+ const m = /^COUNT\s+BY\s+(\w+)/i.exec(sql.trim());
742
+ if (m) {
743
+ result.aggregation = {
744
+ fn: "COUNT",
745
+ field: null,
746
+ groupBy: m[1]
747
+ };
748
+ return result;
749
+ }
750
+ }
751
+ {
752
+ const m = /^GROUP\s+BY\s+(\w+)/i.exec(sql.trim());
753
+ if (m) {
754
+ result.aggregation = {
755
+ fn: "GROUP",
756
+ field: null,
757
+ groupBy: m[1]
758
+ };
759
+ return result;
760
+ }
761
+ }
762
+ {
763
+ const m = /^(AVG|SUM|MIN|MAX)\((\w+)\)\s+BY\s+(\w+)/i.exec(sql.trim());
764
+ if (m) {
765
+ result.aggregation = {
766
+ fn: m[1].toUpperCase(),
767
+ field: m[2],
768
+ groupBy: m[3]
769
+ };
770
+ return result;
771
+ }
772
+ }
773
+ if (upper.startsWith("SELECT")) {
774
+ const fromIdx = upper.indexOf(" FROM ");
775
+ if (fromIdx !== -1) {
776
+ const selectPart = sql.slice(6, fromIdx + sql.length - upper.length).trim();
777
+ if (selectPart !== "*") result.select = selectPart.split(",").map((s) => s.trim()).filter(Boolean);
778
+ }
779
+ }
780
+ const remainder = sql.trim();
781
+ const whereTokenIdx = remainder.toUpperCase().indexOf("WHERE ");
782
+ if (whereTokenIdx !== -1) {
783
+ const afterWhere = remainder.slice(whereTokenIdx + 6).trimStart();
784
+ const upperAfter = afterWhere.toUpperCase();
785
+ const candidates = [upperAfter.indexOf(" ORDER BY "), upperAfter.indexOf(" LIMIT ")].filter((i) => i !== -1);
786
+ const endIdx = candidates.length > 0 ? Math.min(...candidates) : afterWhere.length;
787
+ result.where = parseWhereClause(afterWhere.slice(0, endIdx).trim());
788
+ }
789
+ const orderMatch = /ORDER\s+BY\s+(\w+)(?:\s+(ASC|DESC))?/i.exec(remainder);
790
+ if (orderMatch) {
791
+ var _orderMatch$;
792
+ result.orderBy = {
793
+ field: orderMatch[1],
794
+ dir: ((_orderMatch$ = orderMatch[2]) === null || _orderMatch$ === void 0 ? void 0 : _orderMatch$.toUpperCase()) ?? "ASC"
795
+ };
796
+ }
797
+ const limitMatch = /LIMIT\s+(\d+)/i.exec(remainder);
798
+ if (limitMatch) result.limit = Number.parseInt(limitMatch[1], 10);
799
+ return result;
800
+ }
801
+ /**
802
+ * Split `clause` on a top-level ` AND ` / ` OR ` keyword, ignoring the keyword
803
+ * inside quoted values (e.g. `message LIKE 'foo OR bar'`). Scans with a
804
+ * bounded index walk rather than a regex quantifier so the parser stays
805
+ * ReDoS-safe.
806
+ */
807
+ function splitTopLevel(clause, keyword) {
808
+ const separator = ` ${keyword} `;
809
+ const upper = clause.toUpperCase();
810
+ const parts = [];
811
+ let current = "";
812
+ let quote = null;
813
+ let i = 0;
814
+ while (i < clause.length) {
815
+ const ch = clause[i];
816
+ if (quote !== null) {
817
+ current += ch;
818
+ if (ch === quote) quote = null;
819
+ i += 1;
820
+ continue;
821
+ }
822
+ if (ch === "'" || ch === "\"") {
823
+ quote = ch;
824
+ current += ch;
825
+ i += 1;
826
+ continue;
827
+ }
828
+ if (upper.startsWith(separator, i)) {
829
+ parts.push(current.trim());
830
+ current = "";
831
+ i += separator.length;
832
+ continue;
833
+ }
834
+ current += ch;
835
+ i += 1;
836
+ }
837
+ parts.push(current.trim());
838
+ return parts.filter(Boolean);
839
+ }
840
+ /**
841
+ * Parse a WHERE clause into an AND/OR tree. AND binds tighter than OR, so the
842
+ * clause is first split on top-level `OR`, then each OR branch on top-level
843
+ * `AND`. A single top-level condition is returned unwrapped.
844
+ */
845
+ function parseWhereClause(clause) {
846
+ const orBranches = splitTopLevel(clause.replace(/\s+/g, " ").trim(), "OR").map((orPart) => {
847
+ const conditions = splitTopLevel(orPart, "AND").map((part) => parseCondition(part)).filter((cond) => cond !== null);
848
+ if (conditions.length === 1) return conditions[0];
849
+ return {
850
+ type: "AND",
851
+ conditions
852
+ };
853
+ });
854
+ if (orBranches.length === 0) return {
855
+ type: "AND",
856
+ conditions: []
857
+ };
858
+ if (orBranches.length === 1) return orBranches[0];
859
+ return {
860
+ type: "OR",
861
+ conditions: orBranches
862
+ };
863
+ }
864
+ /** Longest operators first so `>=` / `<=` / `!=` are matched before `>` / `<` / `=`. */
865
+ const COMPARISON_OPS = [
866
+ ">=",
867
+ "<=",
868
+ "!=",
869
+ ">",
870
+ "<",
871
+ "="
872
+ ];
873
+ /** Strip a single matching pair of surrounding single or double quotes. */
874
+ function stripQuotes(value) {
875
+ if (value.length >= 2) {
876
+ const first = value[0];
877
+ const last = value[value.length - 1];
878
+ if (first === "'" && last === "'" || first === "\"" && last === "\"") return value.slice(1, -1);
879
+ }
880
+ return value;
881
+ }
882
+ function parseCondition(expr) {
883
+ const trimmed = expr.trim();
884
+ const fieldMatch = /^(\w+)/.exec(trimmed);
885
+ if (!fieldMatch) return null;
886
+ const field = fieldMatch[1];
887
+ const rest = trimmed.slice(field.length).trimStart();
888
+ const notLikePrefix = /^NOT\s+LIKE\s+/i.exec(rest);
889
+ if (notLikePrefix) return {
890
+ field,
891
+ op: "NOT LIKE",
892
+ value: stripQuotes(rest.slice(notLikePrefix[0].length).trim()),
893
+ caseless: true
894
+ };
895
+ const likePrefix = /^LIKE\s+/i.exec(rest);
896
+ if (likePrefix) return {
897
+ field,
898
+ op: "LIKE",
899
+ value: stripQuotes(rest.slice(likePrefix[0].length).trim()),
900
+ caseless: true
901
+ };
902
+ const notInPrefix = /^NOT\s+IN\s*\(/i.exec(rest);
903
+ if (notInPrefix) return {
904
+ field,
905
+ op: "NOT IN",
906
+ value: rest.slice(notInPrefix[0].length, rest.lastIndexOf(")")).split(",").map((v) => v.trim().replace(/^'|'$/g, ""))
907
+ };
908
+ const inPrefix = /^IN\s*\(/i.exec(rest);
909
+ if (inPrefix) return {
910
+ field,
911
+ op: "IN",
912
+ value: rest.slice(inPrefix[0].length, rest.lastIndexOf(")")).split(",").map((v) => v.trim().replace(/^'|'$/g, ""))
913
+ };
914
+ for (const op of COMPARISON_OPS) if (rest.startsWith(op)) {
915
+ const rawValue = rest.slice(op.length).trim().replace(/^'|'$/g, "");
916
+ const numValue = Number(rawValue);
917
+ return {
918
+ field,
919
+ op,
920
+ value: Number.isNaN(numValue) ? rawValue : numValue
921
+ };
922
+ }
923
+ return null;
924
+ }
925
+ function matchesCondition(row, cond) {
926
+ const rawFieldValue = row[cond.field];
927
+ if (rawFieldValue === void 0) return false;
928
+ const fieldStr = String(rawFieldValue).toLowerCase();
929
+ const condStr = cond.caseless ? String(cond.value).toLowerCase() : String(cond.value);
930
+ switch (cond.op) {
931
+ case "=": return String(rawFieldValue) === String(cond.value);
932
+ case "!=": return String(rawFieldValue) !== String(cond.value);
933
+ case ">": return Number(rawFieldValue) > Number(cond.value);
934
+ case ">=": return Number(rawFieldValue) >= Number(cond.value);
935
+ case "<": return Number(rawFieldValue) < Number(cond.value);
936
+ case "<=": return Number(rawFieldValue) <= Number(cond.value);
937
+ case "LIKE": {
938
+ const pattern = condStr.replace(/%/g, ".*").replace(/_/g, ".");
939
+ return new RegExp(`^${pattern}$`, "i").test(fieldStr);
940
+ }
941
+ case "NOT LIKE": {
942
+ const pattern = condStr.replace(/%/g, ".*").replace(/_/g, ".");
943
+ return !new RegExp(`^${pattern}$`, "i").test(fieldStr);
944
+ }
945
+ case "IN": return cond.value.some((v) => String(rawFieldValue) === v);
946
+ case "NOT IN": return !cond.value.some((v) => String(rawFieldValue) === v);
947
+ default: return false;
948
+ }
949
+ }
950
+ function matchesWhere(row, where) {
951
+ if (!where) return true;
952
+ return matchesNode(row, where);
953
+ }
954
+ /** Evaluate a WHERE tree: AND over its children, OR over its children. */
955
+ function matchesNode(row, node) {
956
+ if ("op" in node) return matchesCondition(row, node);
957
+ const children = node.conditions;
958
+ if (children.length === 0) return true;
959
+ return node.type === "AND" ? children.every((child) => matchesNode(row, child)) : children.some((child) => matchesNode(row, child));
960
+ }
961
+ function runAggregation(entries, agg) {
962
+ const { fn, field, groupBy } = agg;
963
+ if (!groupBy) return picocolors.default.yellow("Aggregation requires a GROUP BY field");
964
+ const groups = {};
965
+ for (const entry of entries) {
966
+ const key = String(entry[groupBy] ?? "(empty)");
967
+ if (!groups[key]) groups[key] = [];
968
+ groups[key].push(entry);
969
+ }
970
+ if (fn === "COUNT" || fn === "GROUP") return formatAsTable(Object.entries(groups).map(([k, v]) => ({
971
+ [groupBy]: k,
972
+ count: v.length
973
+ })).sort((a, b) => b.count - a.count), [groupBy, "count"]);
974
+ if (!field) return picocolors.default.yellow("Numeric aggregation requires a field: AVG(field) BY group");
975
+ return formatAsTable(Object.entries(groups).map(([k, v]) => {
976
+ const nums = v.map((e) => Number(e[field])).filter((n) => !Number.isNaN(n));
977
+ let result;
978
+ switch (fn) {
979
+ case "AVG":
980
+ result = nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
981
+ break;
982
+ case "SUM":
983
+ result = nums.reduce((a, b) => a + b, 0);
984
+ break;
985
+ case "MIN":
986
+ result = nums.length ? Math.min(...nums) : 0;
987
+ break;
988
+ case "MAX":
989
+ result = nums.length ? Math.max(...nums) : 0;
990
+ break;
991
+ default: result = 0;
992
+ }
993
+ return {
994
+ [groupBy]: k,
995
+ [`${fn}(${field})`]: Math.round(result * 100) / 100,
996
+ count: v.length
997
+ };
998
+ }).sort((a, b) => {
999
+ const key = `${fn}(${field})`;
1000
+ return b[key] - a[key];
1001
+ }), [
1002
+ groupBy,
1003
+ `${fn}(${field})`,
1004
+ "count"
1005
+ ]);
1006
+ }
1007
+ function projectRow(row, select) {
1008
+ if (select === "*") return row;
1009
+ const projected = {};
1010
+ for (const field of select) projected[field] = row[field];
1011
+ return projected;
1012
+ }
1013
+ function formatResults(results, format) {
1014
+ if (results.length === 0) return picocolors.default.yellow("No results found");
1015
+ if (format === "json") return JSON.stringify(results, null, 2);
1016
+ if (format === "ndjson") return results.map((r) => JSON.stringify(r)).join("\n");
1017
+ if (format === "table") return formatAsTable(results, Object.keys(results[0] || {}).slice(0, 6));
1018
+ return results.map((r) => {
1019
+ const level = (r.level || "").toUpperCase();
1020
+ const line = JSON.stringify(r);
1021
+ if (level === "ERROR" || level === "CRITICAL") return picocolors.default.red(line);
1022
+ if (level === "WARN") return picocolors.default.yellow(line);
1023
+ if (level === "DEBUG" || level === "VERBOSE") return picocolors.default.gray(line);
1024
+ return line;
1025
+ }).join("\n");
1026
+ }
1027
+ function executeQuery(entries, opts) {
1028
+ var _query$orderBy;
1029
+ let rows = entries;
1030
+ const sinceDate = opts.since ? parseTimeRange(opts.since) : void 0;
1031
+ const untilDate = opts.until ? parseTimeRange(opts.until) : void 0;
1032
+ if (sinceDate || untilDate) rows = rows.filter((row) => {
1033
+ const ts = row.timestamp ? new Date(row.timestamp) : null;
1034
+ if (!ts || Number.isNaN(ts.getTime())) return true;
1035
+ if (sinceDate && ts < sinceDate) return false;
1036
+ if (untilDate && ts > untilDate) return false;
1037
+ return true;
1038
+ });
1039
+ const query = opts.sql ? parseSQL(opts.sql) : {
1040
+ select: "*",
1041
+ where: null,
1042
+ aggregation: null,
1043
+ orderBy: null,
1044
+ limit: null
1045
+ };
1046
+ if (query.where) rows = rows.filter((row) => matchesWhere(row, query.where));
1047
+ if (query.aggregation) return {
1048
+ results: [],
1049
+ aggregationOutput: runAggregation(rows, query.aggregation)
1050
+ };
1051
+ rows = rows.map((row) => projectRow(row, query.select));
1052
+ const orderField = ((_query$orderBy = query.orderBy) === null || _query$orderBy === void 0 ? void 0 : _query$orderBy.field) ?? opts.orderBy;
1053
+ if (orderField) {
1054
+ var _query$orderBy2;
1055
+ const dir = ((_query$orderBy2 = query.orderBy) === null || _query$orderBy2 === void 0 ? void 0 : _query$orderBy2.dir) ?? "ASC";
1056
+ rows.sort((a, b) => {
1057
+ const av = a[orderField];
1058
+ const bv = b[orderField];
1059
+ if (av === void 0 && bv === void 0) return 0;
1060
+ if (av === void 0) return dir === "ASC" ? 1 : -1;
1061
+ if (bv === void 0) return dir === "ASC" ? -1 : 1;
1062
+ if (typeof av === "number" && typeof bv === "number") return dir === "ASC" ? av - bv : bv - av;
1063
+ return dir === "ASC" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
1064
+ });
1065
+ }
1066
+ const limitN = query.limit ?? opts.limit;
1067
+ if (limitN && limitN > 0) rows = rows.slice(0, limitN);
1068
+ return {
1069
+ results: rows,
1070
+ aggregationOutput: null
1071
+ };
1072
+ }
1073
+ const queryCommand = new commander.Command("query").description("Query log files with SQL-like syntax\n\n Examples:\n logixia query app.log --sql \"SELECT * FROM logs WHERE level='error'\"\n logixia query app.log --sql \"COUNT BY level\"\n logixia query app.log --sql \"AVG(duration) BY endpoint\"\n logixia query app.log --since \"last 2 hours\" --format table\n logixia query app.log --follow --sql \"WHERE level='error'\"\n").argument("<file>", "Path to NDJSON or JSON log file").option("--sql <query>", "SQL-like query string").option("--since <time>", "Only include entries after this time (e.g. \"last 2 hours\", \"today\")").option("--until <time>", "Only include entries before this time").option("--limit <n>", "Maximum number of results to return", "0").option("--order-by <field>", "Sort results by this field").option("--format <fmt>", "Output format: line, table, json, ndjson", "line").option("--follow", "Follow file for new entries matching the query (tail mode)", false).action(async (file, opts) => {
1074
+ const full = node_path.default.resolve(process.cwd(), file);
1075
+ if (!node_fs.default.existsSync(full)) {
1076
+ console.error(picocolors.default.red(`File not found: ${full}`));
1077
+ process.exit(2);
1078
+ }
1079
+ const entries = safeParseLogs(node_fs.default.readFileSync(full, "utf8"));
1080
+ const limitN = opts.limit ? Number.parseInt(opts.limit, 10) : 0;
1081
+ const { results, aggregationOutput } = executeQuery(entries, {
1082
+ sql: opts.sql,
1083
+ since: opts.since,
1084
+ until: opts.until,
1085
+ limit: limitN,
1086
+ orderBy: opts.orderBy
1087
+ });
1088
+ if (aggregationOutput !== null) {
1089
+ console.log(picocolors.default.bold("\nAggregation result:"));
1090
+ console.log(aggregationOutput);
1091
+ } else {
1092
+ console.log(picocolors.default.bold(`\nFound ${picocolors.default.cyan(String(results.length))} match${results.length === 1 ? "" : "es"}`));
1093
+ if (results.length > 0) console.log(formatResults(results, opts.format || "line"));
1094
+ }
1095
+ if (opts.follow) {
1096
+ console.log(picocolors.default.dim("\n--- following (ctrl-c to exit) ---"));
1097
+ let pos = node_fs.default.statSync(full).size;
1098
+ let buffer = "";
1099
+ node_fs.default.watchFile(full, { interval: 300 }, (curr) => {
1100
+ if (curr.size <= pos) return;
1101
+ const rs = node_fs.default.createReadStream(full, {
1102
+ start: pos,
1103
+ end: curr.size,
1104
+ encoding: "utf8"
1105
+ });
1106
+ rs.on("data", (chunk) => {
1107
+ buffer += chunk;
1108
+ const lines = buffer.split(/\r?\n/);
1109
+ buffer = lines.pop() ?? "";
1110
+ for (const line of lines.filter(Boolean)) try {
1111
+ const { results: tailResults } = executeQuery([JSON.parse(line)], {
1112
+ sql: opts.sql,
1113
+ since: opts.since,
1114
+ until: opts.until
1115
+ });
1116
+ if (tailResults.length > 0) console.log(formatResults(tailResults, opts.format || "line"));
1117
+ } catch {
1118
+ if (!opts.sql) console.log(line);
1119
+ }
1120
+ });
1121
+ rs.on("end", () => {
1122
+ pos = curr.size;
1123
+ });
1124
+ });
1125
+ }
1126
+ });
1127
+
1128
+ //#endregion
1129
+ //#region src/cli/commands/search.ts
1130
+ function parseQuery(query) {
1131
+ const match = query.match(/^(\w+):(.+)$/);
1132
+ if (match) return {
1133
+ field: match[1],
1134
+ value: match[2]
1135
+ };
1136
+ return { value: query };
1137
+ }
1138
+ async function searchLogs(raw, opts = {}) {
1139
+ const entries = safeParseLogs(raw);
1140
+ const criteria = parseQuery(opts.query || "");
1141
+ return entries.filter((entry) => {
1142
+ const row = entry;
1143
+ if (criteria.field) {
1144
+ const fieldValue = row[criteria.field];
1145
+ if (fieldValue === void 0) return false;
1146
+ return String(fieldValue).toLowerCase().includes(criteria.value.toLowerCase());
1147
+ } else return JSON.stringify(row).toLowerCase().includes(criteria.value.toLowerCase());
1148
+ });
1149
+ }
1150
+ function formatSearchResults(results, format, _context) {
1151
+ if (format === "json") return JSON.stringify(results, null, 2);
1152
+ if (format === "table") {
1153
+ if (results.length === 0) return picocolors.default.yellow("No results found");
1154
+ return formatAsTable(results, Object.keys(results[0] || {}).slice(0, 4));
1155
+ }
1156
+ if (results.length === 0) return picocolors.default.yellow("No results found");
1157
+ return results.map((r, idx) => {
1158
+ const line = JSON.stringify(r);
1159
+ return `${picocolors.default.gray(String(idx + 1) + ":")} ${line}`;
1160
+ }).join("\n");
1161
+ }
1162
+ const searchCommand = new commander.Command("search").description("Search log files with query patterns").argument("<file>", "Path to log file").option("--query <query>", "Search query (e.g., \"user_id:123\" or \"error\")", "").option("--format <fmt>", "Output format (table, json, line)", "line").option("--context <lines>", "Lines of context around matches", "0").action(async (file, opts) => {
1163
+ const full = node_path.default.resolve(process.cwd(), file);
1164
+ if (!node_fs.default.existsSync(full)) {
1165
+ console.error(`File not found: ${full}`);
1166
+ process.exit(2);
1167
+ }
1168
+ if (!opts.query) {
1169
+ console.error("--query is required");
1170
+ process.exit(2);
1171
+ }
1172
+ const results = await searchLogs(node_fs.default.readFileSync(full, "utf8"), opts);
1173
+ console.log(picocolors.default.bold(`\nFound ${results.length} matches`));
1174
+ console.log(formatSearchResults(results, opts.format, Number.parseInt(opts.context || "0")));
1175
+ });
1176
+
1177
+ //#endregion
1178
+ //#region src/cli/commands/stats.ts
1179
+ async function generateStats(raw, opts = {}) {
1180
+ const entries = safeParseLogs(raw);
1181
+ const group = opts.groupBy || "level";
1182
+ const agg = {};
1183
+ let minTime = null;
1184
+ let maxTime = null;
1185
+ for (const e of entries) {
1186
+ const key = (e[group] || e.level || "unknown").toString().toUpperCase();
1187
+ agg[key] = (agg[key] || 0) + 1;
1188
+ if (e.timestamp) {
1189
+ const ts = new Date(e.timestamp);
1190
+ if (!minTime || ts < minTime) minTime = ts;
1191
+ if (!maxTime || ts > maxTime) maxTime = ts;
1192
+ }
1193
+ }
1194
+ return {
1195
+ total: entries.length,
1196
+ groupBy: group,
1197
+ distribution: agg,
1198
+ timeRange: minTime && maxTime ? {
1199
+ start: minTime,
1200
+ end: maxTime
1201
+ } : null
1202
+ };
1203
+ }
1204
+ function formatStatsOutput(stats, filename) {
1205
+ const s = stats;
1206
+ const lines = [];
1207
+ lines.push(picocolors.default.bold(`\nLog Statistics for ${node_path.default.basename(filename)}`));
1208
+ lines.push(picocolors.default.bold("=".repeat(50)));
1209
+ lines.push(picocolors.default.cyan(`Total Entries: ${s["total"].toLocaleString()}`));
1210
+ if (s["timeRange"]) lines.push(picocolors.default.cyan(`Time Range: ${s["timeRange"]["start"].toISOString()} - ${s["timeRange"]["end"].toISOString()}`));
1211
+ lines.push("");
1212
+ lines.push(picocolors.default.bold(`${s["groupBy"].charAt(0).toUpperCase() + s["groupBy"].slice(1)} Distribution:`));
1213
+ const sorted = Object.entries(s["distribution"]).sort(([, a], [, b]) => b - a);
1214
+ for (const [key, count] of sorted) {
1215
+ const percentage = (count / s["total"] * 100).toFixed(1);
1216
+ const bar = "█".repeat(Math.floor(count / s["total"] * 30));
1217
+ let colorFn = picocolors.default.white;
1218
+ if (key === "ERROR") colorFn = picocolors.default.red;
1219
+ else if (key === "WARN") colorFn = picocolors.default.yellow;
1220
+ else if (key === "INFO") colorFn = picocolors.default.blue;
1221
+ else if (key === "DEBUG") colorFn = picocolors.default.gray;
1222
+ lines.push(colorFn(` ${key.padEnd(10)} ${String(count).padStart(8)} (${percentage.padStart(5)}%) ${bar}`));
1223
+ }
1224
+ lines.push("");
1225
+ return lines.join("\n");
1226
+ }
1227
+ const statsCommand = new commander.Command("stats").description("Show statistics for a log file").argument("<file>", "Path to log file").option("--group-by <field>", "Field to group by (default: level)", "level").option("--format <fmt>", "Output format (pretty, json)", "pretty").action(async (file, opts) => {
1228
+ const full = node_path.default.resolve(process.cwd(), file);
1229
+ if (!node_fs.default.existsSync(full)) {
1230
+ console.error(`File not found: ${full}`);
1231
+ process.exit(2);
1232
+ }
1233
+ const stats = await generateStats(node_fs.default.readFileSync(full, "utf8"), opts);
1234
+ if (opts.format === "json") console.log(JSON.stringify(stats, null, 2));
1235
+ else console.log(formatStatsOutput(stats, file));
1236
+ });
1237
+
1238
+ //#endregion
1239
+ //#region src/cli/commands/tail.ts
1240
+ function parseFilter(filter) {
1241
+ if (!filter) return null;
1242
+ const match = filter.match(/^(\w+):(.+)$/);
1243
+ if (!match) return null;
1244
+ return {
1245
+ field: match[1],
1246
+ value: match[2]
1247
+ };
1248
+ }
1249
+ function applyFilter(line, filter) {
1250
+ if (!filter) return true;
1251
+ try {
1252
+ const fieldValue = JSON.parse(line)[filter.field];
1253
+ if (fieldValue === void 0) return false;
1254
+ return fieldValue.toString().toLowerCase().includes(filter.value.toLowerCase());
1255
+ } catch {
1256
+ return line.toLowerCase().includes(filter.value.toLowerCase());
1257
+ }
1258
+ }
1259
+ function highlightLine(line, highlight) {
1260
+ if (!highlight) return line;
1261
+ try {
1262
+ const level = (JSON.parse(line).level || "").toUpperCase();
1263
+ if (level === "ERROR") return picocolors.default.red(line);
1264
+ if (level === "WARN") return picocolors.default.yellow(line);
1265
+ if (level === "INFO") return picocolors.default.blue(line);
1266
+ if (level === "DEBUG") return picocolors.default.gray(line);
1267
+ return line;
1268
+ } catch {
1269
+ const regex = new RegExp(highlight, "gi");
1270
+ return line.replace(regex, (match) => picocolors.default.bgYellow(picocolors.default.black(match)));
1271
+ }
1272
+ }
1273
+ const tailCommand = new commander.Command("tail").description("Follow log files in real-time").argument("<file>", "Path to log file").option("--follow", "Follow file changes", false).option("--filter <filter>", "Filter criteria (e.g., level:error, user_id:123)").option("--highlight <pattern>", "Highlight pattern or color by level").action(async (file, opts) => {
1274
+ const full = node_path.default.resolve(process.cwd(), file);
1275
+ if (!node_fs.default.existsSync(full)) {
1276
+ console.error(`File not found: ${full}`);
1277
+ process.exit(2);
1278
+ }
1279
+ const filterCriteria = parseFilter(opts.filter);
1280
+ const last = node_fs.default.readFileSync(full, "utf8").split(/\r?\n/).filter(Boolean).filter((line) => applyFilter(line, filterCriteria)).slice(-10);
1281
+ for (const line of last) console.log(highlightLine(line, opts.highlight));
1282
+ if (opts.follow) {
1283
+ console.log(picocolors.default.dim("--- following (ctrl-c to exit) ---"));
1284
+ let pos = node_fs.default.statSync(full).size;
1285
+ let buffer = "";
1286
+ node_fs.default.watchFile(full, { interval: 500 }, (curr) => {
1287
+ if (curr.size > pos) {
1288
+ const rs = node_fs.default.createReadStream(full, {
1289
+ start: pos,
1290
+ end: curr.size,
1291
+ encoding: "utf8"
1292
+ });
1293
+ rs.on("data", (chunk) => {
1294
+ buffer += chunk;
1295
+ const lines = buffer.split(/\r?\n/);
1296
+ buffer = lines.pop() || "";
1297
+ for (const line of lines.filter(Boolean)) if (applyFilter(line, filterCriteria)) console.log(highlightLine(line, opts.highlight));
1298
+ });
1299
+ rs.on("end", () => {
1300
+ pos = curr.size;
1301
+ });
1302
+ }
1303
+ });
1304
+ }
1305
+ });
1306
+
1307
+ //#endregion
1308
+ //#region src/cli/index.ts
1309
+ const pkgPath = node_path.default.resolve(__dirname, "../..", "package.json");
1310
+ let pkg;
1311
+ try {
1312
+ pkg = JSON.parse(node_fs.default.readFileSync(pkgPath, "utf8"));
1313
+ } catch {
1314
+ pkg = { version: "0.0.0" };
1315
+ }
1316
+ const program = new commander.Command();
1317
+ program.name("logixia").description("Logixia CLI for log management and analysis").version(pkg.version || "0.0.0").addHelpText("after", `\n${picocolors.default.dim("Sponsor logixia:")} https://github.com/sponsors/webcoderspeed · https://paypal.me/Sniperspeed`);
1318
+ program.addCommand(analyzeCommand);
1319
+ program.addCommand(tailCommand);
1320
+ program.addCommand(statsCommand);
1321
+ program.addCommand(searchCommand);
1322
+ program.addCommand(queryCommand);
1323
+ program.addCommand(exportCommand);
1324
+ program.addCommand(exploreCommand);
1325
+ program.on("command:*", () => {
1326
+ console.error(picocolors.default.red("Invalid command: %s\nSee --help for a list of available commands."), program.args.join(" "));
1327
+ process.exit(1);
1328
+ });
1329
+ function isDirectRun(argv1, file) {
1330
+ if (!argv1) return false;
1331
+ try {
1332
+ return node_fs.default.realpathSync(argv1) === node_fs.default.realpathSync(file);
1333
+ } catch {
1334
+ return false;
1335
+ }
1336
+ }
1337
+ if (isDirectRun(process.argv[1], __filename)) program.parse(process.argv);
1338
+
1339
+ //#endregion
1340
+ exports.isDirectRun = isDirectRun;
1341
+ exports.program = program;