tokmon 0.28.1 → 0.28.3

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.
Files changed (57) hide show
  1. package/README.md +156 -234
  2. package/THIRD_PARTY_NOTICES.md +62 -0
  3. package/dist/{bootstrap-ink-L7QSJBMS.js → bootstrap-ink-WG4WG3R2.js} +1579 -1135
  4. package/dist/chunk-7CH6S7BR.js +447 -0
  5. package/dist/{chunk-CVKCVHS7.js → chunk-CFKUZCMB.js} +70 -310
  6. package/dist/chunk-HNWEJ6MS.js +1094 -0
  7. package/dist/chunk-JZSZEHVD.js +406 -0
  8. package/dist/{chunk-S33XIUAW.js → chunk-P57DVFNH.js} +277 -49
  9. package/dist/{chunk-4HU4EI5T.js → chunk-P5JN5DDX.js} +25 -46
  10. package/dist/chunk-TN6V7XSL.js +512 -0
  11. package/dist/chunk-USKOQIE3.js +278 -0
  12. package/dist/chunk-UYPDMVW5.js +21 -0
  13. package/dist/{cli-command-75LP4IDS.js → cli-command-MD3RJRP4.js} +249 -15
  14. package/dist/cli.js +6 -6
  15. package/dist/{config-HXFJTNLM.js → config-3DGZ47F7.js} +23 -3
  16. package/dist/daemon-EMOA36WB.js +377 -0
  17. package/dist/daemon-handle-JHE5MKMI.js +11 -0
  18. package/dist/server-EPSH52DR.js +12 -0
  19. package/dist/web/assets/{Area-CMXvOw33.js → Area-stRnp8Km.js} +1 -1
  20. package/dist/web/assets/analytics-moZZt9L_.js +2 -0
  21. package/dist/web/assets/breakdown-km9w7myU.js +4 -0
  22. package/dist/web/assets/button-DrX04Cc2.js +1 -0
  23. package/dist/web/assets/{chart-cZoLgpmG.js → chart-D0CtuL2k.js} +1 -1
  24. package/dist/web/assets/explore-B11uNJV1.js +22 -0
  25. package/dist/web/assets/index-RDhXsCNG.js +83 -0
  26. package/dist/web/assets/index-ZmOS8p9O.css +1 -0
  27. package/dist/web/assets/models-DYbZnY34.js +2 -0
  28. package/dist/web/assets/overview-CruICbfJ.js +2 -0
  29. package/dist/web/assets/panel-xckM3_GC.js +1 -0
  30. package/dist/web/assets/settings-sheet-G-BTSIF6.js +1 -0
  31. package/dist/web/assets/share-sheet-weIdd4DW.js +2 -0
  32. package/dist/web/assets/timeline-BHgniliZ.js +1 -0
  33. package/dist/web/index.html +30 -4
  34. package/package.json +13 -4
  35. package/site/THIRD_PARTY_NOTICES.md +83 -0
  36. package/site/public/fonts/OFL.txt +93 -0
  37. package/dist/chunk-3RTWFGGD.js +0 -275
  38. package/dist/chunk-FMP3P2WV.js +0 -299
  39. package/dist/chunk-HP5UZCXP.js +0 -436
  40. package/dist/chunk-SMPY52EV.js +0 -125
  41. package/dist/chunk-XDA5RJST.js +0 -193
  42. package/dist/daemon-CL4FJW26.js +0 -219
  43. package/dist/daemon-handle-QQLJE46Y.js +0 -10
  44. package/dist/server-EUO7CWYH.js +0 -11
  45. package/dist/web/assets/analytics-DqdZXOJL.js +0 -2
  46. package/dist/web/assets/breakdown-kOSSJ3mI.js +0 -4
  47. package/dist/web/assets/explore-vcsM8K9I.js +0 -22
  48. package/dist/web/assets/index-BErCXT7f.css +0 -1
  49. package/dist/web/assets/index-DH3dOnbg.js +0 -83
  50. package/dist/web/assets/models-B5ikm83_.js +0 -2
  51. package/dist/web/assets/overview-DD1kP8CP.js +0 -2
  52. package/dist/web/assets/panel-DTQBwExW.js +0 -1
  53. package/dist/web/assets/primitives-C36qJlA2.js +0 -1
  54. package/dist/web/assets/settings-sheet-_i2zTQjf.js +0 -1
  55. package/dist/web/assets/share-sheet-BLHmrFbm.js +0 -2
  56. package/dist/web/assets/timeline-B4uG-Emo.js +0 -1
  57. package/dist/web/assets/use-dialog-trap-bfaXEMz9.js +0 -1
@@ -1,436 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/config.ts
4
- import { chmod, readFile, writeFile, mkdir, rename, unlink } from "fs/promises";
5
- import { chmodSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
6
- import { join, isAbsolute } from "path";
7
- import { homedir } from "os";
8
-
9
- // src/providers/types.ts
10
- var PROVIDER_IDS = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini", "grok"];
11
-
12
- // src/config-schema.ts
13
- var DEFAULTS = {
14
- revision: 0,
15
- interval: 2,
16
- billingInterval: 5,
17
- clearScreen: true,
18
- privacyMode: true,
19
- privacyToggleKey: "p",
20
- timezone: null,
21
- accounts: [],
22
- activeAccountId: null,
23
- disabledProviders: [],
24
- onboarded: false,
25
- dashboardLayout: "grid",
26
- defaultFocus: "all",
27
- ascii: "auto",
28
- allowNetworkAccess: false,
29
- allowedHosts: [],
30
- resetDisplay: "relative",
31
- knownProviders: []
32
- };
33
- var LEGACY_KNOWN = ["claude", "codex", "cursor"];
34
- var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
35
- var PROVIDER_ORDER = [...PROVIDER_IDS];
36
- var COLOR_PALETTE = [
37
- "cyan",
38
- "magenta",
39
- "green",
40
- "yellow",
41
- "blue",
42
- "red",
43
- "cyanBright",
44
- "magentaBright",
45
- "greenBright"
46
- ];
47
- var PROVIDER_META = {
48
- claude: { name: "Claude", color: "green" },
49
- codex: { name: "Codex", color: "cyan" },
50
- cursor: { name: "Cursor", color: "magenta" },
51
- copilot: { name: "Copilot", color: "white" },
52
- pi: { name: "Pi", color: "blue" },
53
- opencode: { name: "opencode", color: "yellow" },
54
- antigravity: { name: "Antigravity", color: "red" },
55
- gemini: { name: "Gemini", color: "greenBright" },
56
- grok: { name: "Grok", color: "yellowBright" }
57
- };
58
- var EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
59
- var EMAIL_RE_GLOBAL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
60
- function containsEmail(value) {
61
- return typeof value === "string" && EMAIL_RE.test(value);
62
- }
63
- function redactEmail(value) {
64
- return value.replace(EMAIL_RE_GLOBAL, "[redacted]");
65
- }
66
- function getTrackedAccountRows(config, trackedProviders = PROVIDER_ORDER.filter((pid) => !config.disabledProviders.includes(pid)), autoAccounts) {
67
- const tracked = new Set(trackedProviders);
68
- const configuredIds = /* @__PURE__ */ new Set();
69
- const configuredKeys = /* @__PURE__ */ new Set();
70
- const rowIds = /* @__PURE__ */ new Set();
71
- const rowKeys = /* @__PURE__ */ new Set();
72
- const rows = [];
73
- const keyFor = (providerId, homeDir) => `${providerId}:${homeDir && homeDir !== "~" ? homeDir : "~"}`;
74
- const rememberRow = (row) => {
75
- rowIds.add(row.id);
76
- rowKeys.add(keyFor(row.providerId, row.homeDir));
77
- rows.push(row);
78
- };
79
- config.accounts.forEach((account, explicitIndex) => {
80
- const meta = PROVIDER_META[account.providerId];
81
- configuredIds.add(account.id);
82
- configuredKeys.add(keyFor(account.providerId, account.homeDir));
83
- rememberRow({
84
- id: account.id,
85
- providerId: account.providerId,
86
- name: account.name,
87
- homeDir: account.homeDir || "~",
88
- color: account.color || meta.color,
89
- source: "configured",
90
- explicitId: account.id,
91
- explicitIndex
92
- });
93
- });
94
- if (autoAccounts) {
95
- for (const account of autoAccounts) {
96
- if (config.disabledProviders.includes(account.providerId)) continue;
97
- const key = keyFor(account.providerId, account.homeDir);
98
- if (configuredIds.has(account.id) || configuredKeys.has(key) || rowIds.has(account.id) || rowKeys.has(key)) continue;
99
- const meta = PROVIDER_META[account.providerId];
100
- rememberRow({
101
- id: account.id,
102
- providerId: account.providerId,
103
- name: account.name,
104
- homeDir: account.homeDir || "~",
105
- color: account.color || meta.color,
106
- source: "auto"
107
- });
108
- }
109
- }
110
- for (const providerId of PROVIDER_ORDER) {
111
- if (config.disabledProviders.includes(providerId)) continue;
112
- if (!tracked.has(providerId)) continue;
113
- const key = keyFor(providerId, "~");
114
- if (configuredIds.has(providerId) || configuredKeys.has(key) || rowIds.has(providerId) || rowKeys.has(key)) continue;
115
- const meta = PROVIDER_META[providerId];
116
- rememberRow({
117
- id: providerId,
118
- providerId,
119
- name: meta.name,
120
- homeDir: "~",
121
- color: meta.color,
122
- source: "auto"
123
- });
124
- }
125
- return rows;
126
- }
127
- function clampNum(v, fallback, min) {
128
- return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
129
- }
130
- function isValidTimezone(tz) {
131
- try {
132
- new Intl.DateTimeFormat("en-CA", { timeZone: tz });
133
- return true;
134
- } catch {
135
- return false;
136
- }
137
- }
138
- function isRecord(value) {
139
- return !!value && typeof value === "object" && !Array.isArray(value);
140
- }
141
- function validProvider(value) {
142
- return PROVIDER_IDS.includes(value);
143
- }
144
- function sameJson(a, b) {
145
- try {
146
- return JSON.stringify(a) === JSON.stringify(b);
147
- } catch {
148
- return false;
149
- }
150
- }
151
- function normalizeAllowedHost(value) {
152
- if (typeof value !== "string") return null;
153
- const candidate = value.trim();
154
- if (!candidate) return null;
155
- try {
156
- const url = new URL(`http://${candidate}`);
157
- if (url.username || url.password || url.port || url.pathname !== "/" || url.search || url.hash) return null;
158
- const hostname = url.hostname.toLowerCase().replace(/\.$/, "");
159
- if (!hostname || hostname.length > 253 || hostname.includes("*")) return null;
160
- const labels = hostname.split(".");
161
- if (labels.some((label) => !label || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label))) return null;
162
- return hostname;
163
- } catch {
164
- return null;
165
- }
166
- }
167
- function normalizeAllowedHosts(value) {
168
- if (!Array.isArray(value)) return [];
169
- const hosts = value.map(normalizeAllowedHost).filter((host) => host !== null);
170
- return [...new Set(hosts)];
171
- }
172
- function repairConfig(input) {
173
- const reasons = [];
174
- const parsed = isRecord(input) ? input : {};
175
- if (parsed !== input) reasons.push("config root was not an object");
176
- const rawAccounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
177
- if (!Array.isArray(parsed.accounts) && parsed.accounts !== void 0) reasons.push("accounts was not an array");
178
- const accounts = [];
179
- const accountIds = /* @__PURE__ */ new Set();
180
- rawAccounts.forEach((raw, index) => {
181
- if (!isRecord(raw)) {
182
- reasons.push(`accounts[${index}] was not an object`);
183
- return;
184
- }
185
- const providerId = raw.providerId ?? "claude";
186
- if (!validProvider(providerId)) {
187
- reasons.push(`accounts[${index}].providerId was invalid`);
188
- return;
189
- }
190
- const id = typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : "";
191
- const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : "";
192
- if (!id || !name) {
193
- reasons.push(`accounts[${index}] was missing id or name`);
194
- return;
195
- }
196
- if (accountIds.has(id)) {
197
- reasons.push(`accounts[${index}].id was duplicated`);
198
- return;
199
- }
200
- accountIds.add(id);
201
- accounts.push({
202
- id,
203
- providerId,
204
- name,
205
- homeDir: typeof raw.homeDir === "string" && raw.homeDir.trim() ? raw.homeDir : "~",
206
- ...typeof raw.color === "string" && raw.color.trim() ? { color: raw.color } : {}
207
- });
208
- });
209
- const disabledProviders = (Array.isArray(parsed.disabledProviders) ? parsed.disabledProviders : []).filter(validProvider);
210
- if (parsed.disabledProviders !== void 0 && !Array.isArray(parsed.disabledProviders)) {
211
- reasons.push("disabledProviders was not an array");
212
- }
213
- const knownProviders = Array.isArray(parsed.knownProviders) ? parsed.knownProviders.filter(validProvider) : parsed.onboarded === true ? [...LEGACY_KNOWN] : [];
214
- if (parsed.knownProviders !== void 0 && !Array.isArray(parsed.knownProviders)) {
215
- reasons.push("knownProviders was not an array");
216
- }
217
- const allowedHosts = normalizeAllowedHosts(parsed.allowedHosts);
218
- if (parsed.allowedHosts !== void 0 && !sameJson(parsed.allowedHosts, allowedHosts)) {
219
- reasons.push("allowedHosts contained invalid or duplicate hostnames");
220
- }
221
- const activeAccountId = typeof parsed.activeAccountId === "string" && (accountIds.has(parsed.activeAccountId) || PROVIDER_IDS.includes(parsed.activeAccountId)) ? parsed.activeAccountId : null;
222
- if (parsed.activeAccountId !== void 0 && parsed.activeAccountId !== null && activeAccountId === null) {
223
- reasons.push("activeAccountId did not match a known account/provider");
224
- }
225
- const timezone = typeof parsed.timezone === "string" && parsed.timezone.trim() && isValidTimezone(parsed.timezone.trim()) ? parsed.timezone.trim() : null;
226
- if (parsed.timezone !== void 0 && parsed.timezone !== null && timezone === null) {
227
- reasons.push("timezone was invalid");
228
- }
229
- const config = {
230
- ...DEFAULTS,
231
- revision: typeof parsed.revision === "number" && Number.isSafeInteger(parsed.revision) && parsed.revision >= 0 ? parsed.revision : DEFAULTS.revision,
232
- interval: clampNum(parsed.interval, DEFAULTS.interval, 1),
233
- billingInterval: clampNum(parsed.billingInterval, DEFAULTS.billingInterval, 1),
234
- clearScreen: typeof parsed.clearScreen === "boolean" ? parsed.clearScreen : DEFAULTS.clearScreen,
235
- privacyMode: typeof parsed.privacyMode === "boolean" ? parsed.privacyMode : DEFAULTS.privacyMode,
236
- privacyToggleKey: typeof parsed.privacyToggleKey === "string" && parsed.privacyToggleKey.length === 1 ? parsed.privacyToggleKey : DEFAULTS.privacyToggleKey,
237
- timezone,
238
- accounts,
239
- activeAccountId,
240
- disabledProviders,
241
- onboarded: parsed.onboarded === true,
242
- dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
243
- defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
244
- ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
245
- allowNetworkAccess: parsed.allowNetworkAccess === true,
246
- allowedHosts,
247
- resetDisplay: parsed.resetDisplay === "absolute" ? "absolute" : "relative",
248
- knownProviders
249
- };
250
- for (const key of Object.keys(DEFAULTS)) {
251
- if (!(key in parsed)) reasons.push(`missing ${key}`);
252
- }
253
- return { config, repaired: reasons.length > 0 || !sameJson(parsed, config), reasons };
254
- }
255
- function normalizeConfig(parsed) {
256
- try {
257
- return repairConfig(parsed).config;
258
- } catch {
259
- return { ...DEFAULTS };
260
- }
261
- }
262
- function slugify(value) {
263
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
264
- }
265
- function generateAccountId(name, existing) {
266
- const base = slugify(name) || "account";
267
- const taken = new Set(existing.map((a) => a.id));
268
- if (!taken.has(base)) return base;
269
- for (let i = 2; i < 1e3; i++) {
270
- const candidate = `${base}_${i}`;
271
- if (!taken.has(candidate)) return candidate;
272
- }
273
- return `${base}_${Date.now()}`;
274
- }
275
- function pickAccentColor(existing) {
276
- const used = new Set(existing.map((a) => a.color).filter(Boolean));
277
- for (const c of ACCENT_COLORS) {
278
- if (!used.has(c)) return c;
279
- }
280
- return ACCENT_COLORS[existing.length % ACCENT_COLORS.length];
281
- }
282
- function sanitizeTyped(input) {
283
- if (!input) return "";
284
- return input.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\x1bO./g, "").replace(/\x1b/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, "").replace(/\[20[01]~/g, "");
285
- }
286
-
287
- // src/config.ts
288
- function envDir(name) {
289
- const v = process.env[name];
290
- return v && v.trim() && isAbsolute(v.trim()) ? v.trim() : void 0;
291
- }
292
- function configDir() {
293
- if (process.platform === "win32") {
294
- return join(envDir("APPDATA") ?? join(homedir(), "AppData", "Roaming"), "tokmon");
295
- }
296
- return join(envDir("XDG_CONFIG_HOME") ?? join(homedir(), ".config"), "tokmon");
297
- }
298
- function configLocation() {
299
- return join(configDir(), "config.json");
300
- }
301
- function cacheDir() {
302
- if (process.platform === "win32") {
303
- return join(envDir("LOCALAPPDATA") ?? envDir("APPDATA") ?? join(homedir(), "AppData", "Local"), "tokmon", "cache");
304
- }
305
- if (process.platform === "darwin") {
306
- return join(homedir(), "Library", "Caches", "tokmon");
307
- }
308
- return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
309
- }
310
- function snapshotCacheFile() {
311
- return join(cacheDir(), "web-snapshot.json");
312
- }
313
- async function loadConfig() {
314
- let raw;
315
- try {
316
- raw = await readFile(configLocation(), "utf-8");
317
- } catch {
318
- return { ...DEFAULTS };
319
- }
320
- try {
321
- await chmod(configDir(), 448);
322
- await chmod(configLocation(), 384);
323
- } catch {
324
- }
325
- let parsed;
326
- try {
327
- parsed = JSON.parse(raw);
328
- } catch {
329
- try {
330
- const dir = configDir();
331
- await mkdir(dir, { recursive: true, mode: 448 });
332
- await chmod(dir, 448);
333
- const backup = configLocation() + ".bak";
334
- await writeFile(backup, raw, { mode: 384 });
335
- await chmod(backup, 384);
336
- } catch {
337
- }
338
- return { ...DEFAULTS };
339
- }
340
- const repaired = repairConfig(parsed);
341
- if (repaired.repaired) {
342
- try {
343
- await saveConfig(repaired.config);
344
- } catch {
345
- }
346
- }
347
- return repaired.config;
348
- }
349
- var saveQueue = Promise.resolve();
350
- var tempSequence = 0;
351
- function configTempFile(dir) {
352
- tempSequence = (tempSequence + 1) % Number.MAX_SAFE_INTEGER;
353
- return join(dir, `config.json.${process.pid}.${Date.now()}.${tempSequence}.tmp`);
354
- }
355
- function configJson(config) {
356
- return JSON.stringify(normalizeConfig(config), null, 2) + "\n";
357
- }
358
- function saveConfig(config) {
359
- const write = async () => {
360
- const dir = configDir();
361
- await mkdir(dir, { recursive: true, mode: 448 });
362
- await chmod(dir, 448);
363
- const tmp = configTempFile(dir);
364
- try {
365
- await writeFile(tmp, configJson(config), { mode: 384 });
366
- await chmod(tmp, 384);
367
- await rename(tmp, configLocation());
368
- } catch (error) {
369
- try {
370
- await unlink(tmp);
371
- } catch {
372
- }
373
- throw error;
374
- }
375
- };
376
- const operation = saveQueue.catch(() => void 0).then(write);
377
- saveQueue = operation;
378
- return operation;
379
- }
380
- function saveConfigSync(config) {
381
- const dir = configDir();
382
- mkdirSync(dir, { recursive: true, mode: 448 });
383
- chmodSync(dir, 448);
384
- const tmp = configTempFile(dir);
385
- try {
386
- writeFileSync(tmp, configJson(config), { mode: 384 });
387
- chmodSync(tmp, 384);
388
- renameSync(tmp, configLocation());
389
- } catch (error) {
390
- try {
391
- unlinkSync(tmp);
392
- } catch {
393
- }
394
- throw error;
395
- }
396
- }
397
- function expandHome(p) {
398
- if (!p) return homedir();
399
- if (p === "~" || p === "~/" || p === "~\\") return homedir();
400
- if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
401
- return p;
402
- }
403
- function findAccount(config, id) {
404
- if (!id) return null;
405
- return config.accounts.find((a) => a.id === id) ?? null;
406
- }
407
-
408
- export {
409
- PROVIDER_IDS,
410
- DEFAULTS,
411
- ACCENT_COLORS,
412
- COLOR_PALETTE,
413
- PROVIDER_META,
414
- containsEmail,
415
- redactEmail,
416
- getTrackedAccountRows,
417
- clampNum,
418
- isValidTimezone,
419
- normalizeAllowedHost,
420
- normalizeAllowedHosts,
421
- repairConfig,
422
- normalizeConfig,
423
- slugify,
424
- generateAccountId,
425
- pickAccentColor,
426
- sanitizeTyped,
427
- envDir,
428
- configLocation,
429
- cacheDir,
430
- snapshotCacheFile,
431
- loadConfig,
432
- saveConfig,
433
- saveConfigSync,
434
- expandHome,
435
- findAccount
436
- };
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/web/static.ts
4
- import { createReadStream, existsSync, readFileSync } from "fs";
5
- import { stat } from "fs/promises";
6
- import { fileURLToPath } from "url";
7
- import { extname, join, normalize, sep } from "path";
8
- var MIME = {
9
- ".html": "text/html; charset=utf-8",
10
- ".js": "text/javascript; charset=utf-8",
11
- ".mjs": "text/javascript; charset=utf-8",
12
- ".css": "text/css; charset=utf-8",
13
- ".json": "application/json; charset=utf-8",
14
- ".svg": "image/svg+xml",
15
- ".png": "image/png",
16
- ".jpg": "image/jpeg",
17
- ".jpeg": "image/jpeg",
18
- ".gif": "image/gif",
19
- ".ico": "image/x-icon",
20
- ".woff": "font/woff",
21
- ".woff2": "font/woff2",
22
- ".ttf": "font/ttf",
23
- ".otf": "font/otf",
24
- ".map": "application/json; charset=utf-8",
25
- ".webmanifest": "application/manifest+json"
26
- };
27
- function findWebRoot() {
28
- const candidates = ["./web/", "../web/", "../dist/web/", "../../dist/web/"];
29
- for (const rel of candidates) {
30
- try {
31
- const dir = fileURLToPath(new URL(rel, import.meta.url));
32
- if (existsSync(join(dir, "index.html"))) return dir.replace(/[\\/]+$/, "");
33
- } catch {
34
- }
35
- }
36
- return null;
37
- }
38
- function resolveStaticPath(webRoot, urlPath) {
39
- let clean;
40
- try {
41
- clean = decodeURIComponent(urlPath.split("?")[0]);
42
- } catch {
43
- return null;
44
- }
45
- const rel = normalize(clean).replace(/^(\.\.[/\\])+/, "").replace(/^[/\\]+/, "");
46
- const full = join(webRoot, rel);
47
- if (full !== webRoot && !full.startsWith(webRoot + sep)) return null;
48
- return full;
49
- }
50
- function send(res, status, type, body) {
51
- res.writeHead(status, {
52
- "Content-Type": type,
53
- "Cache-Control": "no-store",
54
- "Referrer-Policy": "no-referrer",
55
- "X-Content-Type-Options": "nosniff"
56
- });
57
- res.end(body);
58
- }
59
- function sendJson(res, status, data) {
60
- send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
61
- }
62
- function serveStatic(webRoot, urlPath, res) {
63
- const path = urlPath.split("?")[0];
64
- const filePath = resolveStaticPath(webRoot, path === "/" ? "/index.html" : path);
65
- if (!filePath) {
66
- send(res, 403, "text/plain", "forbidden");
67
- return;
68
- }
69
- void stat(filePath).then((st) => {
70
- if (st.isFile()) {
71
- const type = MIME[extname(filePath).toLowerCase()] || "application/octet-stream";
72
- const immutable = filePath.includes(`${sep}assets${sep}`);
73
- res.writeHead(200, {
74
- "Content-Type": type,
75
- "Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
76
- "Referrer-Policy": "no-referrer",
77
- "X-Content-Type-Options": "nosniff"
78
- });
79
- createReadStream(filePath).pipe(res);
80
- } else {
81
- throw new Error("not a file");
82
- }
83
- }).catch(() => {
84
- if (extname(path)) {
85
- send(res, 404, "text/plain", "not found");
86
- return;
87
- }
88
- const indexPath = join(webRoot, "index.html");
89
- if (!existsSync(indexPath)) {
90
- send(res, 404, "text/plain", "not found");
91
- return;
92
- }
93
- res.writeHead(200, {
94
- "Content-Type": MIME[".html"],
95
- "Cache-Control": "no-cache",
96
- "Referrer-Policy": "no-referrer",
97
- "X-Content-Type-Options": "nosniff"
98
- });
99
- createReadStream(indexPath).on("error", () => {
100
- try {
101
- res.destroy();
102
- } catch {
103
- }
104
- }).pipe(res);
105
- });
106
- }
107
- function appVersion() {
108
- for (const rel of ["../package.json", "../../package.json"]) {
109
- try {
110
- const p = fileURLToPath(new URL(rel, import.meta.url));
111
- const pkg = JSON.parse(readFileSync(p, "utf-8"));
112
- if (typeof pkg.version === "string") return pkg.version;
113
- } catch {
114
- }
115
- }
116
- return "";
117
- }
118
-
119
- export {
120
- findWebRoot,
121
- send,
122
- sendJson,
123
- serveStatic,
124
- appVersion
125
- };