shellrecap 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,1308 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CONFIG_FILENAMES: () => CONFIG_FILENAMES,
24
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG,
25
+ KNOWN_COMMANDS: () => KNOWN_COMMANDS,
26
+ KNOWN_TYPOS: () => KNOWN_TYPOS,
27
+ PERSONAS: () => PERSONAS,
28
+ SECRET_KIND_LABELS: () => SECRET_KIND_LABELS,
29
+ WEEKDAY_NAMES: () => WEEKDAY_NAMES,
30
+ WEEKDAY_SHORT: () => WEEKDAY_SHORT,
31
+ analyze: () => analyze,
32
+ computeAliasStats: () => computeAliasStats,
33
+ computeSecretHits: () => computeSecretHits,
34
+ computeTimeStats: () => computeTimeStats,
35
+ computeTypoStats: () => computeTypoStats,
36
+ daysToYMD: () => daysToYMD,
37
+ derivePersona: () => derivePersona,
38
+ detectFormat: () => detectFormat,
39
+ editDistance: () => editDistance,
40
+ epochToDate: () => epochToDate,
41
+ epochToHour: () => epochToHour,
42
+ epochToWeekday: () => epochToWeekday,
43
+ hourLabel: () => hourLabel,
44
+ isValidTheme: () => isValidTheme,
45
+ mask: () => mask,
46
+ mergeConfig: () => mergeConfig,
47
+ parseBash: () => parseBash,
48
+ parseConfig: () => parseConfig,
49
+ parseFish: () => parseFish,
50
+ parseHistory: () => parseHistory,
51
+ parsePlain: () => parsePlain,
52
+ parseZshExtended: () => parseZshExtended,
53
+ renderCard: () => renderCard,
54
+ splitSegments: () => splitSegments,
55
+ suggestAliasName: () => suggestAliasName,
56
+ toJSON: () => toJSON,
57
+ toMarkdown: () => toMarkdown,
58
+ toSegment: () => toSegment,
59
+ tokenizeEntry: () => tokenizeEntry,
60
+ tokens: () => tokens
61
+ });
62
+ module.exports = __toCommonJS(index_exports);
63
+
64
+ // src/parse.ts
65
+ var ZSH_EXT_RE = /^: (\d{9,12}):(\d+);(.*)$/;
66
+ var BASH_TS_RE = /^#(\d{9,12})\s*$/;
67
+ var FISH_CMD_RE = /^- cmd: (.*)$/;
68
+ var FISH_WHEN_RE = /^\s+when: (\d{9,12})\s*$/;
69
+ function detectFormat(raw) {
70
+ const lines = raw.split("\n", 400).filter((l) => l.length > 0).slice(0, 200);
71
+ let zsh = 0;
72
+ let bashTs = 0;
73
+ let fish = 0;
74
+ for (const l of lines) {
75
+ if (ZSH_EXT_RE.test(l)) zsh++;
76
+ else if (BASH_TS_RE.test(l)) bashTs++;
77
+ else if (FISH_CMD_RE.test(l)) fish++;
78
+ }
79
+ if (fish > 0 && fish >= zsh) return "fish";
80
+ if (zsh > 0) return "zsh-extended";
81
+ if (bashTs > 0) return "bash";
82
+ return "plain";
83
+ }
84
+ function parseZshExtended(raw) {
85
+ const out = [];
86
+ for (const line of raw.split("\n")) {
87
+ const m = line.match(ZSH_EXT_RE);
88
+ if (m) {
89
+ out.push({ cmd: m[3], ts: Number(m[1]) });
90
+ } else if (line.length > 0 && out.length > 0) {
91
+ out[out.length - 1].cmd += "\n" + line;
92
+ }
93
+ }
94
+ return finalize(out);
95
+ }
96
+ function parseBash(raw) {
97
+ const out = [];
98
+ let pendingTs;
99
+ for (const line of raw.split("\n")) {
100
+ if (line.length === 0) continue;
101
+ const ts = line.match(BASH_TS_RE);
102
+ if (ts) {
103
+ pendingTs = Number(ts[1]);
104
+ continue;
105
+ }
106
+ out.push(pendingTs === void 0 ? { cmd: line } : { cmd: line, ts: pendingTs });
107
+ pendingTs = void 0;
108
+ }
109
+ return finalize(out);
110
+ }
111
+ function unescapeFish(s) {
112
+ let out = "";
113
+ for (let i = 0; i < s.length; i++) {
114
+ const c = s[i];
115
+ if (c === "\\" && i + 1 < s.length) {
116
+ const n = s[i + 1];
117
+ if (n === "n") {
118
+ out += "\n";
119
+ i++;
120
+ continue;
121
+ }
122
+ if (n === "\\") {
123
+ out += "\\";
124
+ i++;
125
+ continue;
126
+ }
127
+ }
128
+ out += c;
129
+ }
130
+ return out;
131
+ }
132
+ function parseFish(raw) {
133
+ const out = [];
134
+ for (const line of raw.split("\n")) {
135
+ const cmd = line.match(FISH_CMD_RE);
136
+ if (cmd) {
137
+ out.push({ cmd: unescapeFish(cmd[1]) });
138
+ continue;
139
+ }
140
+ const when = line.match(FISH_WHEN_RE);
141
+ if (when && out.length > 0) {
142
+ out[out.length - 1].ts = Number(when[1]);
143
+ }
144
+ }
145
+ return finalize(out);
146
+ }
147
+ function parsePlain(raw) {
148
+ const out = [];
149
+ for (const line of raw.split("\n")) {
150
+ if (line.trim().length === 0) continue;
151
+ out.push({ cmd: line });
152
+ }
153
+ return finalize(out);
154
+ }
155
+ function finalize(entries) {
156
+ return entries.filter((e) => e.cmd.trim().length > 0);
157
+ }
158
+ function parseHistory(raw, format) {
159
+ const f = format ?? detectFormat(raw);
160
+ switch (f) {
161
+ case "zsh-extended":
162
+ return { entries: parseZshExtended(raw), format: f };
163
+ case "bash":
164
+ return { entries: parseBash(raw), format: f };
165
+ case "fish":
166
+ return { entries: parseFish(raw), format: f };
167
+ case "plain":
168
+ return { entries: parsePlain(raw), format: f };
169
+ }
170
+ }
171
+
172
+ // src/tokenize.ts
173
+ var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "builtin", "exec", "nohup", "time", "doas", "env"]);
174
+ var SUBCOMMAND_CLIS = /* @__PURE__ */ new Set([
175
+ "git",
176
+ "docker",
177
+ "podman",
178
+ "kubectl",
179
+ "helm",
180
+ "npm",
181
+ "pnpm",
182
+ "yarn",
183
+ "bun",
184
+ "deno",
185
+ "cargo",
186
+ "go",
187
+ "pip",
188
+ "pip3",
189
+ "uv",
190
+ "poetry",
191
+ "brew",
192
+ "apt",
193
+ "apt-get",
194
+ "dnf",
195
+ "pacman",
196
+ "gh",
197
+ "glab",
198
+ "aws",
199
+ "gcloud",
200
+ "az",
201
+ "terraform",
202
+ "pulumi",
203
+ "vagrant",
204
+ "systemctl",
205
+ "service",
206
+ "make",
207
+ "just",
208
+ "rails",
209
+ "bundle",
210
+ "mix",
211
+ "composer",
212
+ "dotnet",
213
+ "flutter",
214
+ "gem",
215
+ "conda",
216
+ "nix",
217
+ "tmux",
218
+ "svn"
219
+ ]);
220
+ function splitSegments(cmd) {
221
+ const parts = [];
222
+ let cur = "";
223
+ let q = null;
224
+ let pipes = 0;
225
+ const push = () => {
226
+ const t = cur.trim();
227
+ if (t.length > 0) parts.push(t);
228
+ cur = "";
229
+ };
230
+ for (let i = 0; i < cmd.length; i++) {
231
+ const c = cmd[i];
232
+ if (q) {
233
+ cur += c;
234
+ if (c === q && cmd[i - 1] !== "\\") q = null;
235
+ continue;
236
+ }
237
+ if (c === "'" || c === '"') {
238
+ q = c;
239
+ cur += c;
240
+ continue;
241
+ }
242
+ if (c === "\\" && i + 1 < cmd.length) {
243
+ cur += c + cmd[i + 1];
244
+ i++;
245
+ continue;
246
+ }
247
+ if (c === "|") {
248
+ pipes++;
249
+ if (cmd[i + 1] === "|") i++;
250
+ push();
251
+ continue;
252
+ }
253
+ if (c === "&" && cmd[i + 1] === "&") {
254
+ i++;
255
+ push();
256
+ continue;
257
+ }
258
+ if (c === ";" || c === "\n") {
259
+ push();
260
+ continue;
261
+ }
262
+ cur += c;
263
+ }
264
+ push();
265
+ return { parts, pipes };
266
+ }
267
+ function tokens(segment) {
268
+ const out = [];
269
+ let cur = "";
270
+ let q = null;
271
+ for (let i = 0; i < segment.length; i++) {
272
+ const c = segment[i];
273
+ if (q) {
274
+ if (c === q) q = null;
275
+ else cur += c;
276
+ continue;
277
+ }
278
+ if (c === "'" || c === '"') {
279
+ q = c;
280
+ continue;
281
+ }
282
+ if (c === "\\" && i + 1 < segment.length) {
283
+ cur += segment[i + 1];
284
+ i++;
285
+ continue;
286
+ }
287
+ if (c === " " || c === " ") {
288
+ if (cur) out.push(cur);
289
+ cur = "";
290
+ continue;
291
+ }
292
+ cur += c;
293
+ }
294
+ if (cur) out.push(cur);
295
+ return out;
296
+ }
297
+ var ENV_ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
298
+ function toSegment(part) {
299
+ let tks = tokens(part);
300
+ if (tks.length === 0) return null;
301
+ let sudo = false;
302
+ let guard = 0;
303
+ while (tks.length > 0 && guard++ < 10) {
304
+ const head = tks[0];
305
+ if (ENV_ASSIGN_RE.test(head)) {
306
+ tks = tks.slice(1);
307
+ continue;
308
+ }
309
+ if (WRAPPERS.has(head)) {
310
+ if (head === "sudo" || head === "doas") sudo = true;
311
+ tks = tks.slice(1);
312
+ while (tks.length > 0 && tks[0].startsWith("-")) tks = tks.slice(1);
313
+ continue;
314
+ }
315
+ break;
316
+ }
317
+ if (tks.length === 0) return null;
318
+ let base = tks[0];
319
+ if (base.includes("/")) base = base.slice(base.lastIndexOf("/") + 1);
320
+ base = base.toLowerCase();
321
+ if (base.length === 0) return null;
322
+ let withSub = base;
323
+ if (SUBCOMMAND_CLIS.has(base)) {
324
+ const next = tks.find((t, i) => i > 0 && /^[a-z][a-z0-9_-]{0,19}$/i.test(t));
325
+ if (next) withSub = `${base} ${next.toLowerCase()}`;
326
+ }
327
+ return { base, withSub, tokens: tks, sudo };
328
+ }
329
+ function tokenizeEntry(entry) {
330
+ const { parts, pipes } = splitSegments(entry.cmd);
331
+ const segments = [];
332
+ for (const p of parts) {
333
+ const s = toSegment(p);
334
+ if (s) segments.push(s);
335
+ }
336
+ return { raw: entry.cmd, ts: entry.ts, segments, pipes };
337
+ }
338
+
339
+ // src/date.ts
340
+ function epochToDays(ts, tzOffsetMin) {
341
+ return Math.floor((ts + tzOffsetMin * 60) / 86400);
342
+ }
343
+ function epochToHour(ts, tzOffsetMin) {
344
+ const sec = ((ts + tzOffsetMin * 60) % 86400 + 86400) % 86400;
345
+ return Math.floor(sec / 3600);
346
+ }
347
+ function epochToWeekday(ts, tzOffsetMin) {
348
+ const days = epochToDays(ts, tzOffsetMin);
349
+ return ((days + 4) % 7 + 7) % 7;
350
+ }
351
+ function daysToYMD(z) {
352
+ z += 719468;
353
+ const era = Math.floor(z / 146097);
354
+ const doe = z - era * 146097;
355
+ const yoe = Math.floor((doe - Math.floor(doe / 1460) + Math.floor(doe / 36524) - Math.floor(doe / 146096)) / 365);
356
+ const y = yoe + era * 400;
357
+ const doy = doe - (365 * yoe + Math.floor(yoe / 4) - Math.floor(yoe / 100));
358
+ const mp = Math.floor((5 * doy + 2) / 153);
359
+ const d = doy - Math.floor((153 * mp + 2) / 5) + 1;
360
+ const m = mp < 10 ? mp + 3 : mp - 9;
361
+ return { y: m <= 2 ? y + 1 : y, m, d };
362
+ }
363
+ function epochToDate(ts, tzOffsetMin) {
364
+ const { y, m, d } = daysToYMD(epochToDays(ts, tzOffsetMin));
365
+ return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
366
+ }
367
+ var WEEKDAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
368
+ var WEEKDAY_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
369
+ function hourLabel(h) {
370
+ const period = h < 12 ? "AM" : "PM";
371
+ const base = h % 12 === 0 ? 12 : h % 12;
372
+ return `${base} ${period}`;
373
+ }
374
+
375
+ // src/stats/time.ts
376
+ var NIGHT_HOURS = /* @__PURE__ */ new Set([22, 23, 0, 1, 2, 3, 4]);
377
+ function argmax(arr) {
378
+ let best = 0;
379
+ for (let i = 1; i < arr.length; i++) if (arr[i] > arr[best]) best = i;
380
+ return best;
381
+ }
382
+ function computeTimeStats(entries, tzOffsetMin) {
383
+ const byHour = new Array(24).fill(0);
384
+ const byWeekday = new Array(7).fill(0);
385
+ const byDate = /* @__PURE__ */ new Map();
386
+ let stamped = 0;
387
+ let night = 0;
388
+ let weekend = 0;
389
+ let firstTs;
390
+ let lastTs;
391
+ for (const e of entries) {
392
+ if (e.ts === void 0) continue;
393
+ stamped++;
394
+ const h = epochToHour(e.ts, tzOffsetMin);
395
+ const w = epochToWeekday(e.ts, tzOffsetMin);
396
+ byHour[h] = (byHour[h] ?? 0) + 1;
397
+ byWeekday[w] = (byWeekday[w] ?? 0) + 1;
398
+ if (NIGHT_HOURS.has(h)) night++;
399
+ if (w === 0 || w === 6) weekend++;
400
+ const date = epochToDate(e.ts, tzOffsetMin);
401
+ byDate.set(date, (byDate.get(date) ?? 0) + 1);
402
+ if (firstTs === void 0 || e.ts < firstTs) firstTs = e.ts;
403
+ if (lastTs === void 0 || e.ts > lastTs) lastTs = e.ts;
404
+ }
405
+ let busiestDay;
406
+ for (const [date, commands] of byDate) {
407
+ if (!busiestDay || commands > busiestDay.commands || commands === busiestDay.commands && date < busiestDay.date) {
408
+ busiestDay = { date, commands };
409
+ }
410
+ }
411
+ const n = stamped || 1;
412
+ return {
413
+ hasTimestamps: stamped > 0,
414
+ byHour,
415
+ byWeekday,
416
+ peakHour: argmax(byHour),
417
+ peakWeekday: argmax(byWeekday),
418
+ nightOwlPct: night / n * 100,
419
+ weekendPct: weekend / n * 100,
420
+ firstDate: firstTs === void 0 ? void 0 : epochToDate(firstTs, tzOffsetMin),
421
+ lastDate: lastTs === void 0 ? void 0 : epochToDate(lastTs, tzOffsetMin),
422
+ busiestDay
423
+ };
424
+ }
425
+
426
+ // src/stats/typos.ts
427
+ var KNOWN_TYPOS = {
428
+ gti: "git",
429
+ gut: "git",
430
+ igt: "git",
431
+ sl: "ls",
432
+ lls: "ls",
433
+ grpe: "grep",
434
+ gerp: "grep",
435
+ claer: "clear",
436
+ celar: "clear",
437
+ clera: "clear",
438
+ exti: "exit",
439
+ eixt: "exit",
440
+ pdw: "pwd",
441
+ mkdri: "mkdir",
442
+ mkidr: "mkdir",
443
+ suod: "sudo",
444
+ sduo: "sudo",
445
+ dokcer: "docker",
446
+ dcoker: "docker",
447
+ kubeclt: "kubectl",
448
+ kubctl: "kubectl",
449
+ pyhton: "python",
450
+ pythno: "python",
451
+ nmp: "npm",
452
+ yanr: "yarn",
453
+ vmi: "vim",
454
+ ivm: "vim",
455
+ nvmi: "nvim",
456
+ cd_: "cd",
457
+ "cd..": "cd ..",
458
+ shh: "ssh",
459
+ tial: "tail",
460
+ taill: "tail",
461
+ cta: "cat",
462
+ act: "cat",
463
+ hsitory: "history",
464
+ histroy: "history"
465
+ };
466
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
467
+ "ls",
468
+ "cd",
469
+ "cp",
470
+ "mv",
471
+ "rm",
472
+ "ln",
473
+ "cat",
474
+ "cal",
475
+ "sh",
476
+ "ssh",
477
+ "sed",
478
+ "set",
479
+ "see",
480
+ "pwd",
481
+ "ps",
482
+ "du",
483
+ "df",
484
+ "dd",
485
+ "id",
486
+ "bg",
487
+ "fg",
488
+ "jobs",
489
+ "top",
490
+ "htop",
491
+ "btop",
492
+ "git",
493
+ "gh",
494
+ "go",
495
+ "gd",
496
+ "gs",
497
+ "g",
498
+ "vi",
499
+ "vim",
500
+ "nvim",
501
+ "vd",
502
+ "ed",
503
+ "emacs",
504
+ "man",
505
+ "make",
506
+ "make",
507
+ "node",
508
+ "npm",
509
+ "npx",
510
+ "nvm",
511
+ "pnpm",
512
+ "yarn",
513
+ "bun",
514
+ "deno",
515
+ "python",
516
+ "python3",
517
+ "pip",
518
+ "pip3",
519
+ "ruby",
520
+ "gem",
521
+ "cargo",
522
+ "rustc",
523
+ "java",
524
+ "javac",
525
+ "docker",
526
+ "podman",
527
+ "kubectl",
528
+ "helm",
529
+ "k9s",
530
+ "k",
531
+ "terraform",
532
+ "ansible",
533
+ "curl",
534
+ "wget",
535
+ "http",
536
+ "jq",
537
+ "yq",
538
+ "fzf",
539
+ "rg",
540
+ "ag",
541
+ "fd",
542
+ "find",
543
+ "grep",
544
+ "egrep",
545
+ "awk",
546
+ "cut",
547
+ "sort",
548
+ "uniq",
549
+ "wc",
550
+ "xargs",
551
+ "tee",
552
+ "tr",
553
+ "head",
554
+ "tail",
555
+ "less",
556
+ "more",
557
+ "tar",
558
+ "zip",
559
+ "unzip",
560
+ "gzip",
561
+ "gunzip",
562
+ "xz",
563
+ "zstd",
564
+ "7z",
565
+ "kill",
566
+ "killall",
567
+ "pkill",
568
+ "pgrep",
569
+ "watch",
570
+ "cron",
571
+ "crontab",
572
+ "at",
573
+ "echo",
574
+ "printf",
575
+ "test",
576
+ "true",
577
+ "false",
578
+ "env",
579
+ "export",
580
+ "alias",
581
+ "source",
582
+ "which",
583
+ "whoami",
584
+ "who",
585
+ "w",
586
+ "uname",
587
+ "uptime",
588
+ "date",
589
+ "cal",
590
+ "clear",
591
+ "exit",
592
+ "history",
593
+ "chmod",
594
+ "chown",
595
+ "chgrp",
596
+ "touch",
597
+ "mkdir",
598
+ "rmdir",
599
+ "stat",
600
+ "file",
601
+ "diff",
602
+ "patch",
603
+ "mount",
604
+ "umount",
605
+ "sync",
606
+ "free",
607
+ "vmstat",
608
+ "iostat",
609
+ "lsof",
610
+ "dig",
611
+ "host",
612
+ "ping",
613
+ "traceroute",
614
+ "nc",
615
+ "nmap",
616
+ "rsync",
617
+ "scp",
618
+ "sftp",
619
+ "ftp",
620
+ "telnet",
621
+ "tmux",
622
+ "screen",
623
+ "brew",
624
+ "apt",
625
+ "dnf",
626
+ "yum",
627
+ "pacman",
628
+ "snap",
629
+ "flatpak",
630
+ "nix",
631
+ "code",
632
+ "subl",
633
+ "open",
634
+ "pbcopy",
635
+ "pbpaste",
636
+ "say",
637
+ "osascript",
638
+ "defaults",
639
+ "launchctl",
640
+ "systemctl",
641
+ "service",
642
+ "journalctl",
643
+ "dmesg",
644
+ "sudo",
645
+ "doas",
646
+ "su",
647
+ "passwd",
648
+ "useradd",
649
+ "usermod",
650
+ "ts",
651
+ "tsc",
652
+ "tsx",
653
+ "vite",
654
+ "next",
655
+ "nuxt",
656
+ "astro",
657
+ "eslint",
658
+ "prettier",
659
+ "jest",
660
+ "vitest",
661
+ "mysql",
662
+ "psql",
663
+ "sqlite3",
664
+ "redis-cli",
665
+ "mongo",
666
+ "mongosh"
667
+ ]);
668
+ function editDistance(a, b, max = 2) {
669
+ if (a === b) return 0;
670
+ const la = a.length;
671
+ const lb = b.length;
672
+ if (Math.abs(la - lb) > max) return max + 1;
673
+ let prev2 = null;
674
+ let prev = new Array(lb + 1);
675
+ let cur = new Array(lb + 1);
676
+ for (let j = 0; j <= lb; j++) prev[j] = j;
677
+ for (let i = 1; i <= la; i++) {
678
+ cur[0] = i;
679
+ let rowMin = i;
680
+ for (let j = 1; j <= lb; j++) {
681
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
682
+ let d = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
683
+ if (prev2 && i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
684
+ d = Math.min(d, prev2[j - 2] + 1);
685
+ }
686
+ cur[j] = d;
687
+ if (d < rowMin) rowMin = d;
688
+ }
689
+ if (rowMin > max) return max + 1;
690
+ prev2 = prev.slice();
691
+ [prev, cur] = [cur, prev];
692
+ }
693
+ return prev[lb];
694
+ }
695
+ function computeTypoStats(entries) {
696
+ const baseCounts = /* @__PURE__ */ new Map();
697
+ for (const e of entries) {
698
+ for (const s of e.segments) baseCounts.set(s.base, (baseCounts.get(s.base) ?? 0) + 1);
699
+ }
700
+ const hits = /* @__PURE__ */ new Map();
701
+ for (const [typo, target] of Object.entries(KNOWN_TYPOS)) {
702
+ const count = baseCounts.get(typo) ?? 0;
703
+ if (count > 0) hits.set(typo, { typo, target, count });
704
+ }
705
+ const top = [...baseCounts.entries()].filter(([b]) => !KNOWN_TYPOS[b]).sort((a, b) => b[1] - a[1]).slice(0, 20);
706
+ for (const [base, count] of baseCounts) {
707
+ if (hits.has(base)) continue;
708
+ if (KNOWN_COMMANDS.has(base)) continue;
709
+ if (base.length < 2) continue;
710
+ for (const [target, targetCount] of top) {
711
+ if (target === base) continue;
712
+ if (targetCount < 50) continue;
713
+ if (count > Math.max(3, targetCount * 0.01)) continue;
714
+ if (!KNOWN_COMMANDS.has(target) && targetCount < 200) continue;
715
+ if (editDistance(base, target, 1) === 1) {
716
+ hits.set(base, { typo: base, target, count });
717
+ break;
718
+ }
719
+ }
720
+ }
721
+ const list = [...hits.values()].sort((a, b) => b.count - a.count || a.typo.localeCompare(b.typo));
722
+ const total = list.reduce((s, h) => s + h.count, 0);
723
+ const wastedKeystrokes = list.reduce((s, h) => s + h.typo.length * h.count, 0);
724
+ return { hits: list, total, wastedKeystrokes };
725
+ }
726
+
727
+ // src/stats/aliases.ts
728
+ var SKIP_BASES = /* @__PURE__ */ new Set(["cd", "ls", "alias", "export", "source", "history", "man", "echo"]);
729
+ function suggestAliasName(command, taken) {
730
+ const words = command.split(/\s+/).filter((w) => w.length > 0 && !w.startsWith("-") && !/^[A-Z_]+=/.test(w) && !/^\d/.test(w)).slice(0, 4);
731
+ let name = words.map((w) => w[0].toLowerCase()).join("");
732
+ if (name.length === 0) name = command.slice(0, 2).toLowerCase();
733
+ if (!taken.has(name) && name.length >= 2) {
734
+ taken.add(name);
735
+ return name;
736
+ }
737
+ const second = words[1] ?? words[0] ?? "x";
738
+ for (let i = 1; i < second.length; i++) {
739
+ const cand = name + second[i];
740
+ if (!taken.has(cand)) {
741
+ taken.add(cand);
742
+ return cand;
743
+ }
744
+ }
745
+ for (let n = 2; n < 100; n++) {
746
+ const cand = name + n;
747
+ if (!taken.has(cand)) {
748
+ taken.add(cand);
749
+ return cand;
750
+ }
751
+ }
752
+ taken.add(name);
753
+ return name;
754
+ }
755
+ function computeAliasStats(entries, opts) {
756
+ const counts = /* @__PURE__ */ new Map();
757
+ for (const e of entries) {
758
+ const line = e.raw.trim().replace(/\s+/g, " ");
759
+ if (line.length < opts.minLength) continue;
760
+ if (line.includes("\n")) continue;
761
+ const first = e.segments[0];
762
+ if (!first) continue;
763
+ if (SKIP_BASES.has(first.base)) continue;
764
+ counts.set(line, (counts.get(line) ?? 0) + 1);
765
+ }
766
+ const candidates = [...counts.entries()].filter(([, c]) => c >= opts.minCount).map(([command, count]) => ({ command, count })).sort((a, b) => b.count * b.command.length - a.count * a.command.length || a.command.localeCompare(b.command));
767
+ const taken = /* @__PURE__ */ new Set();
768
+ const suggestions = [];
769
+ for (const { command, count } of candidates) {
770
+ const alias = suggestAliasName(command, taken);
771
+ const saves = Math.max(0, (command.length - alias.length) * count);
772
+ if (saves === 0) continue;
773
+ suggestions.push({ command, alias, count, saves });
774
+ }
775
+ suggestions.sort((a, b) => b.saves - a.saves || a.command.localeCompare(b.command));
776
+ const sliced = suggestions.slice(0, opts.top);
777
+ const totalSavable = suggestions.reduce((s, x) => s + x.saves, 0);
778
+ return { suggestions: sliced, totalSavable };
779
+ }
780
+
781
+ // src/stats/secrets.ts
782
+ var DETECTORS = [
783
+ { kind: "aws-access-key", re: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/ },
784
+ { kind: "github-token", re: /\b(gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/ },
785
+ { kind: "slack-token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/ },
786
+ { kind: "stripe-key", re: /\b[sr]k_live_[A-Za-z0-9]{16,}\b/ },
787
+ { kind: "npm-token", re: /\bnpm_[A-Za-z0-9]{30,}\b/ },
788
+ { kind: "openai-key", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
789
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/ },
790
+ { kind: "bearer-header", re: /\bAuthorization:\s*Bearer\s+[A-Za-z0-9._~+/-]{16,}=*/i },
791
+ // mysql-style -pSECRET / --password=SECRET / --password SECRET / --token=SECRET
792
+ { kind: "password-flag", re: /(?:^|\s)(?:-p[^\s'"=-][^\s'"]*|--(?:password|passwd|token|api-key|apikey)[= ][^\s'"]{4,})/ },
793
+ // PASSWORD=... / TOKEN=... / SECRET=... / API_KEY=... env assignments with a real-looking value
794
+ { kind: "password-env", re: /\b[A-Z0-9_]*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|APIKEY)=[^\s'"$]{6,}/ }
795
+ ];
796
+ function mask(value) {
797
+ const trimmed = value.trim();
798
+ const keep = Math.min(6, Math.max(4, Math.floor(trimmed.length / 6)));
799
+ return `${trimmed.slice(0, keep)}\u2026`;
800
+ }
801
+ function computeSecretHits(entries) {
802
+ const hits = [];
803
+ for (let i = 0; i < entries.length; i++) {
804
+ const raw = entries[i].raw;
805
+ for (const d of DETECTORS) {
806
+ const m = raw.match(d.re);
807
+ if (m) {
808
+ hits.push({ kind: d.kind, masked: mask(m[0]), entryIndex: i + 1 });
809
+ break;
810
+ }
811
+ }
812
+ }
813
+ return hits;
814
+ }
815
+ var SECRET_KIND_LABELS = {
816
+ "aws-access-key": "AWS access key",
817
+ "github-token": "GitHub token",
818
+ "slack-token": "Slack token",
819
+ "stripe-key": "Stripe live key",
820
+ "npm-token": "npm token",
821
+ "openai-key": "API key (sk-\u2026)",
822
+ jwt: "JWT",
823
+ "bearer-header": "Bearer token in a header",
824
+ "password-flag": "password passed as a flag",
825
+ "password-env": "secret in an inline env var"
826
+ };
827
+
828
+ // src/stats/persona.ts
829
+ var PERSONAS = {
830
+ nightOwl: {
831
+ id: "night-owl",
832
+ title: "The Night Owl",
833
+ emoji: "\u{1F989}",
834
+ blurb: "Your terminal sees more moonlight than sunlight."
835
+ },
836
+ pipeWizard: {
837
+ id: "pipe-wizard",
838
+ title: "The Pipe Wizard",
839
+ emoji: "\u{1F9D9}",
840
+ blurb: "Why run one command when five can hold hands?"
841
+ },
842
+ sudoSummoner: {
843
+ id: "sudo-summoner",
844
+ title: "The Sudo Summoner",
845
+ emoji: "\u{1F531}",
846
+ blurb: "Asking for forgiveness, never for permission."
847
+ },
848
+ gitGremlin: {
849
+ id: "git-gremlin",
850
+ title: "The Git Gremlin",
851
+ emoji: "\u{1F419}",
852
+ blurb: "Your shell is basically a git client with extra steps."
853
+ },
854
+ containerCaptain: {
855
+ id: "container-captain",
856
+ title: "The Container Captain",
857
+ emoji: "\u{1F433}",
858
+ blurb: "Everything's fine \u2014 it works in the container."
859
+ },
860
+ vimMonk: {
861
+ id: "vim-monk",
862
+ title: "The Vim Monk",
863
+ emoji: "\u{1F9D8}",
864
+ blurb: "You live inside the editor. The shell is just the hallway."
865
+ },
866
+ typoArtist: {
867
+ id: "typo-artist",
868
+ title: "The Typo Artist",
869
+ emoji: "\u{1F3A8}",
870
+ blurb: "gti, sl, dokcer \u2014 close enough, every time."
871
+ },
872
+ navigator: {
873
+ id: "navigator",
874
+ title: "The Navigator",
875
+ emoji: "\u{1F9ED}",
876
+ blurb: "cd, ls, cd, ls \u2014 forever exploring, never lost (mostly)."
877
+ },
878
+ polyglot: {
879
+ id: "polyglot",
880
+ title: "The Polyglot",
881
+ emoji: "\u{1F310}",
882
+ blurb: "Is there a CLI you haven't tried?"
883
+ },
884
+ steadyOperator: {
885
+ id: "steady-operator",
886
+ title: "The Steady Operator",
887
+ emoji: "\u{1F39B}\uFE0F",
888
+ blurb: "Calm, consistent, quietly unstoppable."
889
+ }
890
+ };
891
+ function share(top, totalSegments, names) {
892
+ if (totalSegments === 0) return 0;
893
+ const set = new Set(names);
894
+ let n = 0;
895
+ for (const c of top) if (set.has(c.name)) n += c.count;
896
+ return n / totalSegments * 100;
897
+ }
898
+ function derivePersona(input, allBases) {
899
+ const { totals, time, typos } = input;
900
+ const entries = totals.entries || 1;
901
+ const gitShare = share(allBases, totals.segments, ["git"]);
902
+ const containerShare = share(allBases, totals.segments, ["docker", "podman", "kubectl", "helm", "docker-compose", "k9s"]);
903
+ const vimShare = share(allBases, totals.segments, ["vim", "nvim", "vi"]);
904
+ const navShare = share(allBases, totals.segments, ["cd", "ls", "ll", "la", "z", "pwd"]);
905
+ if (time.hasTimestamps && time.nightOwlPct >= 35) return PERSONAS.nightOwl;
906
+ if (typos.total / entries >= 0.015 && typos.total >= 20) return PERSONAS.typoArtist;
907
+ if (totals.pipePct >= 12) return PERSONAS.pipeWizard;
908
+ if (totals.sudoPct >= 8) return PERSONAS.sudoSummoner;
909
+ if (gitShare >= 25) return PERSONAS.gitGremlin;
910
+ if (containerShare >= 15) return PERSONAS.containerCaptain;
911
+ if (vimShare >= 10) return PERSONAS.vimMonk;
912
+ if (navShare >= 30) return PERSONAS.navigator;
913
+ if (totals.uniqueBases >= 300) return PERSONAS.polyglot;
914
+ return PERSONAS.steadyOperator;
915
+ }
916
+
917
+ // src/analyze.ts
918
+ function topOf(map, n) {
919
+ return [...map.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)).slice(0, n);
920
+ }
921
+ function analyze(rawEntries, config, ctx) {
922
+ const ignore = new Set(config.ignoreCommands.map((c) => c.toLowerCase()));
923
+ const entries = [];
924
+ for (const e of rawEntries) {
925
+ const p = tokenizeEntry(e);
926
+ if (p.segments.length === 0) continue;
927
+ if (p.segments.every((s) => ignore.has(s.base))) continue;
928
+ entries.push(p);
929
+ }
930
+ const baseCounts = /* @__PURE__ */ new Map();
931
+ const subCounts = /* @__PURE__ */ new Map();
932
+ const uniqueLines = /* @__PURE__ */ new Set();
933
+ let segments = 0;
934
+ let piped = 0;
935
+ let sudoed = 0;
936
+ let totalLen = 0;
937
+ for (const e of entries) {
938
+ uniqueLines.add(e.raw.trim());
939
+ totalLen += e.raw.length;
940
+ if (e.pipes > 0) piped++;
941
+ let sawSudo = false;
942
+ for (const s of e.segments) {
943
+ segments++;
944
+ if (ignore.has(s.base)) continue;
945
+ baseCounts.set(s.base, (baseCounts.get(s.base) ?? 0) + 1);
946
+ if (s.withSub !== s.base) subCounts.set(s.withSub, (subCounts.get(s.withSub) ?? 0) + 1);
947
+ if (s.sudo) sawSudo = true;
948
+ }
949
+ if (sawSudo) sudoed++;
950
+ }
951
+ const n = entries.length || 1;
952
+ const totals = {
953
+ entries: entries.length,
954
+ segments,
955
+ uniqueCommands: uniqueLines.size,
956
+ uniqueBases: baseCounts.size,
957
+ pipePct: piped / n * 100,
958
+ sudoPct: sudoed / n * 100,
959
+ avgLength: totalLen / n
960
+ };
961
+ const time = computeTimeStats(entries, ctx.tzOffsetMin);
962
+ const typos = computeTypoStats(entries);
963
+ const aliases = computeAliasStats(entries, {
964
+ minCount: config.aliasMinCount,
965
+ minLength: config.aliasMinLength,
966
+ top: config.top
967
+ });
968
+ const secrets = config.secretScan ? computeSecretHits(entries) : [];
969
+ const topCommands = topOf(baseCounts, config.top);
970
+ const topSubcommands = topOf(subCounts, config.top);
971
+ const allBases = topOf(baseCounts, baseCounts.size);
972
+ const persona = derivePersona({ totals, time, typos, topCommands }, allBases);
973
+ return {
974
+ source: { label: ctx.sourceLabel, format: ctx.format },
975
+ totals,
976
+ time,
977
+ topCommands,
978
+ topSubcommands,
979
+ typos,
980
+ aliases,
981
+ secrets,
982
+ persona
983
+ };
984
+ }
985
+
986
+ // src/format.ts
987
+ function num(n) {
988
+ const neg = n < 0;
989
+ const s = Math.abs(Math.round(n)).toString();
990
+ let out = "";
991
+ for (let i = 0; i < s.length; i++) {
992
+ if (i > 0 && (s.length - i) % 3 === 0) out += ",";
993
+ out += s[i];
994
+ }
995
+ return neg ? `-${out}` : out;
996
+ }
997
+ function pct(n) {
998
+ return `${Math.round(n)}%`;
999
+ }
1000
+ function bar(value, max, width, full = "\u2588", empty = " ") {
1001
+ if (max <= 0 || width <= 0) return empty.repeat(Math.max(0, width));
1002
+ const filled = Math.round(value / max * width);
1003
+ const clamped = Math.max(value > 0 ? 1 : 0, Math.min(width, filled));
1004
+ return full.repeat(clamped) + empty.repeat(width - clamped);
1005
+ }
1006
+ function ellipsize(s, max) {
1007
+ if (s.length <= max) return s;
1008
+ if (max <= 1) return s.slice(0, max);
1009
+ return s.slice(0, max - 1) + "\u2026";
1010
+ }
1011
+ function xmlEscape(s) {
1012
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1013
+ }
1014
+
1015
+ // src/card.ts
1016
+ var THEMES = {
1017
+ tokyonight: {
1018
+ bgTop: "#1a1b26",
1019
+ bgBottom: "#24283b",
1020
+ text: "#c0caf5",
1021
+ dim: "#565f89",
1022
+ accent: "#7aa2f7",
1023
+ accent2: "#bb9af7",
1024
+ card: "#1f2335"
1025
+ },
1026
+ dark: {
1027
+ bgTop: "#0d1117",
1028
+ bgBottom: "#161b22",
1029
+ text: "#e6edf3",
1030
+ dim: "#8b949e",
1031
+ accent: "#58a6ff",
1032
+ accent2: "#3fb950",
1033
+ card: "#161b22"
1034
+ },
1035
+ light: {
1036
+ bgTop: "#ffffff",
1037
+ bgBottom: "#eef2f7",
1038
+ text: "#111827",
1039
+ dim: "#6b7280",
1040
+ accent: "#2563eb",
1041
+ accent2: "#7c3aed",
1042
+ card: "#f3f4f6"
1043
+ },
1044
+ candy: {
1045
+ bgTop: "#ff6ec4",
1046
+ bgBottom: "#7873f5",
1047
+ text: "#ffffff",
1048
+ dim: "#ffe3f4",
1049
+ accent: "#ffffff",
1050
+ accent2: "#fff27a",
1051
+ card: "rgba(255,255,255,0.14)"
1052
+ }
1053
+ };
1054
+ var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif";
1055
+ var MONO = "'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, Consolas, monospace";
1056
+ var W = 1080;
1057
+ var H = 1350;
1058
+ var PAD = 80;
1059
+ function text(x, y, content, o) {
1060
+ return `<text x="${x}" y="${y}" font-family="${o.family ?? FONT}" font-size="${o.size}" font-weight="${o.weight ?? 400}" fill="${o.fill}" text-anchor="${o.anchor ?? "start"}">${xmlEscape(content)}</text>`;
1061
+ }
1062
+ function rect(x, y, w, h, rx, fill) {
1063
+ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="${fill}"/>`;
1064
+ }
1065
+ function statCard(x, y, w, h, p, value, label) {
1066
+ return rect(x, y, w, h, 22, p.card) + text(x + w / 2, y + 74, value, { size: 50, weight: 800, fill: p.text, anchor: "middle", family: MONO }) + text(x + w / 2, y + 116, label, { size: 23, weight: 500, fill: p.dim, anchor: "middle" });
1067
+ }
1068
+ function renderCard(report, theme = "tokyonight") {
1069
+ const p = THEMES[theme] ?? THEMES.tokyonight;
1070
+ const t = report.totals;
1071
+ const parts = [];
1072
+ parts.push(`<defs>
1073
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
1074
+ <stop offset="0%" stop-color="${p.bgTop}"/>
1075
+ <stop offset="100%" stop-color="${p.bgBottom}"/>
1076
+ </linearGradient>
1077
+ <radialGradient id="glow" cx="80%" cy="8%" r="60%">
1078
+ <stop offset="0%" stop-color="${p.accent}" stop-opacity="0.22"/>
1079
+ <stop offset="100%" stop-color="${p.accent}" stop-opacity="0"/>
1080
+ </radialGradient>
1081
+ </defs>`);
1082
+ parts.push(`<rect width="${W}" height="${H}" fill="url(#bg)"/>`);
1083
+ parts.push(`<rect width="${W}" height="${H}" fill="url(#glow)"/>`);
1084
+ parts.push(text(PAD, 92, "\u276F shellrecap", { size: 30, weight: 800, fill: p.text, family: MONO }));
1085
+ parts.push(text(W - PAD, 92, "\u{1F512} nothing uploaded", { size: 24, weight: 500, fill: p.dim, anchor: "end" }));
1086
+ const range = report.time.hasTimestamps && report.time.firstDate ? `${report.time.firstDate} \u2192 ${report.time.lastDate}` : "your shell history";
1087
+ parts.push(text(PAD, 182, range, { size: 40, weight: 700, fill: p.text }));
1088
+ parts.push(text(PAD, 326, report.persona.emoji, { size: 118, fill: p.text }));
1089
+ parts.push(text(PAD + 168, 296, report.persona.title, { size: 58, weight: 800, fill: p.accent }));
1090
+ parts.push(text(PAD + 168, 344, ellipsize(report.persona.blurb, 58), { size: 24, weight: 500, fill: p.dim }));
1091
+ parts.push(text(PAD, 466, num(t.entries), { size: 104, weight: 800, fill: p.text, family: MONO }));
1092
+ parts.push(text(PAD + 8, 514, "commands typed", { size: 29, weight: 600, fill: p.dim }));
1093
+ const wasted = report.typos.wastedKeystrokes;
1094
+ parts.push(
1095
+ text(W - PAD, 436, num(t.uniqueBases), { size: 44, weight: 800, fill: p.accent, anchor: "end", family: MONO }) + text(W - PAD, 470, "different programs", { size: 22, weight: 500, fill: p.dim, anchor: "end" }) + text(W - PAD, 514, wasted > 0 ? `${num(wasted)} keys lost to typos` : `${num(t.uniqueCommands)} unique lines`, {
1096
+ size: 22,
1097
+ weight: 600,
1098
+ fill: p.accent2,
1099
+ anchor: "end"
1100
+ })
1101
+ );
1102
+ const chartTop = 596;
1103
+ parts.push(text(PAD, chartTop, "Top commands", { size: 28, weight: 700, fill: p.text }));
1104
+ const tops = report.topCommands.slice(0, 5);
1105
+ const maxCount = tops[0]?.count ?? 1;
1106
+ const rowH = 56;
1107
+ const barMax = W - PAD * 2 - 320;
1108
+ tops.forEach((c, i) => {
1109
+ const y = chartTop + 46 + i * rowH;
1110
+ const bw = Math.max(8, Math.round(c.count / maxCount * barMax));
1111
+ parts.push(text(PAD, y + 27, ellipsize(c.name, 13), { size: 26, weight: 700, fill: p.text, family: MONO }));
1112
+ parts.push(`<rect x="${PAD + 220}" y="${y}" width="${bw}" height="36" rx="9" fill="${i === 0 ? p.accent2 : p.accent}" opacity="${i === 0 ? 1 : 0.85 - i * 0.12}"/>`);
1113
+ parts.push(text(PAD + 232 + bw, y + 27, num(c.count), { size: 24, weight: 600, fill: p.dim, family: MONO }));
1114
+ });
1115
+ const gridTop = chartTop + 46 + 5 * rowH + 40;
1116
+ const gap = 24;
1117
+ const cw = (W - PAD * 2 - gap) / 2;
1118
+ const ch = 140;
1119
+ const peak = report.time.hasTimestamps ? hourLabel(report.time.peakHour) : "\u2014";
1120
+ const day = report.time.hasTimestamps ? WEEKDAY_SHORT[report.time.peakWeekday] ?? "\u2014" : "\u2014";
1121
+ parts.push(statCard(PAD, gridTop, cw, ch, p, peak, "peak hour"));
1122
+ parts.push(statCard(PAD + cw + gap, gridTop, cw, ch, p, day, "favorite day"));
1123
+ parts.push(statCard(PAD, gridTop + ch + gap, cw, ch, p, `${Math.round(t.sudoPct)}%`, "sudo rate"));
1124
+ parts.push(
1125
+ statCard(
1126
+ PAD + cw + gap,
1127
+ gridTop + ch + gap,
1128
+ cw,
1129
+ ch,
1130
+ p,
1131
+ report.aliases.totalSavable > 0 ? num(report.aliases.totalSavable) : num(t.uniqueCommands),
1132
+ report.aliases.totalSavable > 0 ? "keystrokes an alias would save" : "unique command lines"
1133
+ )
1134
+ );
1135
+ parts.push(`<line x1="${PAD}" y1="${H - 64}" x2="${W - PAD}" y2="${H - 64}" stroke="${p.dim}" stroke-opacity="0.3" stroke-width="1"/>`);
1136
+ parts.push(text(PAD, H - 32, "what does your terminal say about you?", { size: 24, weight: 500, fill: p.dim }));
1137
+ parts.push(text(W - PAD, H - 32, "github.com/didrod205/shellrecap", { size: 24, weight: 500, fill: p.dim, anchor: "end" }));
1138
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="shellrecap card">${parts.join("")}</svg>`;
1139
+ }
1140
+
1141
+ // src/report/json.ts
1142
+ function toJSON(report) {
1143
+ return JSON.stringify(report, null, 2);
1144
+ }
1145
+
1146
+ // src/report/markdown.ts
1147
+ function toMarkdown(r) {
1148
+ const L = [];
1149
+ const t = r.totals;
1150
+ L.push(`# \u276F shellrecap \u2014 ${r.source.label}`);
1151
+ L.push("");
1152
+ L.push(`> **${r.persona.emoji} ${r.persona.title}** \u2014 ${r.persona.blurb}`);
1153
+ L.push("");
1154
+ L.push(
1155
+ `**${num(t.entries)} commands** \xB7 ${num(t.uniqueCommands)} unique lines \xB7 ${num(t.uniqueBases)} different programs` + (r.time.hasTimestamps && r.time.firstDate ? ` \xB7 ${r.time.firstDate} \u2192 ${r.time.lastDate}` : "")
1156
+ );
1157
+ L.push("");
1158
+ if (r.topCommands.length > 0) {
1159
+ L.push("## Top commands");
1160
+ L.push("");
1161
+ L.push("| Command | Count | |");
1162
+ L.push("| ------- | ----: | --- |");
1163
+ const max = r.topCommands[0].count;
1164
+ for (const c of r.topCommands) {
1165
+ L.push(`| \`${c.name}\` | ${num(c.count)} | \`${bar(c.count, max, 18, "\u2588", "\xB7")}\` |`);
1166
+ }
1167
+ L.push("");
1168
+ }
1169
+ if (r.topSubcommands.length > 0) {
1170
+ L.push("**Favorite moves:** " + r.topSubcommands.slice(0, 6).map((s) => `\`${s.name}\` \xD7${num(s.count)}`).join(" \xB7 "));
1171
+ L.push("");
1172
+ }
1173
+ if (r.time.hasTimestamps) {
1174
+ L.push("## When you type");
1175
+ L.push("");
1176
+ L.push(`Peak hour **${hourLabel(r.time.peakHour)}**, favorite day **${WEEKDAY_NAMES[r.time.peakWeekday]}**, night-owl share ${pct(r.time.nightOwlPct)}, weekends ${pct(r.time.weekendPct)}.`);
1177
+ if (r.time.busiestDay) {
1178
+ L.push(`Busiest day: **${r.time.busiestDay.date}** (${num(r.time.busiestDay.commands)} commands).`);
1179
+ }
1180
+ L.push("");
1181
+ }
1182
+ if (r.typos.hits.length > 0) {
1183
+ L.push("## Typos");
1184
+ L.push("");
1185
+ L.push(r.typos.hits.slice(0, 8).map((h) => `\`${h.typo}\`\u2192\`${h.target}\` \xD7${num(h.count)}`).join(" \xB7 "));
1186
+ L.push("");
1187
+ L.push(`*${num(r.typos.total)} mistyped commands, ${num(r.typos.wastedKeystrokes)} wasted keystrokes.*`);
1188
+ L.push("");
1189
+ }
1190
+ if (r.aliases.suggestions.length > 0) {
1191
+ L.push("## Aliases you're owed");
1192
+ L.push("");
1193
+ L.push("```sh");
1194
+ for (const a of r.aliases.suggestions.slice(0, 5)) {
1195
+ L.push(`alias ${a.alias}='${a.command}' # typed ${num(a.count)}\xD7 \u2014 saves ${num(a.saves)} keystrokes`);
1196
+ }
1197
+ L.push("```");
1198
+ L.push("");
1199
+ L.push(`*Total savable: **${num(r.aliases.totalSavable)} keystrokes**.*`);
1200
+ L.push("");
1201
+ }
1202
+ if (r.secrets.length > 0) {
1203
+ L.push("## \u26A0 Possible secrets in your history");
1204
+ L.push("");
1205
+ for (const s of r.secrets.slice(0, 10)) {
1206
+ L.push(`- \`${s.masked}\` \u2014 ${SECRET_KIND_LABELS[s.kind]} (entry #${num(s.entryIndex)})`);
1207
+ }
1208
+ L.push("");
1209
+ L.push("*Values are masked. Edit your history file to remove these lines.*");
1210
+ L.push("");
1211
+ }
1212
+ L.push("---");
1213
+ L.push("");
1214
+ L.push(`<sub>sudo ${pct(t.sudoPct)} \xB7 pipes ${pct(t.pipePct)} \xB7 generated locally by [shellrecap](https://github.com/didrod205/shellrecap) \u2014 nothing was uploaded.</sub>`);
1215
+ return L.join("\n") + "\n";
1216
+ }
1217
+
1218
+ // src/config.ts
1219
+ var DEFAULT_CONFIG = {
1220
+ theme: "tokyonight",
1221
+ top: 8,
1222
+ aliasMinCount: 10,
1223
+ aliasMinLength: 12,
1224
+ ignoreCommands: [],
1225
+ secretScan: true
1226
+ };
1227
+ var CONFIG_FILENAMES = ["shellrecap.config.json", ".shellrecaprc.json", ".shellrecaprc"];
1228
+ var THEMES2 = ["tokyonight", "dark", "light", "candy"];
1229
+ function asStringArray(v) {
1230
+ if (!Array.isArray(v)) return void 0;
1231
+ return v.filter((x) => typeof x === "string");
1232
+ }
1233
+ function asClampedInt(v, lo, hi) {
1234
+ if (typeof v !== "number" || !Number.isFinite(v)) return void 0;
1235
+ return Math.max(lo, Math.min(hi, Math.floor(v)));
1236
+ }
1237
+ function parseConfig(raw) {
1238
+ if (!raw || typeof raw !== "object") return {};
1239
+ const o = raw;
1240
+ const out = {};
1241
+ if (typeof o["theme"] === "string" && THEMES2.includes(o["theme"])) {
1242
+ out.theme = o["theme"];
1243
+ }
1244
+ const top = asClampedInt(o["top"], 1, 50);
1245
+ if (top !== void 0) out.top = top;
1246
+ const minCount = asClampedInt(o["aliasMinCount"], 2, 1e4);
1247
+ if (minCount !== void 0) out.aliasMinCount = minCount;
1248
+ const minLength = asClampedInt(o["aliasMinLength"], 4, 500);
1249
+ if (minLength !== void 0) out.aliasMinLength = minLength;
1250
+ const ignore = asStringArray(o["ignoreCommands"]);
1251
+ if (ignore) out.ignoreCommands = ignore;
1252
+ if (typeof o["secretScan"] === "boolean") out.secretScan = o["secretScan"];
1253
+ return out;
1254
+ }
1255
+ function mergeConfig(base, override) {
1256
+ return {
1257
+ theme: override.theme ?? base.theme,
1258
+ top: override.top ?? base.top,
1259
+ aliasMinCount: override.aliasMinCount ?? base.aliasMinCount,
1260
+ aliasMinLength: override.aliasMinLength ?? base.aliasMinLength,
1261
+ ignoreCommands: override.ignoreCommands ?? base.ignoreCommands,
1262
+ secretScan: override.secretScan ?? base.secretScan
1263
+ };
1264
+ }
1265
+ function isValidTheme(t) {
1266
+ return THEMES2.includes(t);
1267
+ }
1268
+ // Annotate the CommonJS export names for ESM import in node:
1269
+ 0 && (module.exports = {
1270
+ CONFIG_FILENAMES,
1271
+ DEFAULT_CONFIG,
1272
+ KNOWN_COMMANDS,
1273
+ KNOWN_TYPOS,
1274
+ PERSONAS,
1275
+ SECRET_KIND_LABELS,
1276
+ WEEKDAY_NAMES,
1277
+ WEEKDAY_SHORT,
1278
+ analyze,
1279
+ computeAliasStats,
1280
+ computeSecretHits,
1281
+ computeTimeStats,
1282
+ computeTypoStats,
1283
+ daysToYMD,
1284
+ derivePersona,
1285
+ detectFormat,
1286
+ editDistance,
1287
+ epochToDate,
1288
+ epochToHour,
1289
+ epochToWeekday,
1290
+ hourLabel,
1291
+ isValidTheme,
1292
+ mask,
1293
+ mergeConfig,
1294
+ parseBash,
1295
+ parseConfig,
1296
+ parseFish,
1297
+ parseHistory,
1298
+ parsePlain,
1299
+ parseZshExtended,
1300
+ renderCard,
1301
+ splitSegments,
1302
+ suggestAliasName,
1303
+ toJSON,
1304
+ toMarkdown,
1305
+ toSegment,
1306
+ tokenizeEntry,
1307
+ tokens
1308
+ });