tinylogs 0.1.0 → 0.3.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/README.md +21 -1
- package/dist/cli.js +384 -49
- package/dist/cli.js.map +4 -4
- package/dist/server/index.js +38 -30
- package/dist/server/index.js.map +2 -2
- package/package.json +8 -13
- package/dist/client/index.d.ts +0 -30
- package/dist/client/index.js +0 -65
- package/dist/client/index.js.map +0 -7
- package/dist/src/types.d.ts +0 -17
package/README.md
CHANGED
|
@@ -19,6 +19,24 @@ Non-interactive init (CI/containers):
|
|
|
19
19
|
TINYLOGS_PASSWORD=hunter2 npx tinylogs init --yes --port 4700
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Install & run as a service
|
|
23
|
+
|
|
24
|
+
Install globally so `tinylogs` is on your PATH (recommended for a long-running server):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i -g tinylogs # puts `tinylogs` on PATH
|
|
28
|
+
tinylogs init # create tinylogs.config.json + tinylogs.db in the current dir
|
|
29
|
+
tinylogs start -d # run in the background (writes tinylogs.pid + tinylogs.log)
|
|
30
|
+
tinylogs status # running? pid, url, uptime, version
|
|
31
|
+
tinylogs stop # stop it
|
|
32
|
+
tinylogs restart # stop + start -d
|
|
33
|
+
tinylogs update # update to the latest published version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`npx tinylogs …` still works for one-off use. `start -d`, `status`, and `stop` are supported on
|
|
37
|
+
Linux/macOS; on Windows run the foreground `tinylogs start` under a service manager (NSSM / Task
|
|
38
|
+
Scheduler). The PID and log files live next to your config file.
|
|
39
|
+
|
|
22
40
|
## Sending logs
|
|
23
41
|
|
|
24
42
|
Anything that can make an HTTP request can send logs. `service` and `message` are
|
|
@@ -37,8 +55,10 @@ missing, 401 if the token is wrong).
|
|
|
37
55
|
|
|
38
56
|
### Node client
|
|
39
57
|
|
|
58
|
+
Install the standalone, zero-dependency client package [`@tinylogs/client`](packages/client) (`npm install @tinylogs/client`):
|
|
59
|
+
|
|
40
60
|
```ts
|
|
41
|
-
import { TinyLogsClient } from 'tinylogs/client';
|
|
61
|
+
import { TinyLogsClient } from '@tinylogs/client';
|
|
42
62
|
|
|
43
63
|
const logs = new TinyLogsClient({
|
|
44
64
|
url: 'http://localhost:4700',
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -46,7 +46,7 @@ function resolveDbPath(configPath, cfg) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// src/storage/db.ts
|
|
49
|
-
import
|
|
49
|
+
import { DatabaseSync } from "node:sqlite";
|
|
50
50
|
var MIGRATIONS = [
|
|
51
51
|
`CREATE TABLE logs (
|
|
52
52
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -72,34 +72,43 @@ var MIGRATIONS = [
|
|
|
72
72
|
created_at INTEGER NOT NULL
|
|
73
73
|
);`
|
|
74
74
|
];
|
|
75
|
+
function tx(db, fn) {
|
|
76
|
+
db.exec("BEGIN");
|
|
77
|
+
try {
|
|
78
|
+
const result = fn();
|
|
79
|
+
db.exec("COMMIT");
|
|
80
|
+
return result;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
db.exec("ROLLBACK");
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
function openDb(path) {
|
|
76
|
-
const db = new
|
|
77
|
-
db.
|
|
78
|
-
db.
|
|
87
|
+
const db = new DatabaseSync(path);
|
|
88
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
89
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
79
90
|
db.exec("CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)");
|
|
80
91
|
const current = db.prepare("SELECT COALESCE(MAX(version),0) v FROM schema_migrations").get().v;
|
|
81
|
-
|
|
82
|
-
for (let i =
|
|
92
|
+
tx(db, () => {
|
|
93
|
+
for (let i = current; i < MIGRATIONS.length; i++) {
|
|
83
94
|
db.exec(MIGRATIONS[i]);
|
|
84
95
|
db.prepare("INSERT INTO schema_migrations (version) VALUES (?)").run(i + 1);
|
|
85
96
|
}
|
|
86
97
|
});
|
|
87
|
-
apply(current);
|
|
88
98
|
return db;
|
|
89
99
|
}
|
|
90
100
|
function insertLog(db, rec) {
|
|
91
|
-
|
|
92
|
-
const info = db.prepare("INSERT INTO logs (ts, service, message, labels) VALUES (?,?,?,?)").run(
|
|
101
|
+
return tx(db, () => {
|
|
102
|
+
const info = db.prepare("INSERT INTO logs (ts, service, message, labels) VALUES (?,?,?,?)").run(rec.ts, rec.service, rec.message, JSON.stringify(rec.labels ?? {}));
|
|
93
103
|
const logId = Number(info.lastInsertRowid);
|
|
94
104
|
const ins = db.prepare("INSERT INTO log_labels (log_id, key, value) VALUES (?,?,?)");
|
|
95
|
-
for (const [k, v] of Object.entries(
|
|
105
|
+
for (const [k, v] of Object.entries(rec.labels ?? {})) ins.run(logId, k, String(v));
|
|
96
106
|
return logId;
|
|
97
107
|
});
|
|
98
|
-
return txn(rec);
|
|
99
108
|
}
|
|
100
109
|
function dbSizeBytes(db) {
|
|
101
|
-
const pageCount = db.
|
|
102
|
-
const pageSize = db.
|
|
110
|
+
const pageCount = Number(db.prepare("PRAGMA page_count").get().page_count);
|
|
111
|
+
const pageSize = Number(db.prepare("PRAGMA page_size").get().page_size);
|
|
103
112
|
return pageCount * pageSize;
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -150,6 +159,21 @@ function verifySession(cookieVal, secret) {
|
|
|
150
159
|
return username;
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
// src/style.ts
|
|
163
|
+
function computeSupportsColor() {
|
|
164
|
+
if (process.env.NO_COLOR) return false;
|
|
165
|
+
if (process.env.FORCE_COLOR) return true;
|
|
166
|
+
return Boolean(process.stdout.isTTY);
|
|
167
|
+
}
|
|
168
|
+
var useColor = computeSupportsColor();
|
|
169
|
+
var wrap = (open, close) => (s) => useColor ? `\x1B[${open}m${s}\x1B[${close}m` : s;
|
|
170
|
+
var bold = wrap(1, 22);
|
|
171
|
+
var dim = wrap(2, 22);
|
|
172
|
+
var red = wrap(31, 39);
|
|
173
|
+
var green = wrap(32, 39);
|
|
174
|
+
var yellow = wrap(33, 39);
|
|
175
|
+
var cyan = wrap(36, 39);
|
|
176
|
+
|
|
153
177
|
// src/wizard.ts
|
|
154
178
|
function buildInitConfig(opts) {
|
|
155
179
|
const s = generateSecrets();
|
|
@@ -166,13 +190,13 @@ function buildInitConfig(opts) {
|
|
|
166
190
|
return { config, token: s.token };
|
|
167
191
|
}
|
|
168
192
|
async function runInit(configPath, opts, io) {
|
|
169
|
-
const port = opts.port ?? (Number(await io.prompt(`Port [${DEFAULTS.port}]: `)) || DEFAULTS.port);
|
|
170
|
-
const host = opts.host ?? (await io.prompt(`Host [${DEFAULTS.host}]: `) || DEFAULTS.host);
|
|
171
|
-
const username = opts.username ?? (await io.prompt(
|
|
193
|
+
const port = opts.port ?? (Number(await io.prompt(`Port ${dim(`[${DEFAULTS.port}]`)}: `)) || DEFAULTS.port);
|
|
194
|
+
const host = opts.host ?? (await io.prompt(`Host ${dim(`[${DEFAULTS.host}]`)}: `) || DEFAULTS.host);
|
|
195
|
+
const username = opts.username ?? (await io.prompt(`Admin username ${dim("[admin]")}: `) || "admin");
|
|
172
196
|
const password = opts.password ?? await io.promptHidden("Admin password: ");
|
|
173
197
|
if (!password || password.length === 0) throw new Error("password must not be empty");
|
|
174
|
-
const retentionDays = opts.retentionDays ?? (Number(await io.prompt(`Retention days [${DEFAULTS.retentionDays}]: `)) || DEFAULTS.retentionDays);
|
|
175
|
-
const maxSizeMB = opts.maxSizeMB ?? (Number(await io.prompt(`Max DB size MB [${DEFAULTS.maxSizeMB}]: `)) || DEFAULTS.maxSizeMB);
|
|
198
|
+
const retentionDays = opts.retentionDays ?? (Number(await io.prompt(`Retention days ${dim(`[${DEFAULTS.retentionDays}]`)}: `)) || DEFAULTS.retentionDays);
|
|
199
|
+
const maxSizeMB = opts.maxSizeMB ?? (Number(await io.prompt(`Max DB size MB ${dim(`[${DEFAULTS.maxSizeMB}]`)}: `)) || DEFAULTS.maxSizeMB);
|
|
176
200
|
const dbPath = opts.dbPath ?? DEFAULTS.dbPath;
|
|
177
201
|
const { config, token } = buildInitConfig({ port, host, retentionDays, maxSizeMB, dbPath });
|
|
178
202
|
const db = openDb(resolveDbPath(configPath, config));
|
|
@@ -180,11 +204,13 @@ async function runInit(configPath, opts, io) {
|
|
|
180
204
|
db.close();
|
|
181
205
|
saveConfig(configPath, config);
|
|
182
206
|
io.log("");
|
|
183
|
-
io.log(` tinylogs configured \u2192 ${configPath}`);
|
|
184
|
-
io.log(
|
|
185
|
-
io.log(`
|
|
207
|
+
io.log(` ${green("\u2713")} tinylogs configured \u2192 ${dim(configPath)}`);
|
|
208
|
+
io.log("");
|
|
209
|
+
io.log(` ${yellow("\u26A0")} Ingest token \u2014 shown once, store it now:`);
|
|
210
|
+
io.log("");
|
|
211
|
+
io.log(` ${bold(cyan(token))}`);
|
|
186
212
|
io.log("");
|
|
187
|
-
io.log(` Start with:
|
|
213
|
+
io.log(` ${dim("Start with:")} ${bold("tinylogs start -d")}`);
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
// src/server/index.ts
|
|
@@ -193,15 +219,15 @@ import { createServer } from "node:http";
|
|
|
193
219
|
// src/storage/retention.ts
|
|
194
220
|
function pruneByAge(db, retentionDays, now) {
|
|
195
221
|
const cutoff = now - retentionDays * 864e5;
|
|
196
|
-
return db.prepare("DELETE FROM logs WHERE ts < ?").run(cutoff).changes;
|
|
222
|
+
return Number(db.prepare("DELETE FROM logs WHERE ts < ?").run(cutoff).changes);
|
|
197
223
|
}
|
|
198
224
|
function pruneBySize(db, maxSizeMB, batch = 1e3) {
|
|
199
225
|
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
200
226
|
let deleted = 0;
|
|
201
227
|
while (dbSizeBytes(db) > maxBytes) {
|
|
202
|
-
const changes = db.prepare(
|
|
228
|
+
const changes = Number(db.prepare(
|
|
203
229
|
"DELETE FROM logs WHERE id IN (SELECT id FROM logs ORDER BY id ASC LIMIT ?)"
|
|
204
|
-
).run(batch).changes;
|
|
230
|
+
).run(batch).changes);
|
|
205
231
|
if (changes === 0) break;
|
|
206
232
|
deleted += changes;
|
|
207
233
|
}
|
|
@@ -339,23 +365,21 @@ function queryLogs(db, params) {
|
|
|
339
365
|
ORDER BY logs.id DESC LIMIT ?`;
|
|
340
366
|
return db.prepare(sql).all(...args, limit).map(rowToRecord);
|
|
341
367
|
}
|
|
368
|
+
function rowToLabel(row) {
|
|
369
|
+
return { key: row.key, value: row.value, count: Number(row.count) };
|
|
370
|
+
}
|
|
342
371
|
function queryLabels(db, opts = {}) {
|
|
343
372
|
const services = db.prepare(
|
|
344
373
|
"SELECT service, COUNT(*) count FROM logs GROUP BY service ORDER BY count DESC"
|
|
345
|
-
).all();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
labels = db.prepare(
|
|
355
|
-
`SELECT key, value, COUNT(*) count FROM log_labels
|
|
356
|
-
GROUP BY key, value ORDER BY key, count DESC`
|
|
357
|
-
).all();
|
|
358
|
-
}
|
|
374
|
+
).all().map((row) => ({ service: row.service, count: Number(row.count) }));
|
|
375
|
+
const labels = opts.service ? db.prepare(
|
|
376
|
+
`SELECT ll.key, ll.value, COUNT(*) count FROM log_labels ll
|
|
377
|
+
JOIN logs ON logs.id = ll.log_id WHERE logs.service = ?
|
|
378
|
+
GROUP BY ll.key, ll.value ORDER BY ll.key, count DESC`
|
|
379
|
+
).all(opts.service).map(rowToLabel) : db.prepare(
|
|
380
|
+
`SELECT key, value, COUNT(*) count FROM log_labels
|
|
381
|
+
GROUP BY key, value ORDER BY key, count DESC`
|
|
382
|
+
).all().map(rowToLabel);
|
|
359
383
|
return { services, labels };
|
|
360
384
|
}
|
|
361
385
|
|
|
@@ -519,13 +543,13 @@ async function start(configPath = resolveConfigPath()) {
|
|
|
519
543
|
6e4
|
|
520
544
|
);
|
|
521
545
|
retentionTimer.unref();
|
|
522
|
-
await new Promise((
|
|
546
|
+
await new Promise((resolve3) => server.listen(cfg.port, cfg.host, resolve3));
|
|
523
547
|
const addr = server.address();
|
|
524
548
|
const port = typeof addr === "object" && addr ? addr.port : cfg.port;
|
|
525
549
|
console.log(`[tinylogs] listening on http://${cfg.host}:${port}`);
|
|
526
550
|
return {
|
|
527
551
|
port,
|
|
528
|
-
close: () => new Promise((
|
|
552
|
+
close: () => new Promise((resolve3) => {
|
|
529
553
|
clearInterval(retentionTimer);
|
|
530
554
|
for (const c of wsClients) {
|
|
531
555
|
try {
|
|
@@ -535,12 +559,188 @@ async function start(configPath = resolveConfigPath()) {
|
|
|
535
559
|
}
|
|
536
560
|
server.close(() => {
|
|
537
561
|
db.close();
|
|
538
|
-
|
|
562
|
+
resolve3();
|
|
539
563
|
});
|
|
564
|
+
server.closeIdleConnections?.();
|
|
540
565
|
})
|
|
541
566
|
};
|
|
542
567
|
}
|
|
543
568
|
|
|
569
|
+
// src/daemon.ts
|
|
570
|
+
import { spawn } from "node:child_process";
|
|
571
|
+
import {
|
|
572
|
+
openSync,
|
|
573
|
+
readFileSync as readFileSync2,
|
|
574
|
+
writeFileSync as writeFileSync2,
|
|
575
|
+
existsSync as existsSync2,
|
|
576
|
+
unlinkSync,
|
|
577
|
+
statSync
|
|
578
|
+
} from "node:fs";
|
|
579
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "node:path";
|
|
580
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
581
|
+
function pidPath(configPath) {
|
|
582
|
+
return join3(dirname3(resolve2(configPath)), "tinylogs.pid");
|
|
583
|
+
}
|
|
584
|
+
function logPath(configPath) {
|
|
585
|
+
return join3(dirname3(resolve2(configPath)), "tinylogs.log");
|
|
586
|
+
}
|
|
587
|
+
function readPid(configPath) {
|
|
588
|
+
const p = pidPath(configPath);
|
|
589
|
+
if (!existsSync2(p)) return null;
|
|
590
|
+
const n = Number(readFileSync2(p, "utf8").trim());
|
|
591
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
592
|
+
}
|
|
593
|
+
function isAlive(pid) {
|
|
594
|
+
try {
|
|
595
|
+
process.kill(pid, 0);
|
|
596
|
+
return true;
|
|
597
|
+
} catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function statusInfo(configPath) {
|
|
602
|
+
const cfg = loadConfig(configPath);
|
|
603
|
+
const pid = readPid(configPath);
|
|
604
|
+
if (pid && isAlive(pid)) {
|
|
605
|
+
let uptimeMs = null;
|
|
606
|
+
try {
|
|
607
|
+
uptimeMs = Date.now() - statSync(pidPath(configPath)).mtimeMs;
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
return { running: true, stale: false, pid, port: cfg.port, host: cfg.host, uptimeMs };
|
|
611
|
+
}
|
|
612
|
+
if (pid) {
|
|
613
|
+
try {
|
|
614
|
+
unlinkSync(pidPath(configPath));
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
running: false,
|
|
620
|
+
stale: pid !== null,
|
|
621
|
+
pid: null,
|
|
622
|
+
port: cfg.port,
|
|
623
|
+
host: cfg.host,
|
|
624
|
+
uptimeMs: null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
628
|
+
async function spawnDaemon(configPath) {
|
|
629
|
+
const existing = readPid(configPath);
|
|
630
|
+
if (existing && isAlive(existing)) {
|
|
631
|
+
throw new Error(`already running (pid ${existing})`);
|
|
632
|
+
}
|
|
633
|
+
if (existing) {
|
|
634
|
+
try {
|
|
635
|
+
unlinkSync(pidPath(configPath));
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const cfg = loadConfig(configPath);
|
|
640
|
+
const logFile = logPath(configPath);
|
|
641
|
+
const fd = openSync(logFile, "a");
|
|
642
|
+
const cliEntry = fileURLToPath2(import.meta.url);
|
|
643
|
+
const child = spawn(
|
|
644
|
+
process.execPath,
|
|
645
|
+
[cliEntry, "start", "--config", resolve2(configPath)],
|
|
646
|
+
{ detached: true, stdio: ["ignore", fd, fd] }
|
|
647
|
+
);
|
|
648
|
+
child.unref();
|
|
649
|
+
const pid = child.pid;
|
|
650
|
+
if (!pid) throw new Error("failed to spawn daemon process");
|
|
651
|
+
writeFileSync2(pidPath(configPath), String(pid) + "\n");
|
|
652
|
+
await delay(400);
|
|
653
|
+
if (!isAlive(pid)) {
|
|
654
|
+
let tail = "";
|
|
655
|
+
try {
|
|
656
|
+
tail = readFileSync2(logFile, "utf8").split("\n").slice(-8).join("\n");
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
unlinkSync(pidPath(configPath));
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
throw new Error(`daemon exited immediately. Recent log:
|
|
664
|
+
${tail}`);
|
|
665
|
+
}
|
|
666
|
+
return { pid, port: cfg.port, logFile };
|
|
667
|
+
}
|
|
668
|
+
async function stopDaemon(configPath) {
|
|
669
|
+
const pid = readPid(configPath);
|
|
670
|
+
if (!pid || !isAlive(pid)) {
|
|
671
|
+
if (pid) {
|
|
672
|
+
try {
|
|
673
|
+
unlinkSync(pidPath(configPath));
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return "not-running";
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
process.kill(pid, "SIGTERM");
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
for (let i = 0; i < 50; i++) {
|
|
684
|
+
if (!isAlive(pid)) {
|
|
685
|
+
try {
|
|
686
|
+
unlinkSync(pidPath(configPath));
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
return "stopped";
|
|
690
|
+
}
|
|
691
|
+
await delay(100);
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
process.kill(pid, "SIGKILL");
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
unlinkSync(pidPath(configPath));
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
return "killed";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/update.ts
|
|
705
|
+
import { spawnSync } from "node:child_process";
|
|
706
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
707
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
708
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
709
|
+
function pkgRoot() {
|
|
710
|
+
return dirname4(dirname4(fileURLToPath3(import.meta.url)));
|
|
711
|
+
}
|
|
712
|
+
function currentVersion() {
|
|
713
|
+
try {
|
|
714
|
+
const pkg = JSON.parse(readFileSync3(join4(pkgRoot(), "package.json"), "utf8"));
|
|
715
|
+
return pkg.version ?? "0.0.0";
|
|
716
|
+
} catch {
|
|
717
|
+
return "0.0.0";
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function fetchLatest(timeoutMs = 2500) {
|
|
721
|
+
const ctrl = new AbortController();
|
|
722
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
723
|
+
try {
|
|
724
|
+
const res = await fetch("https://registry.npmjs.org/tinylogs", { signal: ctrl.signal });
|
|
725
|
+
if (!res.ok) return null;
|
|
726
|
+
const data = await res.json();
|
|
727
|
+
return data["dist-tags"]?.latest ?? null;
|
|
728
|
+
} catch {
|
|
729
|
+
return null;
|
|
730
|
+
} finally {
|
|
731
|
+
clearTimeout(t);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function detectInstall() {
|
|
735
|
+
const root = pkgRoot();
|
|
736
|
+
if (existsSync3(join4(root, ".git")) && existsSync3(join4(root, "src"))) return "source";
|
|
737
|
+
return "global";
|
|
738
|
+
}
|
|
739
|
+
function runNpmUpdate() {
|
|
740
|
+
const r = spawnSync("npm", ["i", "-g", "tinylogs@latest"], { stdio: "inherit" });
|
|
741
|
+
return r.status ?? 1;
|
|
742
|
+
}
|
|
743
|
+
|
|
544
744
|
// src/cli.ts
|
|
545
745
|
function parseFlags(argv) {
|
|
546
746
|
const out = {};
|
|
@@ -569,6 +769,17 @@ function makeIo() {
|
|
|
569
769
|
done: () => rl.close()
|
|
570
770
|
};
|
|
571
771
|
}
|
|
772
|
+
function formatUptime(ms) {
|
|
773
|
+
if (ms == null) return "\u2014";
|
|
774
|
+
const s = Math.floor(ms / 1e3);
|
|
775
|
+
const d = Math.floor(s / 86400);
|
|
776
|
+
const h = Math.floor(s % 86400 / 3600);
|
|
777
|
+
const m = Math.floor(s % 3600 / 60);
|
|
778
|
+
if (d) return `${d}d ${h}h`;
|
|
779
|
+
if (h) return `${h}h ${m}m`;
|
|
780
|
+
if (m) return `${m}m`;
|
|
781
|
+
return `${s}s`;
|
|
782
|
+
}
|
|
572
783
|
async function main() {
|
|
573
784
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
574
785
|
const flags = parseFlags(rest);
|
|
@@ -584,7 +795,7 @@ async function main() {
|
|
|
584
795
|
maxSizeMB: flags["max-size-mb"] ? Number(flags["max-size-mb"]) : void 0,
|
|
585
796
|
dbPath: typeof flags["db-path"] === "string" ? flags["db-path"] : void 0
|
|
586
797
|
};
|
|
587
|
-
if (
|
|
798
|
+
if (existsSync4(configPath) && !flags.force) {
|
|
588
799
|
console.error(`Config already exists at ${configPath} (use --force to overwrite).`);
|
|
589
800
|
process.exit(1);
|
|
590
801
|
}
|
|
@@ -605,15 +816,127 @@ async function main() {
|
|
|
605
816
|
return;
|
|
606
817
|
}
|
|
607
818
|
if (cmd === "start") {
|
|
608
|
-
if (!
|
|
609
|
-
console.error(`No config at ${configPath}. Run:
|
|
819
|
+
if (!existsSync4(configPath)) {
|
|
820
|
+
console.error(`No config at ${configPath}. Run: tinylogs init`);
|
|
610
821
|
process.exit(1);
|
|
611
822
|
}
|
|
612
|
-
|
|
823
|
+
const daemon = flags.daemon === true || rest.includes("-d");
|
|
824
|
+
if (daemon) {
|
|
825
|
+
try {
|
|
826
|
+
const { pid, port, logFile } = await spawnDaemon(configPath);
|
|
827
|
+
console.log(`${green("\u2713")} tinylogs started in background`);
|
|
828
|
+
console.log(` ${dim("pid")} ${pid}`);
|
|
829
|
+
console.log(` ${dim("url")} http://127.0.0.1:${port}`);
|
|
830
|
+
console.log(` ${dim("logs")} ${logFile}`);
|
|
831
|
+
console.log(` ${dim("stop")} tinylogs stop`);
|
|
832
|
+
} catch (e) {
|
|
833
|
+
console.error(`${red("\u2717")} ${e.message}`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const srv = await start(configPath);
|
|
839
|
+
const shutdown = async () => {
|
|
840
|
+
try {
|
|
841
|
+
await srv.close();
|
|
842
|
+
} finally {
|
|
843
|
+
process.exit(0);
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
process.on("SIGTERM", shutdown);
|
|
847
|
+
process.on("SIGINT", shutdown);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (cmd === "status") {
|
|
851
|
+
if (!existsSync4(configPath)) {
|
|
852
|
+
console.error(`No config at ${configPath}. Run: tinylogs init`);
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
const s = statusInfo(configPath);
|
|
856
|
+
if (s.running) {
|
|
857
|
+
console.log(`${green("\u25CF")} running`);
|
|
858
|
+
console.log(` ${dim("pid")} ${s.pid}`);
|
|
859
|
+
const shownHost = s.host === "0.0.0.0" || s.host === "::" ? "127.0.0.1" : s.host;
|
|
860
|
+
console.log(` ${dim("url")} http://${shownHost}:${s.port}`);
|
|
861
|
+
console.log(` ${dim("uptime")} ${formatUptime(s.uptimeMs)}`);
|
|
862
|
+
} else {
|
|
863
|
+
console.log(`${dim("\u25CB")} stopped${s.stale ? " " + yellow("(cleaned stale pidfile)") : ""}`);
|
|
864
|
+
}
|
|
865
|
+
console.log(` ${dim("version")} ${currentVersion()}`);
|
|
866
|
+
const latest = await fetchLatest();
|
|
867
|
+
if (latest && latest !== currentVersion()) {
|
|
868
|
+
console.log(` ${yellow("\u26A0")} update available: ${latest} \u2014 run: tinylogs update`);
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (cmd === "stop") {
|
|
873
|
+
if (!existsSync4(configPath)) {
|
|
874
|
+
console.error(`No config at ${configPath}.`);
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
const r = await stopDaemon(configPath);
|
|
878
|
+
if (r === "not-running") console.log(`${dim("\u25CB")} not running`);
|
|
879
|
+
else if (r === "stopped") console.log(`${green("\u2713")} stopped`);
|
|
880
|
+
else console.log(`${yellow("\u26A0")} force-killed (did not stop gracefully within 5s)`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (cmd === "restart") {
|
|
884
|
+
if (!existsSync4(configPath)) {
|
|
885
|
+
console.error(`No config at ${configPath}. Run: tinylogs init`);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
await stopDaemon(configPath);
|
|
889
|
+
try {
|
|
890
|
+
const { pid, port, logFile } = await spawnDaemon(configPath);
|
|
891
|
+
console.log(`${green("\u2713")} tinylogs restarted`);
|
|
892
|
+
console.log(` ${dim("pid")} ${pid}`);
|
|
893
|
+
console.log(` ${dim("url")} http://127.0.0.1:${port}`);
|
|
894
|
+
console.log(` ${dim("logs")} ${logFile}`);
|
|
895
|
+
} catch (e) {
|
|
896
|
+
console.error(`${red("\u2717")} ${e.message}`);
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
902
|
+
console.log(`tinylogs ${currentVersion()}`);
|
|
903
|
+
const latest = await fetchLatest();
|
|
904
|
+
if (latest && latest !== currentVersion()) {
|
|
905
|
+
console.log(`${yellow("\u26A0")} update available: ${latest} \u2014 run: tinylogs update`);
|
|
906
|
+
} else if (latest) {
|
|
907
|
+
console.log(dim("up to date"));
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (cmd === "update") {
|
|
912
|
+
const cur = currentVersion();
|
|
913
|
+
const latest = await fetchLatest();
|
|
914
|
+
console.log(` ${dim("current")} ${cur}`);
|
|
915
|
+
if (latest) console.log(` ${dim("latest")} ${latest}`);
|
|
916
|
+
if (latest && latest === cur) {
|
|
917
|
+
console.log(`${green("\u2713")} already up to date`);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (detectInstall() === "source") {
|
|
921
|
+
console.log(`${yellow("\u26A0")} running from a source checkout \u2014 update with:`);
|
|
922
|
+
console.log(` git pull && npm install && npm run build`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
console.log(dim("\u2192 npm i -g tinylogs@latest"));
|
|
926
|
+
const code = runNpmUpdate();
|
|
927
|
+
if (code !== 0) {
|
|
928
|
+
console.error(`${red("\u2717")} npm update failed`);
|
|
929
|
+
process.exit(code);
|
|
930
|
+
}
|
|
931
|
+
console.log(`${green("\u2713")} updated to ${latest ?? "latest"}`);
|
|
932
|
+
if (existsSync4(configPath)) {
|
|
933
|
+
const s = statusInfo(configPath);
|
|
934
|
+
if (s.running) console.log(`${yellow("\u26A0")} daemon is running \u2014 run: tinylogs restart`);
|
|
935
|
+
}
|
|
613
936
|
return;
|
|
614
937
|
}
|
|
615
938
|
if (cmd === "rotate-token") {
|
|
616
|
-
if (!
|
|
939
|
+
if (!existsSync4(configPath)) {
|
|
617
940
|
console.error(`No config at ${configPath}.`);
|
|
618
941
|
process.exit(1);
|
|
619
942
|
}
|
|
@@ -625,7 +948,19 @@ async function main() {
|
|
|
625
948
|
console.log(` ${s.token}`);
|
|
626
949
|
return;
|
|
627
950
|
}
|
|
628
|
-
console.log(
|
|
951
|
+
console.log(
|
|
952
|
+
[
|
|
953
|
+
"tinylogs \u2014 usage:",
|
|
954
|
+
" tinylogs init [--yes --password ...]",
|
|
955
|
+
" tinylogs start [-d|--daemon] start (foreground, or background with -d)",
|
|
956
|
+
" tinylogs status is it running? pid, url, uptime, version",
|
|
957
|
+
" tinylogs stop stop the background server",
|
|
958
|
+
" tinylogs restart stop + start -d",
|
|
959
|
+
" tinylogs update update to the latest published version",
|
|
960
|
+
" tinylogs version show version (and check for updates)",
|
|
961
|
+
" tinylogs rotate-token issue a new ingest token"
|
|
962
|
+
].join("\n")
|
|
963
|
+
);
|
|
629
964
|
if (cmd && cmd !== "help") process.exit(1);
|
|
630
965
|
}
|
|
631
966
|
main().catch((err) => {
|