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