tokmon 0.19.7 → 0.20.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.
@@ -1,564 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- PROVIDERS,
4
- buildAccounts,
5
- cacheDir,
6
- detectProviders,
7
- resolveTimezone
8
- } from "./chunk-UAPL47GL.js";
9
-
10
- // src/web/server.ts
11
- import { createServer } from "http";
12
-
13
- // src/web/colors.ts
14
- var NAMED_HEX = {
15
- green: "#6caa71",
16
- greenBright: "#79be7e",
17
- cyan: "#7ccbcd",
18
- cyanBright: "#84dde0",
19
- blue: "#6d96b4",
20
- blueBright: "#67b5ed",
21
- magenta: "#bd7bcd",
22
- magentaBright: "#d389e5",
23
- yellow: "#c4ac62",
24
- yellowBright: "#d9c074",
25
- red: "#b45648",
26
- redBright: "#cf6a5a",
27
- white: "#dee5eb",
28
- whiteBright: "#f3f5f5",
29
- gray: "#8d9090",
30
- grey: "#8d9090"
31
- };
32
- var FALLBACK = "#8d9090";
33
- function namedHex(name) {
34
- if (!name) return FALLBACK;
35
- if (name.startsWith("#")) return name;
36
- return NAMED_HEX[name] ?? FALLBACK;
37
- }
38
- function colorHex(accountColor, providerColorName) {
39
- if (accountColor) {
40
- if (accountColor.startsWith("#")) return accountColor;
41
- const named = NAMED_HEX[accountColor];
42
- if (named) return named;
43
- }
44
- return namedHex(providerColorName);
45
- }
46
-
47
- // src/web/data.ts
48
- async function resolveAccounts(config) {
49
- const detected = await detectProviders();
50
- const accounts = buildAccounts({ ...config, disabledProviders: [] }, detected);
51
- return accounts.map((a) => {
52
- const p = PROVIDERS[a.providerId];
53
- return {
54
- account: a,
55
- hasUsage: p.hasUsage || !!p.fetchTable,
56
- hasBilling: p.hasBilling,
57
- color: colorHex(a.color, PROVIDERS[a.providerId].color)
58
- };
59
- });
60
- }
61
- async function fetchAccountSummary(account, tz) {
62
- const p = PROVIDERS[account.providerId];
63
- if (!p.fetchSummary) return null;
64
- return p.fetchSummary(account, tz).catch(() => null);
65
- }
66
- async function fetchAccountTable(account, tz) {
67
- const p = PROVIDERS[account.providerId];
68
- if (!p.fetchTable) return null;
69
- return p.fetchTable(account, tz).catch(() => null);
70
- }
71
- async function fetchAccountBilling(account) {
72
- const p = PROVIDERS[account.providerId];
73
- if (!p.fetchBilling) return null;
74
- return p.fetchBilling(account).catch(() => null);
75
- }
76
- function assembleSnapshot(opts) {
77
- const accounts = opts.resolved.map((r) => {
78
- const u = opts.usage.get(r.account.id);
79
- return {
80
- id: r.account.id,
81
- providerId: r.account.providerId,
82
- name: r.account.name,
83
- color: r.color,
84
- hasUsage: r.hasUsage,
85
- hasBilling: r.hasBilling,
86
- dashboard: u?.dashboard ?? null,
87
- table: u?.table ?? null,
88
- billing: opts.billing.get(r.account.id) ?? null
89
- };
90
- });
91
- const seen = /* @__PURE__ */ new Set();
92
- const providers = [];
93
- for (const r of opts.resolved) {
94
- if (seen.has(r.account.providerId)) continue;
95
- seen.add(r.account.providerId);
96
- providers.push({
97
- id: r.account.providerId,
98
- name: PROVIDERS[r.account.providerId].name,
99
- color: namedHex(PROVIDERS[r.account.providerId].color)
100
- });
101
- }
102
- return {
103
- version: opts.version,
104
- generatedAt: Date.now(),
105
- tz: opts.tz,
106
- intervalMs: opts.intervalMs,
107
- providers,
108
- accounts
109
- };
110
- }
111
- function tzFor(config) {
112
- return resolveTimezone(config.timezone);
113
- }
114
-
115
- // src/web/static.ts
116
- import { createReadStream, existsSync, readFileSync } from "fs";
117
- import { stat } from "fs/promises";
118
- import { fileURLToPath } from "url";
119
- import { extname, join, normalize, sep } from "path";
120
- var MIME = {
121
- ".html": "text/html; charset=utf-8",
122
- ".js": "text/javascript; charset=utf-8",
123
- ".mjs": "text/javascript; charset=utf-8",
124
- ".css": "text/css; charset=utf-8",
125
- ".json": "application/json; charset=utf-8",
126
- ".svg": "image/svg+xml",
127
- ".png": "image/png",
128
- ".jpg": "image/jpeg",
129
- ".jpeg": "image/jpeg",
130
- ".gif": "image/gif",
131
- ".ico": "image/x-icon",
132
- ".woff": "font/woff",
133
- ".woff2": "font/woff2",
134
- ".ttf": "font/ttf",
135
- ".otf": "font/otf",
136
- ".map": "application/json; charset=utf-8",
137
- ".webmanifest": "application/manifest+json"
138
- };
139
- function findWebRoot() {
140
- const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
141
- for (const rel of candidates) {
142
- try {
143
- const dir = fileURLToPath(new URL(rel, import.meta.url));
144
- if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
145
- } catch {
146
- }
147
- }
148
- return null;
149
- }
150
- function resolveStaticPath(webRoot, urlPath) {
151
- let clean;
152
- try {
153
- clean = decodeURIComponent(urlPath.split("?")[0]);
154
- } catch {
155
- return null;
156
- }
157
- const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
158
- const full = join(webRoot, rel);
159
- if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
160
- return full;
161
- }
162
- function send(res, status, type, body) {
163
- res.writeHead(status, { "Content-Type": type, "Cache-Control": "no-store" });
164
- res.end(body);
165
- }
166
- function sendJson(res, status, data) {
167
- send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
168
- }
169
- function serveStatic(webRoot, urlPath, res) {
170
- const path = urlPath.split("?")[0];
171
- const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
172
- if (!filePath) {
173
- send(res, 403, "text/plain", "forbidden");
174
- return;
175
- }
176
- void stat(filePath).then((st) => {
177
- if (st.isFile()) {
178
- const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
179
- const immutable = filePath.includes(`${sep}assets${sep}`);
180
- res.writeHead(200, {
181
- "Content-Type": type,
182
- "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache"
183
- });
184
- createReadStream(filePath).pipe(res);
185
- } else {
186
- throw new Error("not a file");
187
- }
188
- }).catch(() => {
189
- if (extname(path)) {
190
- send(res, 404, "text/plain", "not found");
191
- return;
192
- }
193
- const indexPath = join(webRoot, "index.html");
194
- if (!existsSync(indexPath)) {
195
- send(res, 404, "text/plain", "not found");
196
- return;
197
- }
198
- res.writeHead(200, { "Content-Type": MIME[".html"], "Cache-Control": "no-cache" });
199
- createReadStream(indexPath).on("error", () => {
200
- try {
201
- res.destroy();
202
- } catch {
203
- }
204
- }).pipe(res);
205
- });
206
- }
207
- function appVersion() {
208
- for (const rel of ["../package.json", "../../package.json"]) {
209
- try {
210
- const p = fileURLToPath(new URL(rel, import.meta.url));
211
- const pkg = JSON.parse(readFileSync(p, "utf-8"));
212
- if (typeof pkg.version === "string") return pkg.version;
213
- } catch {
214
- }
215
- }
216
- return "";
217
- }
218
-
219
- // src/web/vite-dev.ts
220
- import { existsSync as existsSync2 } from "fs";
221
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
222
- import { createRequire } from "module";
223
- import { join as join2 } from "path";
224
- function isDevMode() {
225
- const forced = process.env.TOKMON_WEB_MODE;
226
- if (forced === "dev") return true;
227
- if (forced === "prod") return false;
228
- return import.meta.url.includes("/src/");
229
- }
230
- function findWebSource() {
231
- for (const rel of ["../../web/", "../web/", "./web/"]) {
232
- try {
233
- const dir = fileURLToPath2(new URL(rel, import.meta.url));
234
- if (existsSync2(join2(dir, "vite.config.ts")) && existsSync2(join2(dir, "index.html"))) {
235
- return dir.replace(/[\\/]+$/, "");
236
- }
237
- } catch {
238
- }
239
- }
240
- return null;
241
- }
242
- async function createViteDevServer(httpServer, log) {
243
- const root = findWebSource();
244
- if (!root) {
245
- log(" \u26A0 dev mode: web/ source not found");
246
- return null;
247
- }
248
- try {
249
- const req = createRequire(pathToFileURL(join2(root, "package.json")).href);
250
- const vitePath = req.resolve("vite");
251
- const vite = await import(pathToFileURL(vitePath).href);
252
- const dev = await vite.createServer({
253
- root,
254
- configFile: join2(root, "vite.config.ts"),
255
- server: { middlewareMode: true, hmr: { server: httpServer } },
256
- appType: "spa",
257
- clearScreen: false,
258
- logLevel: "warn"
259
- });
260
- log(" \u25C6 dev mode \u2014 Vite HMR attached (edit web/src and it hot-reloads)");
261
- return dev;
262
- } catch (e) {
263
- log(` \u26A0 couldn't start Vite dev server: ${e.message}`);
264
- log(" run `pnpm --prefix web install` (or `npm run web:install`) for HMR, or `npm run build` for a static bundle");
265
- return null;
266
- }
267
- }
268
- var MISSING_BUILD_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>tokmon web</title>
269
- <style>body{background:#0a0d0e;color:#cdd6d8;font:14px ui-monospace,Menlo,monospace;padding:3rem;line-height:1.7}
270
- code{color:#e6b450}</style></head><body>
271
- <h1 style="color:#00d787">tokmon web</h1>
272
- <p>The dashboard isn't available.</p>
273
- <p><b>Prod:</b> run <code>npm run build</code> (builds <code>dist/web</code>), then <code>tokmon serve</code>.</p>
274
- <p><b>Dev:</b> run <code>pnpm --prefix web install</code> so the Vite dev server (HMR) can start.</p>
275
- </body></html>`;
276
-
277
- // src/web/data-engine.ts
278
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
279
- import { join as join3 } from "path";
280
- var TABLE_INTERVAL_MS = 3e5;
281
- var SSE_HEARTBEAT_MS = 25e3;
282
- var IDLE_PAUSE_MS = 6e4;
283
- var SNAPSHOT_CACHE_THROTTLE_MS = 2e4;
284
- var snapshotCacheFile = () => join3(cacheDir(), "web-snapshot.json");
285
- var sseFrame = (s) => `event: snapshot
286
- data: ${JSON.stringify(s)}
287
-
288
- `;
289
- function createDataEngine(opts) {
290
- const { version, tz, summaryIntervalMs, billingIntervalMs, resolved } = opts;
291
- const usage = /* @__PURE__ */ new Map();
292
- const billing = /* @__PURE__ */ new Map();
293
- let current = null;
294
- let currentFrame = null;
295
- const sseClients = /* @__PURE__ */ new Map();
296
- let lastActivity = Date.now();
297
- let stopped = false;
298
- let summaryTimer;
299
- let tableTimer;
300
- let billingTimer;
301
- let lastPersist = 0;
302
- const idle = () => sseClients.size === 0 && Date.now() - lastActivity > IDLE_PAUSE_MS;
303
- const usageEntry = (id) => {
304
- let u = usage.get(id);
305
- if (!u) {
306
- u = { dashboard: null, table: null };
307
- usage.set(id, u);
308
- }
309
- return u;
310
- };
311
- const hydrateFromCache = () => {
312
- try {
313
- const cached = JSON.parse(readFileSync2(snapshotCacheFile(), "utf-8"));
314
- if (!cached || !Array.isArray(cached.accounts)) return;
315
- for (const a of cached.accounts) {
316
- if (a.dashboard || a.table) usage.set(a.id, { dashboard: a.dashboard, table: a.table });
317
- if (a.billing) billing.set(a.id, a.billing);
318
- }
319
- current = assembleSnapshot({ version, tz, intervalMs: summaryIntervalMs, resolved, usage, billing });
320
- currentFrame = sseFrame(current);
321
- } catch {
322
- }
323
- };
324
- const persist = () => {
325
- if (!current) return;
326
- if (!current.accounts.some((a) => a.hasUsage && a.table != null)) return;
327
- if (Date.now() - lastPersist < SNAPSHOT_CACHE_THROTTLE_MS) return;
328
- lastPersist = Date.now();
329
- try {
330
- mkdirSync(cacheDir(), { recursive: true });
331
- writeFileSync(snapshotCacheFile(), JSON.stringify(current));
332
- } catch {
333
- }
334
- };
335
- const rebuild = () => {
336
- if (stopped) return;
337
- current = assembleSnapshot({ version, tz, intervalMs: summaryIntervalMs, resolved, usage, billing });
338
- currentFrame = sseFrame(current);
339
- persist();
340
- if (sseClients.size === 0) return;
341
- for (const res of sseClients.keys()) {
342
- try {
343
- res.write(currentFrame);
344
- } catch {
345
- }
346
- }
347
- };
348
- const usageAccounts = resolved.filter((r) => r.hasUsage);
349
- const billingAccounts = resolved.filter((r) => r.hasBilling);
350
- let summaryBusy = false;
351
- const refreshSummary = async (force = false) => {
352
- if (stopped || summaryBusy || !force && idle()) return;
353
- summaryBusy = true;
354
- try {
355
- for (const r of usageAccounts) {
356
- if (stopped) return;
357
- usageEntry(r.account.id).dashboard = await fetchAccountSummary(r.account, tz);
358
- }
359
- rebuild();
360
- } finally {
361
- summaryBusy = false;
362
- }
363
- };
364
- let tableBusy = false;
365
- const refreshTable = async (force = false) => {
366
- if (stopped || tableBusy || !force && idle()) return;
367
- tableBusy = true;
368
- try {
369
- for (const r of usageAccounts) {
370
- if (stopped) return;
371
- usageEntry(r.account.id).table = await fetchAccountTable(r.account, tz);
372
- }
373
- rebuild();
374
- } finally {
375
- tableBusy = false;
376
- }
377
- };
378
- let billingBusy = false;
379
- const refreshBilling = async (force = false) => {
380
- if (stopped || billingBusy || !force && idle()) return;
381
- billingBusy = true;
382
- try {
383
- for (const r of billingAccounts) {
384
- if (stopped) return;
385
- billing.set(r.account.id, await fetchAccountBilling(r.account));
386
- }
387
- rebuild();
388
- } finally {
389
- billingBusy = false;
390
- }
391
- };
392
- hydrateFromCache();
393
- return {
394
- snapshot: () => current,
395
- start() {
396
- void refreshSummary(true);
397
- void refreshTable(true);
398
- void refreshBilling(true);
399
- summaryTimer = setInterval(() => {
400
- void refreshSummary();
401
- }, summaryIntervalMs);
402
- tableTimer = setInterval(() => {
403
- void refreshTable();
404
- }, TABLE_INTERVAL_MS);
405
- billingTimer = setInterval(() => {
406
- void refreshBilling();
407
- }, billingIntervalMs);
408
- summaryTimer.unref?.();
409
- tableTimer.unref?.();
410
- billingTimer.unref?.();
411
- },
412
- touch() {
413
- lastActivity = Date.now();
414
- },
415
- addSseClient(res) {
416
- res.writeHead(200, {
417
- "Content-Type": "text/event-stream",
418
- "Cache-Control": "no-cache, no-transform",
419
- Connection: "keep-alive"
420
- });
421
- res.write("retry: 3000\n\n");
422
- if (currentFrame) res.write(currentFrame);
423
- const beat = setInterval(() => {
424
- try {
425
- res.write(": ping\n\n");
426
- } catch {
427
- }
428
- }, SSE_HEARTBEAT_MS);
429
- beat.unref?.();
430
- sseClients.set(res, beat);
431
- lastActivity = Date.now();
432
- if (!current || Date.now() - current.generatedAt > summaryIntervalMs) {
433
- void refreshSummary(true);
434
- void refreshTable(true);
435
- }
436
- return () => {
437
- clearInterval(beat);
438
- sseClients.delete(res);
439
- };
440
- },
441
- stop() {
442
- stopped = true;
443
- clearInterval(summaryTimer);
444
- clearInterval(tableTimer);
445
- clearInterval(billingTimer);
446
- for (const [res, beat] of sseClients) {
447
- clearInterval(beat);
448
- try {
449
- res.end();
450
- } catch {
451
- }
452
- }
453
- sseClients.clear();
454
- }
455
- };
456
- }
457
-
458
- // src/web/server.ts
459
- var HOST = "127.0.0.1";
460
- var DEFAULT_PORT = 4317;
461
- var MAX_PORT_TRIES = 20;
462
- var MIN_SUMMARY_INTERVAL_MS = 8e3;
463
- var BILLING_INTERVAL_FALLBACK_MIN = 5;
464
- function createRouter(engine, vite, webRoot) {
465
- return (req, res) => {
466
- const url = req.url || "/";
467
- const path = url.split("?")[0];
468
- if (path === "/api/data") {
469
- engine.touch();
470
- sendJson(res, 200, engine.snapshot() ?? { pending: true });
471
- return;
472
- }
473
- if (path === "/healthz") {
474
- sendJson(res, 200, { ok: true, ready: engine.snapshot() !== null });
475
- return;
476
- }
477
- if (path === "/api/stream") {
478
- const cleanup = engine.addSseClient(res);
479
- req.on("close", cleanup);
480
- return;
481
- }
482
- if (vite) {
483
- vite.middlewares(req, res, () => {
484
- send(res, 404, "text/plain", "not found");
485
- });
486
- return;
487
- }
488
- if (!webRoot) {
489
- send(res, 503, "text/html; charset=utf-8", MISSING_BUILD_HTML);
490
- return;
491
- }
492
- serveStatic(webRoot, url, res);
493
- };
494
- }
495
- async function startWebServer(opts) {
496
- const { config } = opts;
497
- const tz = tzFor(config);
498
- const version = appVersion();
499
- const summaryIntervalMs = Math.max(MIN_SUMMARY_INTERVAL_MS, (config.interval || 2) * 1e3);
500
- const billingIntervalMs = Math.max(1, config.billingInterval || BILLING_INTERVAL_FALLBACK_MIN) * 6e4;
501
- const log = (msg) => {
502
- if (opts.log) process.stdout.write(msg + "\n");
503
- };
504
- const resolved = await resolveAccounts(config);
505
- const server = createServer();
506
- let vite = null;
507
- if (isDevMode()) vite = await createViteDevServer(server, log);
508
- const webRoot = vite ? null : findWebRoot();
509
- if (!vite && !webRoot) log(" \u26A0 no dashboard available \u2014 see the page for build/dev instructions");
510
- const engine = createDataEngine({ version, tz, summaryIntervalMs, billingIntervalMs, resolved });
511
- server.addListener("request", createRouter(engine, vite, webRoot));
512
- const port = await listenWithFallback(server, opts.port ?? DEFAULT_PORT);
513
- const serverUrl = `http://${HOST}:${port}`;
514
- if (vite?.warmupRequest) {
515
- try {
516
- await Promise.race([vite.warmupRequest("/src/main.tsx"), delay(5e3)]);
517
- } catch {
518
- }
519
- }
520
- engine.start();
521
- return {
522
- url: serverUrl,
523
- port,
524
- snapshot: engine.snapshot,
525
- stop: () => new Promise((resolve) => {
526
- engine.stop();
527
- const closeHttp = () => {
528
- server.close(() => resolve());
529
- server.closeAllConnections?.();
530
- };
531
- if (vite) vite.close().then(closeHttp, closeHttp);
532
- else closeHttp();
533
- })
534
- };
535
- }
536
- function delay(ms) {
537
- return new Promise((resolve) => {
538
- const t = setTimeout(resolve, ms);
539
- t.unref?.();
540
- });
541
- }
542
- function listenWithFallback(server, startPort) {
543
- return new Promise((resolve, reject) => {
544
- let port = startPort;
545
- let tries = 0;
546
- const attempt = () => {
547
- server.once("error", (err) => {
548
- if (err.code === "EADDRINUSE" && tries < MAX_PORT_TRIES) {
549
- tries++;
550
- port++;
551
- setImmediate(attempt);
552
- } else {
553
- reject(err);
554
- }
555
- });
556
- server.listen(port, HOST, () => resolve(port));
557
- };
558
- attempt();
559
- });
560
- }
561
-
562
- export {
563
- startWebServer
564
- };
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- startWebServer
4
- } from "./chunk-STCMWCFW.js";
5
- import "./chunk-UAPL47GL.js";
6
- export {
7
- startWebServer
8
- };