@lowdep/log-tail 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rushabh Shah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # log-tail
2
+
3
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen) ![Node](https://img.shields.io/badge/node-%3E%3D14-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
4
+
5
+
6
+ Cross-platform `tail -f` replacement with regex filtering, JSON log parsing, level-based colorization, and multi-file watching. Zero dependencies.
7
+
8
+ Windows has no `tail -f`. PowerShell's `Get-Content -Wait` is clunky and doesn't filter. `log-tail` runs anywhere Node.js does.
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g log-tail
16
+ ```
17
+
18
+ Or without installing:
19
+
20
+ ```bash
21
+ npx log-tail server.log
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ log-tail server.log # tail -f equivalent
30
+ log-tail app.log error.log # Multiple files, labeled output
31
+ log-tail server.log --grep "ERROR|WARN" # Filter lines
32
+ log-tail access.log --highlight "404" # Highlight matches in red
33
+ log-tail app.json --json # Pretty-print JSON log lines
34
+ log-tail server.log -n 100 --no-follow # Like `tail -100`, no follow
35
+ log-tail server.log --exclude "/health" # Skip noisy lines
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Auto-Colorization
41
+
42
+ Lines containing standard log levels are colored automatically:
43
+
44
+ | Pattern | Color |
45
+ |---|---|
46
+ | `ERROR`, `FATAL`, `CRITICAL` | red |
47
+ | `WARN`, `WARNING` | yellow |
48
+ | `INFO`, `NOTICE` | cyan |
49
+ | `DEBUG`, `TRACE` | dim |
50
+
51
+ HTTP status codes (`4xx`, `5xx`, etc.) are also highlighted.
52
+
53
+ Disable with `--no-color`.
54
+
55
+ ---
56
+
57
+ ## JSON Log Mode
58
+
59
+ Given lines like:
60
+ ```json
61
+ {"level":"error","msg":"DB query timed out","timestamp":"2024-08-01T12:00:00Z","duration_ms":4012}
62
+ ```
63
+
64
+ Run `log-tail app.log --json`:
65
+ ```
66
+ 2024-08-01T12:00:00Z ERROR DB query timed out duration_ms=4012
67
+ ```
68
+
69
+ Recognized fields:
70
+ - Message: `message`, `msg`, `text`
71
+ - Level: `level`, `severity`, `lvl`
72
+ - Timestamp: `timestamp`, `time`, `ts`
73
+
74
+ All other fields are shown as `key=value` extras.
75
+
76
+ ---
77
+
78
+ ## Multi-File
79
+
80
+ When watching multiple files, each line is labeled with the filename in its own color:
81
+
82
+ ```
83
+ server.log │ GET /api/users 200 12ms
84
+ worker.log │ Processing job #12345
85
+ server.log │ POST /api/login 401 4ms
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Options
91
+
92
+ | Flag | Default | Description |
93
+ |---|---|---|
94
+ | `-n, --lines <N>` | `10` | Show last N lines before following |
95
+ | `--follow / --no-follow` | follow | Keep watching for new lines |
96
+ | `--grep <pattern>` | — | Only show lines matching regex |
97
+ | `--exclude <pattern>` | — | Skip lines matching regex |
98
+ | `--highlight <pattern>` | — | Highlight matches in bold red |
99
+ | `--interval <ms>` | `250` | Poll interval |
100
+ | `--json` | off | Pretty-print JSON log lines |
101
+ | `--no-color` | off | Disable colorization |
102
+
103
+ ---
104
+
105
+ ## License
106
+
107
+ MIT
108
+
109
+ ---
110
+
111
+ ## Keywords
112
+
113
+ `tail -f windows` · `tail follow` · `log viewer` · `follow logs` · `tail logs` · `log tail` · `live logs` · `json logs` · `cross-platform` · `zero dependencies`
114
+
115
+ ---
116
+
117
+ <div align="center">
118
+
119
+ **Built to solve, shared to help — Rushabh Shah 🛠️✨**
120
+
121
+ <sub>One of 40+ zero-dependency developer CLI tools — no <code>node_modules</code>, ever.</sub>
122
+
123
+ </div>
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const VERSION = '1.0.0';
8
+
9
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
10
+ const isTTY = process.stdout.isTTY;
11
+ const c = (code, t) => isTTY ? `\x1b[${code}m${t}\x1b[0m` : t;
12
+ const bold = t => c('1', t);
13
+ const dim = t => c('2', t);
14
+ const red = t => c('31', t);
15
+ const green = t => c('32', t);
16
+ const yellow = t => c('33', t);
17
+ const cyan = t => c('36', t);
18
+ const blue = t => c('34', t);
19
+ const magenta = t => c('35', t);
20
+
21
+ const FILE_COLORS = [cyan, magenta, yellow, blue, green];
22
+
23
+ // ─── Log line colorization (level detection) ──────────────────────────────────
24
+ const LEVEL_PATTERNS = [
25
+ { re: /\b(ERROR|FATAL|CRITICAL|EMERG)\b/i, color: red },
26
+ { re: /\b(WARN|WARNING)\b/i, color: yellow },
27
+ { re: /\b(INFO|NOTICE)\b/i, color: cyan },
28
+ { re: /\b(DEBUG|TRACE)\b/i, color: dim },
29
+ ];
30
+
31
+ function colorizeLine(line, colorize) {
32
+ if (!colorize) return line;
33
+ for (const { re, color } of LEVEL_PATTERNS) {
34
+ if (re.test(line)) {
35
+ return color(line);
36
+ }
37
+ }
38
+ // Highlight HTTP status codes
39
+ return line.replace(/\b([45]\d\d)\b/g, m => red(m))
40
+ .replace(/\b(3\d\d)\b/g, m => yellow(m))
41
+ .replace(/\b(2\d\d)\b/g, m => green(m));
42
+ }
43
+
44
+ // ─── JSON line pretty-printer ─────────────────────────────────────────────────
45
+ function tryParseJson(line) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null;
48
+ try { return JSON.parse(trimmed); } catch { return null; }
49
+ }
50
+
51
+ function formatJsonLine(obj) {
52
+ const level = obj.level || obj.severity || obj.lvl || '';
53
+ const msg = obj.message || obj.msg || obj.text || '';
54
+ const ts = obj.timestamp || obj.time || obj.ts || '';
55
+ const levelUp = String(level).toUpperCase();
56
+
57
+ let levelColored = levelUp;
58
+ if (/ERROR|FATAL/.test(levelUp)) levelColored = red(levelUp.padEnd(5));
59
+ else if (/WARN/.test(levelUp)) levelColored = yellow(levelUp.padEnd(5));
60
+ else if (/INFO/.test(levelUp)) levelColored = cyan(levelUp.padEnd(5));
61
+ else if (/DEBUG/.test(levelUp)) levelColored = dim(levelUp.padEnd(5));
62
+ else if (levelUp) levelColored = levelUp.padEnd(5);
63
+
64
+ const extras = Object.entries(obj)
65
+ .filter(([k]) => !['level', 'severity', 'lvl', 'message', 'msg', 'text', 'timestamp', 'time', 'ts'].includes(k))
66
+ .map(([k, v]) => `${dim(k)}=${typeof v === 'object' ? JSON.stringify(v) : v}`)
67
+ .join(' ');
68
+
69
+ return `${dim(ts)} ${levelColored} ${msg} ${dim(extras)}`.trim();
70
+ }
71
+
72
+ // ─── Tail one file ────────────────────────────────────────────────────────────
73
+ function tailFile(filePath, opts, onLine, fileLabel) {
74
+ const { lines, follow, pollInterval } = opts;
75
+
76
+ let position = 0;
77
+ let leftover = '';
78
+
79
+ // Read last N lines from end
80
+ if (lines > 0 && fs.existsSync(filePath)) {
81
+ const stat = fs.statSync(filePath);
82
+ const size = stat.size;
83
+ const chunkSize = Math.min(size, 64 * 1024 * Math.max(1, Math.ceil(lines / 100)));
84
+ if (chunkSize > 0) {
85
+ const fd = fs.openSync(filePath, 'r');
86
+ const buf = Buffer.alloc(chunkSize);
87
+ fs.readSync(fd, buf, 0, chunkSize, Math.max(0, size - chunkSize));
88
+ fs.closeSync(fd);
89
+ const text = buf.toString('utf8');
90
+ const lineArr = text.split(/\r?\n/);
91
+ // Drop trailing empty string if file ended with newline
92
+ if (lineArr.length && lineArr[lineArr.length - 1] === '') lineArr.pop();
93
+ // Drop first entry if our read window started mid-line
94
+ if (chunkSize < size && lineArr.length) lineArr.shift();
95
+ const lastN = lineArr.slice(-lines);
96
+ for (const line of lastN) if (line) onLine(line, fileLabel);
97
+ }
98
+ position = size;
99
+ }
100
+
101
+ if (!follow) return null;
102
+
103
+ // Poll for changes (simpler & more reliable than fs.watch across platforms)
104
+ return setInterval(() => {
105
+ try {
106
+ const stat = fs.statSync(filePath);
107
+ if (stat.size < position) {
108
+ // Truncated/rotated — reset
109
+ position = 0;
110
+ leftover = '';
111
+ }
112
+ if (stat.size > position) {
113
+ const fd = fs.openSync(filePath, 'r');
114
+ const length = stat.size - position;
115
+ const buf = Buffer.alloc(length);
116
+ fs.readSync(fd, buf, 0, length, position);
117
+ fs.closeSync(fd);
118
+ position += length;
119
+ const chunk = leftover + buf.toString('utf8');
120
+ const parts = chunk.split(/\r?\n/);
121
+ leftover = parts.pop();
122
+ for (const line of parts) if (line) onLine(line, fileLabel);
123
+ }
124
+ } catch { /* file may be temporarily unavailable */ }
125
+ }, pollInterval);
126
+ }
127
+
128
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
129
+ const args = process.argv.slice(2);
130
+ const VALUE_FLAGS = new Set(['--lines', '-n', '--grep', '--exclude', '--interval', '--highlight']);
131
+
132
+ function getFlag(...names) {
133
+ for (const f of names) {
134
+ const i = args.indexOf(f);
135
+ if (i !== -1) return args[i + 1];
136
+ }
137
+ return null;
138
+ }
139
+ function hasFlag(...names) { return names.some(f => args.includes(f)); }
140
+
141
+ const positional = [];
142
+ for (let i = 0; i < args.length; i++) {
143
+ if (args[i].startsWith('-')) {
144
+ if (VALUE_FLAGS.has(args[i])) i++;
145
+ } else positional.push(args[i]);
146
+ }
147
+
148
+ if (hasFlag('--version', '-v')) {
149
+ console.log(`log-tail v${VERSION}`); process.exit(0);
150
+ }
151
+
152
+ if (hasFlag('--help', '-h') || !positional.length) {
153
+ console.log(`
154
+ ${bold('log-tail')} — Cross-platform tail -f replacement with regex filtering
155
+
156
+ ${bold('USAGE')}
157
+ log-tail <file...> [options]
158
+
159
+ ${bold('OPTIONS')}
160
+ -n, --lines <N> Show last N lines before following (default: 10)
161
+ -f, --follow Keep watching for new lines (default: on for single TTY)
162
+ --no-follow Print last N lines and exit
163
+ --grep <pattern> Only show lines matching regex
164
+ --exclude <pattern> Skip lines matching regex
165
+ --highlight <pat> Highlight matches in bold red
166
+ --interval <ms> Poll interval (default: 250)
167
+ --no-color Disable level/status colorization
168
+ --json Pretty-print JSON log lines (level, msg, extras)
169
+ --version Show version
170
+
171
+ ${bold('EXAMPLES')}
172
+ log-tail server.log # tail -f equivalent
173
+ log-tail app.log error.log # Multiple files, labeled output
174
+ log-tail server.log --grep "ERROR|WARN" # Filter
175
+ log-tail access.log --highlight "404" # Highlight 404s
176
+ log-tail app.json --json # Pretty-print JSON logs
177
+ log-tail server.log -n 100 --no-follow # Like tail -100
178
+ `);
179
+ process.exit(positional.length ? 0 : 1);
180
+ }
181
+
182
+ const files = positional.map(p => path.resolve(p));
183
+ const numLines = parseInt(getFlag('--lines', '-n') || '10', 10);
184
+ const noFollow = hasFlag('--no-follow');
185
+ const follow = !noFollow;
186
+ const grepPattern = getFlag('--grep') ? new RegExp(getFlag('--grep')) : null;
187
+ const excludePat = getFlag('--exclude') ? new RegExp(getFlag('--exclude')) : null;
188
+ const highlight = getFlag('--highlight') ? new RegExp(getFlag('--highlight'), 'g') : null;
189
+ const pollInterval= parseInt(getFlag('--interval') || '250', 10);
190
+ const noColor = hasFlag('--no-color');
191
+ const jsonMode = hasFlag('--json');
192
+
193
+ for (const f of files) {
194
+ if (!fs.existsSync(f)) {
195
+ console.error(red(`\nFile not found: ${f}\n`)); process.exit(1);
196
+ }
197
+ }
198
+
199
+ const showLabel = files.length > 1;
200
+
201
+ function emit(line, fileLabel) {
202
+ if (grepPattern && !grepPattern.test(line)) return;
203
+ if (excludePat && excludePat.test(line)) return;
204
+
205
+ let out = line;
206
+
207
+ if (jsonMode) {
208
+ const obj = tryParseJson(line);
209
+ if (obj) out = formatJsonLine(obj);
210
+ }
211
+
212
+ if (!noColor) out = colorizeLine(out, true);
213
+
214
+ if (highlight) {
215
+ out = out.replace(highlight, m => bold(red(m)));
216
+ }
217
+
218
+ if (showLabel) {
219
+ process.stdout.write(`${fileLabel} ${dim('│')} ${out}\n`);
220
+ } else {
221
+ process.stdout.write(out + '\n');
222
+ }
223
+ }
224
+
225
+ console.log();
226
+ const intervals = [];
227
+ files.forEach((f, i) => {
228
+ const label = showLabel ? FILE_COLORS[i % FILE_COLORS.length](path.basename(f)) : '';
229
+ const iv = tailFile(f, { lines: numLines, follow, pollInterval }, emit, label);
230
+ if (iv) intervals.push(iv);
231
+ });
232
+
233
+ if (follow) {
234
+ process.on('SIGINT', () => {
235
+ intervals.forEach(clearInterval);
236
+ console.log(dim('\n Stopped.\n'));
237
+ process.exit(0);
238
+ });
239
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@lowdep/log-tail",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform tail -f with regex filtering, multi-file, JSON pretty-print, and color — zero dependencies",
5
+ "bin": {
6
+ "log-tail": "bin/log-tail.js"
7
+ },
8
+ "keywords": [
9
+ "tail",
10
+ "log",
11
+ "logs",
12
+ "cli",
13
+ "follow",
14
+ "filter",
15
+ "developer-tools",
16
+ "cross-platform",
17
+ "zero-dependencies",
18
+ "tail -f windows",
19
+ "tail follow",
20
+ "log viewer",
21
+ "follow logs",
22
+ "tail logs",
23
+ "log tail",
24
+ "live logs",
25
+ "json logs",
26
+ "zero dependencies"
27
+ ],
28
+ "author": "Rushabh Shah",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=14"
32
+ },
33
+ "files": [
34
+ "bin/"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Rushabh5000/log-tail.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Rushabh5000/log-tail/issues"
42
+ },
43
+ "homepage": "https://github.com/Rushabh5000/log-tail#readme",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }