@alint-js/cli 0.0.34 → 0.0.36

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-D8-tnU0B.mjs";
2
+ import { t as executeCli } from "../cli-Ct9DDkxy.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -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.34";
19
+ var version = "0.0.36";
20
20
  //#endregion
21
21
  //#region src/cli/commands/command.ts
22
22
  function defineCommand(node) {
@@ -873,7 +873,7 @@ function createRuleGroups(state) {
873
873
  }
874
874
  function estimateEta(durationMs, terminal, planned) {
875
875
  if (terminal <= 0 || planned <= 0 || durationMs <= 0) return "eta ?";
876
- return `eta ~${formatDuration(durationMs * (planned - terminal) / terminal)}`;
876
+ return `eta ~${formatDuration$1(durationMs * (planned - terminal) / terminal)}`;
877
877
  }
878
878
  function fitRow(row, columns) {
879
879
  if (columns <= 0) return "";
@@ -927,7 +927,7 @@ function formatContentRows(groups, state, options, now, contentBudget) {
927
927
  }
928
928
  return rows;
929
929
  }
930
- function formatDuration(ms) {
930
+ function formatDuration$1(ms) {
931
931
  return `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
932
932
  }
933
933
  function formatFooters(state, options, now) {
@@ -937,7 +937,7 @@ function formatFooters(state, options, now) {
937
937
  const eta = estimateEta(elapsed, terminal, state.execution.planned);
938
938
  const projectedTokens = terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
939
939
  return [
940
- fitRow(`${terminal}/${state.execution.planned}${progressBar} ${formatDuration(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
940
+ fitRow(`${terminal}/${state.execution.planned}${progressBar} ${formatDuration$1(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
941
941
  fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
942
942
  fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
943
943
  ];
@@ -965,7 +965,7 @@ function formatTargetRow(jobState, options, now, last) {
965
965
  const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
966
966
  const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
967
967
  const startedAt = jobState.retry?.startedAt ?? jobState.startedAt ?? now;
968
- return fitRow(`${last ? " └─" : " ├─"} ${inputPath} > ${target} ${jobState.retry ? `${jobState.retry.attempt}/${jobState.retry.maxAttempts} retrying elapsed ${formatDuration(now - startedAt)}` : `running ${formatDuration(now - startedAt)}`}`, options.columns);
968
+ return fitRow(`${last ? " └─" : " ├─"} ${inputPath} > ${target} ${jobState.retry ? `${jobState.retry.attempt}/${jobState.retry.maxAttempts} retrying elapsed ${formatDuration$1(now - startedAt)}` : `running ${formatDuration$1(now - startedAt)}`}`, options.columns);
969
969
  }
970
970
  function styleRow(row, state, warnCount, errorCount, options) {
971
971
  let styledRow = row;
@@ -1433,6 +1433,7 @@ function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
1433
1433
  row.totalTok += totalTok;
1434
1434
  if (countRun) row.runs += 1;
1435
1435
  rows.set(key, row);
1436
+ return row;
1436
1437
  }
1437
1438
  function isEnoent(error) {
1438
1439
  return error instanceof Error && error.code === "ENOENT";
@@ -1465,6 +1466,7 @@ async function query(dir, filter, now) {
1465
1466
  let totalIn = 0;
1466
1467
  let totalOut = 0;
1467
1468
  let totalTok = 0;
1469
+ let totalDuration;
1468
1470
  for (const run of runs) {
1469
1471
  if (since !== void 0 && run.ts < since) continue;
1470
1472
  if (filter.cwd !== void 0 && run.cwd !== filter.cwd) continue;
@@ -1482,10 +1484,18 @@ async function query(dir, filter, now) {
1482
1484
  accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
1483
1485
  seenKeys.add(key);
1484
1486
  }
1487
+ if (dimension !== "rule") continue;
1488
+ for (const { durationMs, ruleId } of run.ruleDurations ?? []) {
1489
+ const row = accumulate(rows, ruleId, 0, 0, 0, !seenKeys.has(ruleId));
1490
+ seenKeys.add(ruleId);
1491
+ row.durationMs = (row.durationMs ?? 0) + durationMs;
1492
+ totalDuration = (totalDuration ?? 0) + durationMs;
1493
+ }
1485
1494
  }
1486
1495
  return {
1487
1496
  dimension,
1488
1497
  rows: [...rows.values()].sort((left, right) => right.totalTok - left.totalTok),
1498
+ totalDuration,
1489
1499
  totalIn,
1490
1500
  totalOut,
1491
1501
  totalRuns,
@@ -1546,6 +1556,7 @@ function createRunStat(input) {
1546
1556
  cwd: input.cwd,
1547
1557
  durationMs: input.durationMs,
1548
1558
  ruleCounts: input.ruleCounts,
1559
+ ruleDurations: input.ruleDurations,
1549
1560
  usage: {
1550
1561
  inTok: input.usage.inputTokens,
1551
1562
  outTok: input.usage.outputTokens,
@@ -1583,10 +1594,16 @@ function createStatsCollector() {
1583
1594
  };
1584
1595
  let startedAt;
1585
1596
  let endedAt;
1597
+ const ruleMs = /* @__PURE__ */ new Map();
1586
1598
  return {
1587
1599
  counts,
1588
1600
  durationMs: () => startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0,
1589
1601
  reporter: {
1602
+ onJobEnd: (payload) => {
1603
+ if (payload.startedAt === void 0 || payload.endedAt === void 0) return;
1604
+ const elapsed = Math.max(payload.endedAt - payload.startedAt, 0);
1605
+ ruleMs.set(payload.job.ruleId, (ruleMs.get(payload.job.ruleId) ?? 0) + elapsed);
1606
+ },
1590
1607
  onRunEnd: (payload) => {
1591
1608
  counts.cached = payload.execution.cached;
1592
1609
  counts.cancelled = payload.execution.cancelled;
@@ -1598,7 +1615,11 @@ function createStatsCollector() {
1598
1615
  onRunStart: (payload) => {
1599
1616
  startedAt = payload.startedAt;
1600
1617
  }
1601
- }
1618
+ },
1619
+ ruleDurations: () => ruleMs.size === 0 ? void 0 : [...ruleMs].map(([ruleId, durationMs]) => ({
1620
+ durationMs,
1621
+ ruleId
1622
+ }))
1602
1623
  };
1603
1624
  }
1604
1625
  function mergeProgressReporters(base, extra) {
@@ -1679,6 +1700,7 @@ async function writeRunStats(target, collector, result, cwd) {
1679
1700
  failed: result.execution.failed,
1680
1701
  planned: result.execution.planned
1681
1702
  },
1703
+ ruleDurations: collector.ruleDurations(),
1682
1704
  usage: result.usage
1683
1705
  }));
1684
1706
  } catch {}
@@ -2332,14 +2354,25 @@ const DIMENSION_LABEL = {
2332
2354
  operation: "operation",
2333
2355
  rule: "rule"
2334
2356
  };
2357
+ const METRIC_LABEL = {
2358
+ duration: "time",
2359
+ runs: "runs",
2360
+ tokens: "total"
2361
+ };
2362
+ function formatMetric(row, metric) {
2363
+ if (metric === "runs") return String(row.runs);
2364
+ if (metric === "duration") return formatDuration(row.durationMs);
2365
+ return formatTokens(row.totalTok);
2366
+ }
2335
2367
  function formatStatsAggregate(aggregate, options = {}) {
2336
2368
  const paint = (text, style) => options.color === true ? style(text) : text;
2337
- const runLabel = aggregate.totalRuns === 1 ? "run" : "runs";
2338
- const lines = [`${aggregate.totalRuns} ${runLabel} — ${formatTokens(aggregate.totalIn)} in / ${formatTokens(aggregate.totalOut)} out / ${paint(`${formatTokens(aggregate.totalTok)} total`, colors.cyan)}`, ""];
2369
+ const metric = options.metric ?? "tokens";
2370
+ const lines = [summaryLine(aggregate, paint), ""];
2339
2371
  if (aggregate.rows.length === 0) {
2340
2372
  lines.push("No stats recorded yet.");
2341
2373
  return `${lines.join("\n")}\n`;
2342
2374
  }
2375
+ const withTime = aggregate.dimension === "rule";
2343
2376
  const header = [
2344
2377
  DIMENSION_LABEL[aggregate.dimension],
2345
2378
  "runs",
@@ -2347,13 +2380,18 @@ function formatStatsAggregate(aggregate, options = {}) {
2347
2380
  "output",
2348
2381
  "total"
2349
2382
  ];
2350
- const rows = aggregate.rows.map((row) => [
2351
- row.key,
2352
- String(row.runs),
2353
- formatTokens(row.inTok),
2354
- formatTokens(row.outTok),
2355
- formatTokens(row.totalTok)
2356
- ]);
2383
+ if (withTime) header.push("time");
2384
+ const rows = sortRows(aggregate.rows, metric).map((row) => {
2385
+ const cells = [
2386
+ row.key,
2387
+ String(row.runs),
2388
+ formatTokens(row.inTok),
2389
+ formatTokens(row.outTok),
2390
+ formatTokens(row.totalTok)
2391
+ ];
2392
+ if (withTime) cells.push(formatDuration(row.durationMs));
2393
+ return cells;
2394
+ });
2357
2395
  const widths = header.map((cell, index) => Math.max(cell.length, ...rows.map((row) => row[index].length)));
2358
2396
  const renderRow = (cells, isHeader) => cells.map((cell, index) => {
2359
2397
  const aligned = index === 0 ? cell.padEnd(widths[index]) : cell.padStart(widths[index]);
@@ -2365,6 +2403,67 @@ function formatStatsAggregate(aggregate, options = {}) {
2365
2403
  function formatTokens(value) {
2366
2404
  return value.toLocaleString("en-US");
2367
2405
  }
2406
+ function metricValue(row, metric) {
2407
+ if (metric === "runs") return row.runs;
2408
+ if (metric === "duration") return row.durationMs ?? 0;
2409
+ return row.totalTok;
2410
+ }
2411
+ function sortRows(rows, metric) {
2412
+ return [...rows].sort((left, right) => metricValue(right, metric) - metricValue(left, metric));
2413
+ }
2414
+ function summaryLine(aggregate, paint) {
2415
+ const runLabel = aggregate.totalRuns === 1 ? "run" : "runs";
2416
+ const time = aggregate.totalDuration === void 0 ? "" : ` / ${formatDuration(aggregate.totalDuration)} time`;
2417
+ return `${aggregate.totalRuns} ${runLabel} — ${formatTokens(aggregate.totalIn)} in / ${formatTokens(aggregate.totalOut)} out / ${paint(`${formatTokens(aggregate.totalTok)} total`, colors.cyan)}${time}`;
2418
+ }
2419
+ function formatDuration(ms) {
2420
+ return ms === void 0 ? "—" : `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
2421
+ }
2422
+ //#endregion
2423
+ //#region src/cli/commands/stats/chart.ts
2424
+ const BAR_WIDTH = 32;
2425
+ const FULL_BLOCK = "█";
2426
+ const PARTIAL_BLOCKS = [
2427
+ "",
2428
+ "▏",
2429
+ "▎",
2430
+ "▍",
2431
+ "▌",
2432
+ "▋",
2433
+ "▊",
2434
+ "▉"
2435
+ ];
2436
+ function formatStatsChart(aggregate, options = {}) {
2437
+ const paint = (text, style) => options.color === true ? style(text) : text;
2438
+ const metric = options.metric ?? "tokens";
2439
+ const lines = [summaryLine(aggregate, paint), ""];
2440
+ if (aggregate.rows.length === 0) {
2441
+ lines.push("No stats recorded yet.");
2442
+ return `${lines.join("\n")}\n`;
2443
+ }
2444
+ const rows = sortRows(aggregate.rows, metric);
2445
+ const maxValue = Math.max(...rows.map((row) => metricValue(row, metric)));
2446
+ const columnTotal = rows.reduce((sum, row) => sum + metricValue(row, metric), 0);
2447
+ const label = DIMENSION_LABEL[aggregate.dimension];
2448
+ const valueLabel = METRIC_LABEL[metric];
2449
+ const keyWidth = Math.max(label.length, ...rows.map((row) => row.key.length));
2450
+ const values = rows.map((row) => formatMetric(row, metric));
2451
+ const valueWidth = Math.max(valueLabel.length, ...values.map((value) => value.length));
2452
+ lines.push(paint(`${label.padEnd(keyWidth)} ${" ".repeat(BAR_WIDTH)} ${valueLabel.padStart(valueWidth)} share`, colors.bold));
2453
+ rows.forEach((row, index) => {
2454
+ const value = metricValue(row, metric);
2455
+ const bar = renderBar(maxValue === 0 ? 0 : value / maxValue);
2456
+ const share = columnTotal === 0 ? 0 : value / columnTotal * 100;
2457
+ lines.push(`${row.key.padEnd(keyWidth)} ${paint(bar, colors.cyan)} ${values[index].padStart(valueWidth)} ${share.toFixed(1).padStart(5)}%`);
2458
+ });
2459
+ return `${lines.join("\n")}\n`;
2460
+ }
2461
+ function renderBar(fraction) {
2462
+ const eighths = fraction <= 0 ? 0 : Math.max(1, Math.round(fraction * BAR_WIDTH * 8));
2463
+ const full = Math.floor(eighths / 8);
2464
+ const remainder = eighths % 8;
2465
+ return `${FULL_BLOCK.repeat(full)}${PARTIAL_BLOCKS[remainder]}`.padEnd(BAR_WIDTH);
2466
+ }
2368
2467
  //#endregion
2369
2468
  //#region src/cli/commands/stats/stats.ts
2370
2469
  const DIMENSIONS = /* @__PURE__ */ new Set([
@@ -2373,16 +2472,23 @@ const DIMENSIONS = /* @__PURE__ */ new Set([
2373
2472
  "operation",
2374
2473
  "rule"
2375
2474
  ]);
2475
+ const METRICS = /* @__PURE__ */ new Set([
2476
+ "duration",
2477
+ "runs",
2478
+ "tokens"
2479
+ ]);
2376
2480
  async function runStatsCommand(options, io) {
2377
2481
  try {
2378
2482
  const dimension = parseDimension(options.by);
2483
+ const metric = parseMetric(options.metric);
2484
+ if (metric === "duration" && dimension !== "rule") throw new Error("The duration metric is only available with --by rule.");
2379
2485
  const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd)]);
2380
2486
  const aggregate = await createJsonlStatsStore({ dir: statsLocation(resolveConfigRunner(config)?.stats) ?? statsLocation(setupConfig.runner?.stats) ?? getStatsDir(io.env) }).query({
2381
2487
  by: dimension,
2382
2488
  cwd: options.here === true ? io.cwd : options.cwd,
2383
2489
  since: options.since
2384
2490
  });
2385
- io.stdout.write(options.json === true ? `${JSON.stringify(aggregate, null, 2)}\n` : formatStatsAggregate(aggregate, { color: io.stdout.isTTY === true }));
2491
+ io.stdout.write(render(aggregate, options, metric, io.stdout.isTTY === true));
2386
2492
  return 0;
2387
2493
  } catch (error) {
2388
2494
  io.stderr.write(`${errorMessageFrom$1(error) ?? "Failed to read stats."}\n`);
@@ -2394,6 +2500,22 @@ function parseDimension(value) {
2394
2500
  if (DIMENSIONS.has(value)) return value;
2395
2501
  throw new Error(`Invalid --by "${value}": use rule, operation, model, or dir.`);
2396
2502
  }
2503
+ function parseMetric(value) {
2504
+ if (value === void 0) return "tokens";
2505
+ if (METRICS.has(value)) return value;
2506
+ throw new Error(`Invalid --metric "${value}": use tokens, runs, or duration.`);
2507
+ }
2508
+ function render(aggregate, options, metric, color) {
2509
+ if (options.json === true) return `${JSON.stringify(aggregate, null, 2)}\n`;
2510
+ if (options.chart === true) return formatStatsChart(aggregate, {
2511
+ color,
2512
+ metric
2513
+ });
2514
+ return formatStatsAggregate(aggregate, {
2515
+ color,
2516
+ metric
2517
+ });
2518
+ }
2397
2519
  function statsLocation(stats) {
2398
2520
  return typeof stats === "object" ? stats.location : void 0;
2399
2521
  }
@@ -2425,6 +2547,14 @@ const commandTree = [
2425
2547
  description: "Only include runs from the current directory",
2426
2548
  flags: "--here"
2427
2549
  },
2550
+ {
2551
+ description: "Render grouped rows as a bar chart",
2552
+ flags: "--chart"
2553
+ },
2554
+ {
2555
+ description: "Rank by tokens, runs, or duration (duration needs --by rule)",
2556
+ flags: "--metric <metric>"
2557
+ },
2428
2558
  {
2429
2559
  description: "Output JSON",
2430
2560
  flags: "--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-D8-tnU0B.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-Ct9DDkxy.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.34",
4
+ "version": "0.0.36",
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/config": "0.0.34",
31
- "@alint-js/core": "0.0.34"
30
+ "@alint-js/core": "0.0.36",
31
+ "@alint-js/config": "0.0.36"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@pnpm/find-workspace-dir": "^1000.1.5",