@alint-js/cli 0.0.35 → 0.0.37
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/bin/index.mjs +1 -1
- package/dist/{cli-CZbrV2gX.mjs → cli-Dm3rTcjL.mjs} +425 -62
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/dist/bin/index.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import Gitignore from "gitignore-fs";
|
|
|
16
16
|
import { Minimatch, minimatch } from "minimatch";
|
|
17
17
|
import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
|
|
18
18
|
//#region package.json
|
|
19
|
-
var version = "0.0.
|
|
19
|
+
var version = "0.0.37";
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/cli/commands/command.ts
|
|
22
22
|
function defineCommand(node) {
|
|
@@ -67,13 +67,14 @@ function formatCommandHelp(sections, nodes, argv, rootHelp) {
|
|
|
67
67
|
body: commands.map((command) => ` ${command.pattern.padEnd(longestCommand)} ${command.description}`).join("\n"),
|
|
68
68
|
title: "Commands"
|
|
69
69
|
};
|
|
70
|
+
const optionSection = formatOptionsSection(node.options ?? []);
|
|
70
71
|
const usageIndex = normalizedSections.findIndex((section) => section.title === "Usage");
|
|
71
|
-
|
|
72
|
-
return [
|
|
72
|
+
const withCommands = usageIndex === -1 ? [commandSection, ...normalizedSections] : [
|
|
73
73
|
...normalizedSections.slice(0, usageIndex + 1),
|
|
74
74
|
commandSection,
|
|
75
75
|
...normalizedSections.slice(usageIndex + 1)
|
|
76
76
|
];
|
|
77
|
+
return optionSection ? [...withCommands, optionSection] : withCommands;
|
|
77
78
|
}
|
|
78
79
|
function formatExamples(examples) {
|
|
79
80
|
return examples.map((example) => example.split("\n").map((line) => ` ${line}`).join("\n")).join("\n\n");
|
|
@@ -193,11 +194,14 @@ function shouldSkipOptionValue(arg) {
|
|
|
193
194
|
"--config",
|
|
194
195
|
"--cwd",
|
|
195
196
|
"--format",
|
|
197
|
+
"--interval",
|
|
198
|
+
"--metric",
|
|
196
199
|
"--model",
|
|
197
200
|
"--provider-endpoint",
|
|
198
201
|
"--provider-header",
|
|
199
202
|
"--provider-id",
|
|
200
203
|
"--provider-model",
|
|
204
|
+
"--rule",
|
|
201
205
|
"--rule-concurrency",
|
|
202
206
|
"--since",
|
|
203
207
|
"--timeout-ms",
|
|
@@ -1386,20 +1390,29 @@ function isCi(env = process.env) {
|
|
|
1386
1390
|
//#region src/cli/stats/since.ts
|
|
1387
1391
|
/**
|
|
1388
1392
|
* Parse a `--since` time into an inclusive lower-bound timestamp.
|
|
1389
|
-
* This is a tiny helper only to meet the needs.
|
|
1390
1393
|
*
|
|
1391
1394
|
* Supported formats:
|
|
1392
|
-
* - `Nd`/`
|
|
1393
|
-
*
|
|
1395
|
+
* - `Nh`/`Nd`/`Nw`/`Nm`/`Ny` — now minus N hours/days/weeks/months/years.
|
|
1396
|
+
* Weeks, months, and years are fixed approximations (7/30/365 days). `m` is
|
|
1397
|
+
* months here, not minutes — a day is the finest window this tool tracks.
|
|
1398
|
+
* - `YYYY-MM`/`YYYY-MM-DD` — start of that UTC month/day.
|
|
1394
1399
|
*
|
|
1395
1400
|
* Edge cases:
|
|
1396
1401
|
* - An empty time means "no lower bound".
|
|
1397
|
-
* - An unrecognized time
|
|
1402
|
+
* - An unrecognized time throws.
|
|
1398
1403
|
*/
|
|
1404
|
+
const HOUR_MS = 36e5;
|
|
1405
|
+
const UNIT_MS = {
|
|
1406
|
+
d: 24 * HOUR_MS,
|
|
1407
|
+
h: HOUR_MS,
|
|
1408
|
+
m: 720 * HOUR_MS,
|
|
1409
|
+
w: 168 * HOUR_MS,
|
|
1410
|
+
y: 365 * 24 * HOUR_MS
|
|
1411
|
+
};
|
|
1399
1412
|
function parseSince(time, now) {
|
|
1400
1413
|
if (time === void 0 || time === "") return;
|
|
1401
|
-
const relative = /^(\d+)([
|
|
1402
|
-
if (relative) return now - Number(relative[1]) *
|
|
1414
|
+
const relative = /^(\d+)([dhmwy])$/u.exec(time);
|
|
1415
|
+
if (relative) return now - Number(relative[1]) * UNIT_MS[relative[2]];
|
|
1403
1416
|
const calendar = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/u.exec(time);
|
|
1404
1417
|
if (calendar) {
|
|
1405
1418
|
const year = Number(calendar[1]);
|
|
@@ -1407,16 +1420,32 @@ function parseSince(time, now) {
|
|
|
1407
1420
|
const day = calendar[3] === void 0 ? 1 : Number(calendar[3]);
|
|
1408
1421
|
return Date.UTC(year, month - 1, day);
|
|
1409
1422
|
}
|
|
1410
|
-
throw new Error(`Invalid --since "${time}": use e.g. 7d,
|
|
1423
|
+
throw new Error(`Invalid --since "${time}": use e.g. 24h, 7d, 2w, 3m, 1y, 2025-01, or 2025-01-15.`);
|
|
1411
1424
|
}
|
|
1412
1425
|
//#endregion
|
|
1413
1426
|
//#region src/cli/stats/jsonl.ts
|
|
1414
1427
|
const DEFAULT_RETENTION_MONTHS = 12;
|
|
1415
1428
|
const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
|
|
1429
|
+
const DAY_MS = 864e5;
|
|
1430
|
+
const MONTH_LABELS$1 = [
|
|
1431
|
+
"Jan",
|
|
1432
|
+
"Feb",
|
|
1433
|
+
"Mar",
|
|
1434
|
+
"Apr",
|
|
1435
|
+
"May",
|
|
1436
|
+
"Jun",
|
|
1437
|
+
"Jul",
|
|
1438
|
+
"Aug",
|
|
1439
|
+
"Sep",
|
|
1440
|
+
"Oct",
|
|
1441
|
+
"Nov",
|
|
1442
|
+
"Dec"
|
|
1443
|
+
];
|
|
1416
1444
|
function createJsonlStatsStore(options) {
|
|
1417
1445
|
const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
|
|
1418
1446
|
return {
|
|
1419
1447
|
query: (filter) => query(options.dir, filter ?? {}, Date.now),
|
|
1448
|
+
querySeries: (filter) => querySeries(options.dir, filter ?? {}, Date.now),
|
|
1420
1449
|
record: (input) => record(options.dir, input, Date.now(), retentionMonths)
|
|
1421
1450
|
};
|
|
1422
1451
|
}
|
|
@@ -1433,6 +1462,45 @@ function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
|
|
|
1433
1462
|
row.totalTok += totalTok;
|
|
1434
1463
|
if (countRun) row.runs += 1;
|
|
1435
1464
|
rows.set(key, row);
|
|
1465
|
+
return row;
|
|
1466
|
+
}
|
|
1467
|
+
function bucketLabel(startMs, interval) {
|
|
1468
|
+
const date = new Date(startMs);
|
|
1469
|
+
if (interval === "month") return MONTH_LABELS$1[date.getUTCMonth()];
|
|
1470
|
+
return `${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
1471
|
+
}
|
|
1472
|
+
function bucketStart(ts, interval) {
|
|
1473
|
+
const date = new Date(ts);
|
|
1474
|
+
if (interval === "month") return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
|
|
1475
|
+
const dayStart = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
1476
|
+
if (interval === "week") return dayStart - (new Date(dayStart).getUTCDay() + 6) % 7 * DAY_MS;
|
|
1477
|
+
return dayStart;
|
|
1478
|
+
}
|
|
1479
|
+
function chooseInterval(since, nowMs, timestamps) {
|
|
1480
|
+
if (since !== void 0) return intervalForDuration(nowMs - since);
|
|
1481
|
+
if (timestamps.length < 2) return "day";
|
|
1482
|
+
return intervalForDuration(Math.max(...timestamps) - Math.min(...timestamps));
|
|
1483
|
+
}
|
|
1484
|
+
function fillBuckets(buckets, interval) {
|
|
1485
|
+
if (buckets.size === 0) return [];
|
|
1486
|
+
const starts = [...buckets.keys()].sort((left, right) => left - right);
|
|
1487
|
+
const last = starts.at(-1);
|
|
1488
|
+
const filled = [];
|
|
1489
|
+
for (let startMs = starts[0]; startMs <= last; startMs = nextBucketStart(startMs, interval)) filled.push(buckets.get(startMs) ?? {
|
|
1490
|
+
inTok: 0,
|
|
1491
|
+
key: bucketLabel(startMs, interval),
|
|
1492
|
+
outTok: 0,
|
|
1493
|
+
runs: 0,
|
|
1494
|
+
startMs,
|
|
1495
|
+
totalTok: 0
|
|
1496
|
+
});
|
|
1497
|
+
return filled;
|
|
1498
|
+
}
|
|
1499
|
+
function intervalForDuration(ms) {
|
|
1500
|
+
const days = ms / DAY_MS;
|
|
1501
|
+
if (days <= 21) return "day";
|
|
1502
|
+
if (days <= 120) return "week";
|
|
1503
|
+
return "month";
|
|
1436
1504
|
}
|
|
1437
1505
|
function isEnoent(error) {
|
|
1438
1506
|
return error instanceof Error && error.code === "ENOENT";
|
|
@@ -1441,6 +1509,13 @@ function monthIndexOf(ts) {
|
|
|
1441
1509
|
const date = new Date(ts);
|
|
1442
1510
|
return date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
1443
1511
|
}
|
|
1512
|
+
function nextBucketStart(startMs, interval) {
|
|
1513
|
+
if (interval === "month") {
|
|
1514
|
+
const date = new Date(startMs);
|
|
1515
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
|
|
1516
|
+
}
|
|
1517
|
+
return startMs + (interval === "week" ? 7 * DAY_MS : DAY_MS);
|
|
1518
|
+
}
|
|
1444
1519
|
function parseRunLine(line) {
|
|
1445
1520
|
try {
|
|
1446
1521
|
const value = JSON.parse(line);
|
|
@@ -1459,6 +1534,7 @@ async function prune(dir, ts, retentionMonths) {
|
|
|
1459
1534
|
async function query(dir, filter, now) {
|
|
1460
1535
|
const dimension = filter.by ?? "model";
|
|
1461
1536
|
const since = parseSince(filter.since, now());
|
|
1537
|
+
const rules = ruleSet(filter.rules);
|
|
1462
1538
|
const runs = await readRuns(dir);
|
|
1463
1539
|
const rows = /* @__PURE__ */ new Map();
|
|
1464
1540
|
let totalRuns = 0;
|
|
@@ -1466,18 +1542,19 @@ async function query(dir, filter, now) {
|
|
|
1466
1542
|
let totalOut = 0;
|
|
1467
1543
|
let totalTok = 0;
|
|
1468
1544
|
for (const run of runs) {
|
|
1469
|
-
if (since
|
|
1470
|
-
|
|
1545
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
1546
|
+
const slice = sliceRun(run, rules);
|
|
1547
|
+
if (!slice.touched) continue;
|
|
1471
1548
|
totalRuns += 1;
|
|
1472
|
-
totalIn +=
|
|
1473
|
-
totalOut +=
|
|
1474
|
-
totalTok +=
|
|
1549
|
+
totalIn += slice.inTok;
|
|
1550
|
+
totalOut += slice.outTok;
|
|
1551
|
+
totalTok += slice.totalTok;
|
|
1475
1552
|
if (dimension === "dir") {
|
|
1476
|
-
accumulate(rows, run.cwd,
|
|
1553
|
+
accumulate(rows, run.cwd, slice.inTok, slice.outTok, slice.totalTok, true);
|
|
1477
1554
|
continue;
|
|
1478
1555
|
}
|
|
1479
1556
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
1480
|
-
for (const usageRecord of
|
|
1557
|
+
for (const usageRecord of slice.usageRecords) {
|
|
1481
1558
|
const key = recordKey(usageRecord, dimension);
|
|
1482
1559
|
accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
|
|
1483
1560
|
seenKeys.add(key);
|
|
@@ -1492,6 +1569,55 @@ async function query(dir, filter, now) {
|
|
|
1492
1569
|
totalTok
|
|
1493
1570
|
};
|
|
1494
1571
|
}
|
|
1572
|
+
async function querySeries(dir, filter, now) {
|
|
1573
|
+
const nowMs = now();
|
|
1574
|
+
const since = parseSince(filter.since, nowMs);
|
|
1575
|
+
const rules = ruleSet(filter.rules);
|
|
1576
|
+
const runs = await readRuns(dir);
|
|
1577
|
+
const sliced = [];
|
|
1578
|
+
for (const run of runs) {
|
|
1579
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
1580
|
+
const slice = sliceRun(run, rules);
|
|
1581
|
+
if (slice.touched) sliced.push({
|
|
1582
|
+
slice,
|
|
1583
|
+
ts: run.ts
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const interval = filter.interval ?? chooseInterval(since, nowMs, sliced.map((entry) => entry.ts));
|
|
1587
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1588
|
+
let totalRuns = 0;
|
|
1589
|
+
let totalIn = 0;
|
|
1590
|
+
let totalOut = 0;
|
|
1591
|
+
let totalTok = 0;
|
|
1592
|
+
for (const { slice, ts } of sliced) {
|
|
1593
|
+
const startMs = bucketStart(ts, interval);
|
|
1594
|
+
const bucket = buckets.get(startMs) ?? {
|
|
1595
|
+
inTok: 0,
|
|
1596
|
+
key: bucketLabel(startMs, interval),
|
|
1597
|
+
outTok: 0,
|
|
1598
|
+
runs: 0,
|
|
1599
|
+
startMs,
|
|
1600
|
+
totalTok: 0
|
|
1601
|
+
};
|
|
1602
|
+
bucket.runs += 1;
|
|
1603
|
+
bucket.inTok += slice.inTok;
|
|
1604
|
+
bucket.outTok += slice.outTok;
|
|
1605
|
+
bucket.totalTok += slice.totalTok;
|
|
1606
|
+
buckets.set(startMs, bucket);
|
|
1607
|
+
totalRuns += 1;
|
|
1608
|
+
totalIn += slice.inTok;
|
|
1609
|
+
totalOut += slice.outTok;
|
|
1610
|
+
totalTok += slice.totalTok;
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
buckets: fillBuckets(buckets, interval),
|
|
1614
|
+
interval,
|
|
1615
|
+
totalIn,
|
|
1616
|
+
totalOut,
|
|
1617
|
+
totalRuns,
|
|
1618
|
+
totalTok
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1495
1621
|
async function readRuns(dir) {
|
|
1496
1622
|
const files = await statsFiles(dir);
|
|
1497
1623
|
const runs = [];
|
|
@@ -1519,6 +1645,29 @@ function recordKey(record, dimension) {
|
|
|
1519
1645
|
if (dimension === "operation") return record.operation ?? record.ruleId;
|
|
1520
1646
|
return `${record.providerId}/${record.modelId}`;
|
|
1521
1647
|
}
|
|
1648
|
+
function ruleSet(rules) {
|
|
1649
|
+
return rules && rules.length > 0 ? new Set(rules) : void 0;
|
|
1650
|
+
}
|
|
1651
|
+
function runInWindow(run, since, cwd) {
|
|
1652
|
+
return (since === void 0 || run.ts >= since) && (cwd === void 0 || run.cwd === cwd);
|
|
1653
|
+
}
|
|
1654
|
+
function sliceRun(run, rules) {
|
|
1655
|
+
if (rules === void 0) return {
|
|
1656
|
+
inTok: run.usage.inTok,
|
|
1657
|
+
outTok: run.usage.outTok,
|
|
1658
|
+
totalTok: run.usage.totalTok,
|
|
1659
|
+
touched: true,
|
|
1660
|
+
usageRecords: run.usage.records
|
|
1661
|
+
};
|
|
1662
|
+
const usageRecords = run.usage.records.filter((record) => rules.has(record.ruleId));
|
|
1663
|
+
return {
|
|
1664
|
+
inTok: sumBy(usageRecords, (record) => record.inTok),
|
|
1665
|
+
outTok: sumBy(usageRecords, (record) => record.outTok),
|
|
1666
|
+
totalTok: sumBy(usageRecords, (record) => record.totalTok),
|
|
1667
|
+
touched: usageRecords.length > 0,
|
|
1668
|
+
usageRecords
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1522
1671
|
function statsFileName(ts) {
|
|
1523
1672
|
const date = new Date(ts);
|
|
1524
1673
|
return `stats-${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}.jsonl`;
|
|
@@ -1539,6 +1688,11 @@ async function statsFiles(dir) {
|
|
|
1539
1688
|
} : void 0;
|
|
1540
1689
|
}).filter((entry) => entry !== void 0).sort((left, right) => left.month - right.month);
|
|
1541
1690
|
}
|
|
1691
|
+
function sumBy(items, select) {
|
|
1692
|
+
let total = 0;
|
|
1693
|
+
for (const item of items) total += select(item);
|
|
1694
|
+
return total;
|
|
1695
|
+
}
|
|
1542
1696
|
//#endregion
|
|
1543
1697
|
//#region src/cli/stats/record.ts
|
|
1544
1698
|
function createRunStat(input) {
|
|
@@ -2332,10 +2486,17 @@ const DIMENSION_LABEL = {
|
|
|
2332
2486
|
operation: "operation",
|
|
2333
2487
|
rule: "rule"
|
|
2334
2488
|
};
|
|
2489
|
+
function formatMetric(row, metric, exact = false) {
|
|
2490
|
+
return formatMetricValue(metricValue(row, metric), metric, exact);
|
|
2491
|
+
}
|
|
2492
|
+
function formatMetricValue(value, metric, exact = false) {
|
|
2493
|
+
return metric === "runs" ? String(value) : formatTokens(value, exact);
|
|
2494
|
+
}
|
|
2335
2495
|
function formatStatsAggregate(aggregate, options = {}) {
|
|
2336
2496
|
const paint = (text, style) => options.color === true ? style(text) : text;
|
|
2337
|
-
const
|
|
2338
|
-
const
|
|
2497
|
+
const metric = options.metric ?? "tokens";
|
|
2498
|
+
const exact = options.exact === true;
|
|
2499
|
+
const lines = [summaryLine(aggregate, paint, exact), ""];
|
|
2339
2500
|
if (aggregate.rows.length === 0) {
|
|
2340
2501
|
lines.push("No stats recorded yet.");
|
|
2341
2502
|
return `${lines.join("\n")}\n`;
|
|
@@ -2347,12 +2508,12 @@ function formatStatsAggregate(aggregate, options = {}) {
|
|
|
2347
2508
|
"output",
|
|
2348
2509
|
"total"
|
|
2349
2510
|
];
|
|
2350
|
-
const rows = aggregate.rows.map((row) => [
|
|
2511
|
+
const rows = sortRows(aggregate.rows, metric).map((row) => [
|
|
2351
2512
|
row.key,
|
|
2352
2513
|
String(row.runs),
|
|
2353
|
-
formatTokens(row.inTok),
|
|
2354
|
-
formatTokens(row.outTok),
|
|
2355
|
-
formatTokens(row.totalTok)
|
|
2514
|
+
formatTokens(row.inTok, exact),
|
|
2515
|
+
formatTokens(row.outTok, exact),
|
|
2516
|
+
formatTokens(row.totalTok, exact)
|
|
2356
2517
|
]);
|
|
2357
2518
|
const widths = header.map((cell, index) => Math.max(cell.length, ...rows.map((row) => row[index].length)));
|
|
2358
2519
|
const renderRow = (cells, isHeader) => cells.map((cell, index) => {
|
|
@@ -2362,14 +2523,39 @@ function formatStatsAggregate(aggregate, options = {}) {
|
|
|
2362
2523
|
lines.push(renderRow(header, true), ...rows.map((row) => renderRow(row, false)));
|
|
2363
2524
|
return `${lines.join("\n")}\n`;
|
|
2364
2525
|
}
|
|
2365
|
-
function formatTokens(value) {
|
|
2366
|
-
return value.toLocaleString("en-US");
|
|
2526
|
+
function formatTokens(value, exact = false) {
|
|
2527
|
+
if (exact || value < 1e4) return value.toLocaleString("en-US");
|
|
2528
|
+
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
|
2529
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
|
2530
|
+
return `${(value / 1e3).toFixed(2)}k`;
|
|
2531
|
+
}
|
|
2532
|
+
function metricValue(row, metric) {
|
|
2533
|
+
return metric === "runs" ? row.runs : row.totalTok;
|
|
2534
|
+
}
|
|
2535
|
+
function sortRows(rows, metric) {
|
|
2536
|
+
return [...rows].sort((left, right) => metricValue(right, metric) - metricValue(left, metric));
|
|
2537
|
+
}
|
|
2538
|
+
function summaryLine(totals, paint, exact = false) {
|
|
2539
|
+
const runLabel = totals.totalRuns === 1 ? "run" : "runs";
|
|
2540
|
+
return `${totals.totalRuns} ${runLabel} — ${formatTokens(totals.totalIn, exact)} in / ${formatTokens(totals.totalOut, exact)} out / ${paint(`${formatTokens(totals.totalTok, exact)} total`, colors.cyan)}`;
|
|
2367
2541
|
}
|
|
2368
2542
|
//#endregion
|
|
2369
|
-
//#region src/cli/commands/stats/
|
|
2370
|
-
const
|
|
2371
|
-
const
|
|
2372
|
-
const
|
|
2543
|
+
//#region src/cli/commands/stats/timeline.ts
|
|
2544
|
+
const CHART_HEIGHT = 6;
|
|
2545
|
+
const BAR_GAP = 2;
|
|
2546
|
+
const COLUMN_STRIDE = 3;
|
|
2547
|
+
const VERTICAL_BLOCKS = [
|
|
2548
|
+
" ",
|
|
2549
|
+
"▁",
|
|
2550
|
+
"▂",
|
|
2551
|
+
"▃",
|
|
2552
|
+
"▄",
|
|
2553
|
+
"▅",
|
|
2554
|
+
"▆",
|
|
2555
|
+
"▇",
|
|
2556
|
+
"█"
|
|
2557
|
+
];
|
|
2558
|
+
const HORIZONTAL_BLOCKS = [
|
|
2373
2559
|
"",
|
|
2374
2560
|
"▏",
|
|
2375
2561
|
"▎",
|
|
@@ -2379,32 +2565,143 @@ const PARTIAL_BLOCKS = [
|
|
|
2379
2565
|
"▊",
|
|
2380
2566
|
"▉"
|
|
2381
2567
|
];
|
|
2382
|
-
|
|
2568
|
+
const FULL_BLOCK = "█";
|
|
2569
|
+
const HORIZONTAL_BAR_MIN = 8;
|
|
2570
|
+
const HORIZONTAL_BAR_MAX = 40;
|
|
2571
|
+
const SUMMARY_GAP = 3;
|
|
2572
|
+
const SUMMARY_MAX_COLUMNS = 4;
|
|
2573
|
+
const MONTH_LABELS = [
|
|
2574
|
+
"Jan",
|
|
2575
|
+
"Feb",
|
|
2576
|
+
"Mar",
|
|
2577
|
+
"Apr",
|
|
2578
|
+
"May",
|
|
2579
|
+
"Jun",
|
|
2580
|
+
"Jul",
|
|
2581
|
+
"Aug",
|
|
2582
|
+
"Sep",
|
|
2583
|
+
"Oct",
|
|
2584
|
+
"Nov",
|
|
2585
|
+
"Dec"
|
|
2586
|
+
];
|
|
2587
|
+
function formatStatsTimeline(series, options = {}) {
|
|
2383
2588
|
const paint = (text, style) => options.color === true ? style(text) : text;
|
|
2384
|
-
const
|
|
2385
|
-
const
|
|
2386
|
-
|
|
2589
|
+
const metric = options.metric ?? "tokens";
|
|
2590
|
+
const exact = options.exact === true;
|
|
2591
|
+
const ctx = {
|
|
2592
|
+
exact,
|
|
2593
|
+
interval: series.interval,
|
|
2594
|
+
metric,
|
|
2595
|
+
paint
|
|
2596
|
+
};
|
|
2597
|
+
const lines = [
|
|
2598
|
+
summaryLine(series, paint, exact),
|
|
2599
|
+
"",
|
|
2600
|
+
caption(series, metric, options.rules)
|
|
2601
|
+
];
|
|
2602
|
+
if (series.buckets.length === 0) {
|
|
2387
2603
|
lines.push("No stats recorded yet.");
|
|
2388
2604
|
return `${lines.join("\n")}\n`;
|
|
2389
2605
|
}
|
|
2390
|
-
const
|
|
2391
|
-
const
|
|
2392
|
-
const
|
|
2393
|
-
const
|
|
2394
|
-
const
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2606
|
+
const values = series.buckets.map((bucket) => metricValue(bucket, metric));
|
|
2607
|
+
const maxValue = Math.max(...values);
|
|
2608
|
+
const axisWidth = Math.max(formatMetricValue(maxValue, metric, exact).length, 1);
|
|
2609
|
+
const verticalWidth = axisWidth + 2 + series.buckets.length * COLUMN_STRIDE;
|
|
2610
|
+
const width = options.columns ?? 80;
|
|
2611
|
+
if (options.vertical === true && verticalWidth <= width) {
|
|
2612
|
+
const chart = renderVertical(series.buckets, values, maxValue, axisWidth, ctx);
|
|
2613
|
+
const list = summaryList(series.buckets, width, ctx);
|
|
2614
|
+
return `${[
|
|
2615
|
+
...lines,
|
|
2616
|
+
...chart,
|
|
2617
|
+
"",
|
|
2618
|
+
...list
|
|
2619
|
+
].join("\n")}\n`;
|
|
2620
|
+
}
|
|
2621
|
+
return `${[...lines, ...renderHorizontal(series.buckets, values, maxValue, width, ctx)].join("\n")}\n`;
|
|
2622
|
+
}
|
|
2623
|
+
function axisRows(buckets, interval, chartWidth) {
|
|
2624
|
+
if (interval === "month") return [endpointAxis(buckets, chartWidth)];
|
|
2625
|
+
const days = Array.from({ length: chartWidth + 4 }).fill(" ");
|
|
2626
|
+
const months = Array.from({ length: chartWidth + 4 }).fill(" ");
|
|
2627
|
+
buckets.forEach((bucket, index) => {
|
|
2628
|
+
const date = new Date(bucket.startMs);
|
|
2629
|
+
const at = BAR_GAP + index * COLUMN_STRIDE;
|
|
2630
|
+
const previous = index === 0 ? void 0 : new Date(buckets[index - 1].startMs);
|
|
2631
|
+
writeAt(days, at, String(date.getUTCDate()).padStart(2, "0"));
|
|
2632
|
+
if (startsNewMonth(previous, date)) writeAt(months, at, MONTH_LABELS[date.getUTCMonth()]);
|
|
2633
|
+
});
|
|
2634
|
+
return [days.join("").replace(/\s+$/u, ""), months.join("").replace(/\s+$/u, "")];
|
|
2635
|
+
}
|
|
2636
|
+
function caption(series, metric, rules) {
|
|
2637
|
+
const filter = rules && rules.length > 0 ? ` — ${rules.join(", ")}` : "";
|
|
2638
|
+
return `${metric} by ${series.interval}${filter}`;
|
|
2639
|
+
}
|
|
2640
|
+
function endpointAxis(buckets, chartWidth) {
|
|
2641
|
+
const first = buckets[0].key;
|
|
2642
|
+
if (buckets.length === 1) return `${" ".repeat(BAR_GAP)}${first}`;
|
|
2643
|
+
const last = buckets.at(-1).key;
|
|
2644
|
+
const lastStart = chartWidth - last.length;
|
|
2645
|
+
const firstEnd = BAR_GAP + first.length;
|
|
2646
|
+
return lastStart > firstEnd ? `${" ".repeat(BAR_GAP)}${first}${" ".repeat(lastStart - firstEnd)}${last}` : `${" ".repeat(BAR_GAP)}${first} … ${last}`;
|
|
2647
|
+
}
|
|
2648
|
+
function renderHorizontal(buckets, values, maxValue, width, ctx) {
|
|
2649
|
+
const labels = buckets.map((bucket) => formatMetric(bucket, ctx.metric, ctx.exact));
|
|
2650
|
+
const keyWidth = Math.max(...buckets.map((bucket) => bucket.key.length));
|
|
2651
|
+
const valueWidth = Math.max(...labels.map((label) => label.length));
|
|
2652
|
+
const barWidth = Math.max(HORIZONTAL_BAR_MIN, Math.min(HORIZONTAL_BAR_MAX, width - keyWidth - valueWidth - 6));
|
|
2653
|
+
return buckets.map((bucket, index) => {
|
|
2654
|
+
const bar = renderHorizontalBar(maxValue === 0 ? 0 : values[index] / maxValue, barWidth);
|
|
2655
|
+
return `${bucket.key.padEnd(keyWidth)} ${ctx.paint(bar, colors.cyan)} ${labels[index].padStart(valueWidth)}`;
|
|
2400
2656
|
});
|
|
2401
|
-
return `${lines.join("\n")}\n`;
|
|
2402
2657
|
}
|
|
2403
|
-
function
|
|
2404
|
-
const eighths = fraction <= 0 ? 0 : Math.max(1, Math.round(fraction *
|
|
2658
|
+
function renderHorizontalBar(fraction, width) {
|
|
2659
|
+
const eighths = fraction <= 0 ? 0 : Math.max(1, Math.round(fraction * width * 8));
|
|
2405
2660
|
const full = Math.floor(eighths / 8);
|
|
2406
|
-
|
|
2407
|
-
|
|
2661
|
+
return `${FULL_BLOCK.repeat(full)}${HORIZONTAL_BLOCKS[eighths % 8]}`.padEnd(width);
|
|
2662
|
+
}
|
|
2663
|
+
function renderVertical(buckets, values, maxValue, axisWidth, ctx) {
|
|
2664
|
+
const columnAt = (index) => BAR_GAP + index * COLUMN_STRIDE;
|
|
2665
|
+
const chartWidth = columnAt(buckets.length - 1) + 1;
|
|
2666
|
+
const eighths = values.map((value) => {
|
|
2667
|
+
if (value <= 0 || maxValue === 0) return 0;
|
|
2668
|
+
return Math.max(1, Math.round(value / maxValue * CHART_HEIGHT * 8));
|
|
2669
|
+
});
|
|
2670
|
+
const gutter = " ".repeat(axisWidth);
|
|
2671
|
+
const lines = [];
|
|
2672
|
+
for (let row = CHART_HEIGHT - 1; row >= 0; row -= 1) {
|
|
2673
|
+
const cells = Array.from({ length: chartWidth }).fill(" ");
|
|
2674
|
+
eighths.forEach((total, index) => {
|
|
2675
|
+
cells[columnAt(index)] = VERTICAL_BLOCKS[Math.max(0, Math.min(8, total - row * 8))];
|
|
2676
|
+
});
|
|
2677
|
+
const label = row === CHART_HEIGHT - 1 ? formatMetricValue(maxValue, ctx.metric, ctx.exact).padStart(axisWidth) : gutter;
|
|
2678
|
+
const axis = row === CHART_HEIGHT - 1 ? "┤" : "│";
|
|
2679
|
+
lines.push(`${label} ${axis}${ctx.paint(cells.join("").replace(/\s+$/u, ""), colors.cyan)}`);
|
|
2680
|
+
}
|
|
2681
|
+
const baseline = Array.from({ length: chartWidth }).fill("─");
|
|
2682
|
+
buckets.forEach((_, index) => {
|
|
2683
|
+
baseline[columnAt(index)] = "┴";
|
|
2684
|
+
});
|
|
2685
|
+
lines.push(`${"0".padStart(axisWidth)} ┼${baseline.join("")}`);
|
|
2686
|
+
for (const labelRow of axisRows(buckets, ctx.interval, chartWidth)) lines.push(`${gutter} ${labelRow}`);
|
|
2687
|
+
return lines;
|
|
2688
|
+
}
|
|
2689
|
+
function startsNewMonth(previous, date) {
|
|
2690
|
+
if (previous === void 0) return true;
|
|
2691
|
+
return previous.getUTCFullYear() !== date.getUTCFullYear() || previous.getUTCMonth() !== date.getUTCMonth();
|
|
2692
|
+
}
|
|
2693
|
+
function summaryList(buckets, width, ctx) {
|
|
2694
|
+
const labels = buckets.map((bucket) => formatMetric(bucket, ctx.metric, ctx.exact));
|
|
2695
|
+
const keyWidth = Math.max(...buckets.map((bucket) => bucket.key.length));
|
|
2696
|
+
const valueWidth = Math.max(...labels.map((label) => label.length));
|
|
2697
|
+
const cellWidth = keyWidth + 2 + valueWidth;
|
|
2698
|
+
const perRow = Math.max(1, Math.min(SUMMARY_MAX_COLUMNS, Math.floor((width + SUMMARY_GAP) / (cellWidth + SUMMARY_GAP))));
|
|
2699
|
+
const rows = [];
|
|
2700
|
+
for (let start = 0; start < buckets.length; start += perRow) rows.push(buckets.slice(start, start + perRow).map((bucket, offset) => `${bucket.key.padEnd(keyWidth)} ${labels[start + offset].padStart(valueWidth)}`).join(" ".repeat(SUMMARY_GAP)));
|
|
2701
|
+
return rows;
|
|
2702
|
+
}
|
|
2703
|
+
function writeAt(cells, at, text) {
|
|
2704
|
+
for (let index = 0; index < text.length && at + index < cells.length; index += 1) cells[at + index] = text[index];
|
|
2408
2705
|
}
|
|
2409
2706
|
//#endregion
|
|
2410
2707
|
//#region src/cli/commands/stats/stats.ts
|
|
@@ -2414,16 +2711,48 @@ const DIMENSIONS = /* @__PURE__ */ new Set([
|
|
|
2414
2711
|
"operation",
|
|
2415
2712
|
"rule"
|
|
2416
2713
|
]);
|
|
2714
|
+
const METRICS = /* @__PURE__ */ new Set(["runs", "tokens"]);
|
|
2715
|
+
const INTERVALS = /* @__PURE__ */ new Set([
|
|
2716
|
+
"day",
|
|
2717
|
+
"month",
|
|
2718
|
+
"week"
|
|
2719
|
+
]);
|
|
2417
2720
|
async function runStatsCommand(options, io) {
|
|
2418
2721
|
try {
|
|
2419
|
-
const
|
|
2420
|
-
const
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2722
|
+
const metric = parseMetric(options.metric);
|
|
2723
|
+
const rules = parseRules(options.rule);
|
|
2724
|
+
const cwd = options.here === true ? io.cwd : options.cwd;
|
|
2725
|
+
const color = io.stdout.isTTY === true;
|
|
2726
|
+
const exact = options.exactNumbers === true;
|
|
2727
|
+
const store = await resolveStore(io);
|
|
2728
|
+
if (options.chart === true) {
|
|
2729
|
+
const series = await store.querySeries({
|
|
2730
|
+
cwd,
|
|
2731
|
+
interval: parseInterval(options.interval),
|
|
2732
|
+
rules,
|
|
2733
|
+
since: options.since
|
|
2734
|
+
});
|
|
2735
|
+
io.stdout.write(options.json === true ? `${JSON.stringify(series, null, 2)}\n` : formatStatsTimeline(series, {
|
|
2736
|
+
color,
|
|
2737
|
+
columns: io.stdout.columns,
|
|
2738
|
+
exact,
|
|
2739
|
+
metric,
|
|
2740
|
+
rules,
|
|
2741
|
+
vertical: options.vertical === true
|
|
2742
|
+
}));
|
|
2743
|
+
return 0;
|
|
2744
|
+
}
|
|
2745
|
+
const aggregate = await store.query({
|
|
2746
|
+
by: parseDimension(options.by),
|
|
2747
|
+
cwd,
|
|
2748
|
+
rules,
|
|
2424
2749
|
since: options.since
|
|
2425
2750
|
});
|
|
2426
|
-
io.stdout.write(
|
|
2751
|
+
io.stdout.write(options.json === true ? `${JSON.stringify(aggregate, null, 2)}\n` : formatStatsAggregate(aggregate, {
|
|
2752
|
+
color,
|
|
2753
|
+
exact,
|
|
2754
|
+
metric
|
|
2755
|
+
}));
|
|
2427
2756
|
return 0;
|
|
2428
2757
|
} catch (error) {
|
|
2429
2758
|
io.stderr.write(`${errorMessageFrom$1(error) ?? "Failed to read stats."}\n`);
|
|
@@ -2435,10 +2764,24 @@ function parseDimension(value) {
|
|
|
2435
2764
|
if (DIMENSIONS.has(value)) return value;
|
|
2436
2765
|
throw new Error(`Invalid --by "${value}": use rule, operation, model, or dir.`);
|
|
2437
2766
|
}
|
|
2438
|
-
function
|
|
2439
|
-
if (
|
|
2440
|
-
if (
|
|
2441
|
-
|
|
2767
|
+
function parseInterval(value) {
|
|
2768
|
+
if (value === void 0) return;
|
|
2769
|
+
if (INTERVALS.has(value)) return value;
|
|
2770
|
+
throw new Error(`Invalid --interval "${value}": use day, week, or month.`);
|
|
2771
|
+
}
|
|
2772
|
+
function parseMetric(value) {
|
|
2773
|
+
if (value === void 0) return "tokens";
|
|
2774
|
+
if (METRICS.has(value)) return value;
|
|
2775
|
+
throw new Error(`Invalid --metric "${value}": use tokens or runs.`);
|
|
2776
|
+
}
|
|
2777
|
+
function parseRules(value) {
|
|
2778
|
+
if (value === void 0) return;
|
|
2779
|
+
const rules = value.split(",").map((rule) => rule.trim()).filter(Boolean);
|
|
2780
|
+
return rules.length > 0 ? rules : void 0;
|
|
2781
|
+
}
|
|
2782
|
+
async function resolveStore(io) {
|
|
2783
|
+
const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd)]);
|
|
2784
|
+
return createJsonlStatsStore({ dir: statsLocation(resolveConfigRunner(config)?.stats) ?? statsLocation(setupConfig.runner?.stats) ?? getStatsDir(io.env) });
|
|
2442
2785
|
}
|
|
2443
2786
|
function statsLocation(stats) {
|
|
2444
2787
|
return typeof stats === "object" ? stats.location : void 0;
|
|
@@ -2456,11 +2799,31 @@ const commandTree = [
|
|
|
2456
2799
|
name: "stats",
|
|
2457
2800
|
options: [
|
|
2458
2801
|
{
|
|
2459
|
-
description: "
|
|
2802
|
+
description: "Render a usage timeline instead of the table",
|
|
2803
|
+
flags: "--chart"
|
|
2804
|
+
},
|
|
2805
|
+
{
|
|
2806
|
+
description: "Group the table by rule, operation, model, or dir",
|
|
2460
2807
|
flags: "--by <dimension>"
|
|
2461
2808
|
},
|
|
2462
2809
|
{
|
|
2463
|
-
description: "
|
|
2810
|
+
description: "Chart bucket: day, week, or month (default: auto)",
|
|
2811
|
+
flags: "--interval <interval>"
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
description: "Draw the chart as vertical bars instead of horizontal",
|
|
2815
|
+
flags: "--vertical"
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
description: "Rank/plot by tokens or runs",
|
|
2819
|
+
flags: "--metric <metric>"
|
|
2820
|
+
},
|
|
2821
|
+
{
|
|
2822
|
+
description: "Only include these rules (comma-separated)",
|
|
2823
|
+
flags: "--rule <ids>"
|
|
2824
|
+
},
|
|
2825
|
+
{
|
|
2826
|
+
description: "Only include runs since this time (e.g. 24h, 7d, 4w, 3m, 2025-01)",
|
|
2464
2827
|
flags: "--since <time>"
|
|
2465
2828
|
},
|
|
2466
2829
|
{
|
|
@@ -2472,8 +2835,8 @@ const commandTree = [
|
|
|
2472
2835
|
flags: "--here"
|
|
2473
2836
|
},
|
|
2474
2837
|
{
|
|
2475
|
-
description: "
|
|
2476
|
-
flags: "--
|
|
2838
|
+
description: "Show full counts instead of compact k/M/B",
|
|
2839
|
+
flags: "--exact-numbers"
|
|
2477
2840
|
},
|
|
2478
2841
|
{
|
|
2479
2842
|
description: "Output JSON",
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-
|
|
1
|
+
import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-Dm3rTcjL.mjs";
|
|
2
2
|
import { ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated } from "@alint-js/config";
|
|
3
3
|
import { defineConfig } from "@alint-js/core";
|
|
4
4
|
export { defineConfig, executeCli, formatDiagnostics, formatJson, formatStylish, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alint-js/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.37",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"types": "./dist/index.d.mts",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"pathe": "^2.0.3",
|
|
28
28
|
"table": "^6.9.0",
|
|
29
29
|
"tinyrainbow": "^3.1.0",
|
|
30
|
-
"@alint-js/
|
|
31
|
-
"@alint-js/
|
|
30
|
+
"@alint-js/config": "0.0.37",
|
|
31
|
+
"@alint-js/core": "0.0.37"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@pnpm/find-workspace-dir": "^1000.1.5",
|