| 1 | import { createHash } from 'node:crypto'; |
| 2 | |
| 3 | const useColor = |
| 4 | process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb'; |
| 5 | |
| 6 | const wrap = (open, close) => (s) => |
| 7 | useColor ? `\x1b[${open}m${s}\x1b[${close}m` : String(s); |
| 8 | |
| 9 | export const c = { |
| 10 | bold: wrap(1, 22), |
| 11 | dim: wrap(2, 22), |
| 12 | red: wrap(31, 39), |
| 13 | green: wrap(32, 39), |
| 14 | yellow: wrap(33, 39), |
| 15 | blue: wrap(34, 39), |
| 16 | magenta: wrap(35, 39), |
| 17 | cyan: wrap(36, 39), |
| 18 | gray: wrap(90, 39), |
| 19 | }; |
| 20 | |
| 21 | export function sha256(text) { |
| 22 | return createHash('sha256').update(text, 'utf8').digest('hex'); |
| 23 | } |
| 24 | |
| 25 | export function truncate(s, n = 80) { |
| 26 | if (!s) return ''; |
| 27 | const one = s.replace(/\s+/g, ' ').trim(); |
| 28 | return one.length <= n ? one : `${one.slice(0, n - 1).trimEnd()}...`; |
| 29 | } |
| 30 | |
| 31 | export function plural(n, word, pluralWord) { |
| 32 | return `${n} ${n === 1 ? word : pluralWord || `${word}s`}`; |
| 33 | } |
| 34 | |
| 35 | export function formatDuration(ms) { |
| 36 | if (!Number.isFinite(ms) || ms <= 0) return null; |
| 37 | const minutes = Math.round(ms / 60000); |
| 38 | if (minutes < 60) return `${minutes} min`; |
| 39 | const hours = ms / 3600000; |
| 40 | if (hours < 48) return `${Math.round(hours * 10) / 10} hours`; |
| 41 | const days = Math.round(ms / 86400000); |
| 42 | return `${days} days`; |
| 43 | } |
| 44 | |
| 45 | export function formatDay(ts) { |
| 46 | if (!ts) return null; |
| 47 | const d = new Date(ts); |
| 48 | if (Number.isNaN(d.getTime())) return null; |
| 49 | return d.toISOString().slice(0, 10); |
| 50 | } |
| 51 | |
| 52 | export function daySpan(timestamps) { |
| 53 | const valid = timestamps.map((t) => new Date(t).getTime()).filter(Number.isFinite); |
| 54 | if (!valid.length) return null; |
| 55 | const span = Math.max(...valid) - Math.min(...valid); |
| 56 | const days = Math.max(1, Math.ceil(span / 86400000)); |
| 57 | return days; |
| 58 | } |
| 59 | |
| 60 | export function shannonEntropy(s) { |
| 61 | if (!s) return 0; |
| 62 | const freq = new Map(); |
| 63 | for (const ch of s) freq.set(ch, (freq.get(ch) || 0) + 1); |
| 64 | let entropy = 0; |
| 65 | for (const count of freq.values()) { |
| 66 | const p = count / s.length; |
| 67 | entropy -= p * Math.log2(p); |
| 68 | } |
| 69 | return entropy; |
| 70 | } |
| 71 | |
| 72 | export function mdEscapePipe(s) { |
| 73 | return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); |
| 74 | } |
| 75 | |
| 76 | export function escapeMd(text) { |
| 77 | return String(text == null ? '' : text) |
| 78 | .replace(/&/g, '&') |
| 79 | .replace(/</g, '<') |
| 80 | .replace(/>/g, '>'); |
| 81 | } |
| 82 | |
| 83 | export function escapeMdTags(text) { |
| 84 | return String(text == null ? '' : text) |
| 85 | .replace(/</g, '<') |
| 86 | .replace(/>/g, '>'); |
| 87 | } |
| 88 | |
| 89 | export const ExitCode = Object.freeze({ |
| 90 | OK: 0, |
| 91 | ERROR: 1, |
| 92 | USAGE: 2, |
| 93 | NO_DATA: 3, |
| 94 | WOULD_LEAK: 4, |
| 95 | }); |
| 96 | |
| 97 | export class TreetraceError extends Error { |
| 98 | constructor(message, exitCode = ExitCode.ERROR) { |
| 99 | super(message); |
| 100 | this.name = 'TreetraceError'; |
| 101 | this.exitCode = exitCode; |
| 102 | } |
| 103 | } |