@alint-js/cli 0.0.8 → 0.0.9

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-D7cUTZnE.mjs";
2
+ import { t as executeCli } from "../cli-GIOYMybU.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -1,12 +1,12 @@
1
1
  import process from "node:process";
2
2
  import { inspect } from "node:util";
3
3
  import { cac } from "cac";
4
- import { getGlobalSetupConfigPath, getProjectSetupConfigPath, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
4
+ import { getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
5
5
  import { AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForFile, runAlint } from "@alint-js/core";
6
- import { relative, resolve } from "pathe";
6
+ import { join, relative, resolve } from "pathe";
7
7
  import { getBorderCharacters, table } from "table";
8
8
  import { errorMessageFrom } from "@moeru/std/error";
9
- import { readFile, readdir, stat } from "node:fs/promises";
9
+ import { appendFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
10
10
  import c, { createColors } from "tinyrainbow";
11
11
  import cliSpinners from "cli-spinners";
12
12
  import { relative as relative$1 } from "node:path";
@@ -183,8 +183,10 @@ function rewriteUsageSection(sections, path, node) {
183
183
  function shouldSkipOptionValue(arg) {
184
184
  if (arg.includes("=")) return false;
185
185
  return [
186
+ "--by",
186
187
  "--cache-location",
187
188
  "--config",
189
+ "--cwd",
188
190
  "--file-concurrency",
189
191
  "--format",
190
192
  "--model",
@@ -193,6 +195,7 @@ function shouldSkipOptionValue(arg) {
193
195
  "--provider-id",
194
196
  "--provider-model",
195
197
  "--rule-concurrency",
198
+ "--since",
196
199
  "--timeout-ms",
197
200
  "-f",
198
201
  "-l"
@@ -499,7 +502,7 @@ function formatJson(result) {
499
502
  }
500
503
  //#endregion
501
504
  //#region src/cli/reporters/stylish.ts
502
- const colors$1 = createColors({ force: true });
505
+ const colors$2 = createColors({ force: true });
503
506
  function formatStylish(input, options = {}) {
504
507
  const diagnostics = Array.isArray(input) ? input : input.diagnostics;
505
508
  const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
@@ -541,12 +544,12 @@ function createStyle(color) {
541
544
  warning: identity
542
545
  };
543
546
  return {
544
- error: colors$1.red,
545
- file: colors$1.underline,
546
- location: colors$1.dim,
547
- ruleId: colors$1.dim,
548
- summaryToken: colors$1.cyan,
549
- warning: colors$1.yellow
547
+ error: colors$2.red,
548
+ file: colors$2.underline,
549
+ location: colors$2.dim,
550
+ ruleId: colors$2.dim,
551
+ summaryToken: colors$2.cyan,
552
+ warning: colors$2.yellow
550
553
  };
551
554
  }
552
555
  function formatSummary(diagnostics, totalTokens, style) {
@@ -594,7 +597,7 @@ function countDiagnostics$1(diagnostics, severity) {
594
597
  }
595
598
  //#endregion
596
599
  //#region src/cli/reporters/progress/summary.ts
597
- const colors = createColors({ force: true });
600
+ const colors$1 = createColors({ force: true });
598
601
  function createSummaryProgressReporter(options) {
599
602
  const now = () => options.clock?.() ?? Date.now();
600
603
  const state = {
@@ -802,8 +805,8 @@ function replaceFirst(row, search, replacement) {
802
805
  function styleRow(row, state, warnCount, errorCount, options) {
803
806
  let styledRow = row;
804
807
  const spinner = options.spinnerFrames[state.spinnerIndex] ?? "";
805
- if (spinner) styledRow = replaceFirst(styledRow, spinner, colors.cyan(spinner));
806
- styledRow = styledRow.replace(/\|/g, colors.gray("|")).replace(`${warnCount} warn`, colors.yellow(`${warnCount} warn`)).replace(`${errorCount} error`, (errorCount > 0 ? colors.red : colors.gray)(`${errorCount} error`)).replace(/(\d+\/\d+\/\d+\/\d+)/, (match) => state.errored > 0 ? colors.red(match) : colors.gray(match)).replace(/(\d[\d,]* tokens)/g, (match) => colors.cyan(match));
808
+ if (spinner) styledRow = replaceFirst(styledRow, spinner, colors$1.cyan(spinner));
809
+ styledRow = styledRow.replace(/\|/g, colors$1.gray("|")).replace(`${warnCount} warn`, colors$1.yellow(`${warnCount} warn`)).replace(`${errorCount} error`, (errorCount > 0 ? colors$1.red : colors$1.gray)(`${errorCount} error`)).replace(/(\d+\/\d+\/\d+\/\d+)/, (match) => state.errored > 0 ? colors$1.red(match) : colors$1.gray(match)).replace(/(\d[\d,]* tokens)/g, (match) => colors$1.cyan(match));
807
810
  return styledRow;
808
811
  }
809
812
  //#endregion
@@ -1011,6 +1014,7 @@ function resolveConfigRunner(config) {
1011
1014
  }
1012
1015
  function resolveRunnerConfig(setupConfig, config, options) {
1013
1016
  const cache = resolveRunnerCacheConfig(setupConfig.runner?.cache, config.runner?.cache, options);
1017
+ const stats = resolveRunnerStatsConfig(setupConfig.runner?.stats, config.runner?.stats, options);
1014
1018
  const fileConcurrency = parsePositiveIntegerOption(options.fileConcurrency, "--file-concurrency");
1015
1019
  const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
1016
1020
  const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
@@ -1020,6 +1024,7 @@ function resolveRunnerConfig(setupConfig, config, options) {
1020
1024
  cache,
1021
1025
  fileConcurrency: fileConcurrency ?? config.runner?.fileConcurrency ?? setupConfig.runner?.fileConcurrency,
1022
1026
  ruleConcurrency: ruleConcurrency ?? config.runner?.ruleConcurrency ?? setupConfig.runner?.ruleConcurrency,
1027
+ stats,
1023
1028
  timeoutMs: timeoutMs ?? config.runner?.timeoutMs ?? setupConfig.runner?.timeoutMs
1024
1029
  };
1025
1030
  return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
@@ -1033,6 +1038,15 @@ function mergeRunnerCacheConfig(setupCache, configCache) {
1033
1038
  };
1034
1039
  return configCache;
1035
1040
  }
1041
+ function mergeRunnerStatsConfig(setupStats, configStats) {
1042
+ if (configStats === void 0) return setupStats;
1043
+ if (typeof configStats === "boolean") return configStats;
1044
+ if (typeof setupStats === "object") return {
1045
+ ...setupStats,
1046
+ ...configStats
1047
+ };
1048
+ return configStats;
1049
+ }
1036
1050
  function parsePositiveIntegerOption(value, label) {
1037
1051
  if (value === void 0) return;
1038
1052
  const parsed = Number(value);
@@ -1048,6 +1062,304 @@ function resolveRunnerCacheConfig(setupCache, configCache, options) {
1048
1062
  } : { location: options.cacheLocation };
1049
1063
  return configuredCache;
1050
1064
  }
1065
+ function resolveRunnerStatsConfig(setupStats, configStats, options) {
1066
+ if (options.stats === false) return false;
1067
+ return mergeRunnerStatsConfig(setupStats, configStats);
1068
+ }
1069
+ //#endregion
1070
+ //#region src/cli/stats/ci.ts
1071
+ function isCi(env = process.env) {
1072
+ const ci = env.CI;
1073
+ if (ci !== void 0 && ci !== "" && ci !== "0" && ci !== "false") return true;
1074
+ return Boolean(env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.BUILDKITE || env.TF_BUILD);
1075
+ }
1076
+ //#endregion
1077
+ //#region src/cli/stats/since.ts
1078
+ /**
1079
+ * Parse a `--since` time into an inclusive lower-bound timestamp.
1080
+ * This is a tiny helper only to meet the needs.
1081
+ *
1082
+ * Supported formats:
1083
+ * - `Nd`/`Nh` — Now minus N days/hours.
1084
+ * - `YYYY-MM`/`YYYY-MM-DD` — Start of that UTC month/day.
1085
+ *
1086
+ * Edge cases:
1087
+ * - An empty time means "no lower bound".
1088
+ * - An unrecognized time will cause an error to be thrown.
1089
+ */
1090
+ function parseSince(time, now) {
1091
+ if (time === void 0 || time === "") return;
1092
+ const relative = /^(\d+)([dh])$/u.exec(time);
1093
+ if (relative) return now - Number(relative[1]) * (relative[2] === "d" ? 864e5 : 36e5);
1094
+ const calendar = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/u.exec(time);
1095
+ if (calendar) {
1096
+ const year = Number(calendar[1]);
1097
+ const month = Number(calendar[2]);
1098
+ const day = calendar[3] === void 0 ? 1 : Number(calendar[3]);
1099
+ return Date.UTC(year, month - 1, day);
1100
+ }
1101
+ throw new Error(`Invalid --since "${time}": use e.g. 7d, 24h, 2025-01, or 2025-01-15.`);
1102
+ }
1103
+ //#endregion
1104
+ //#region src/cli/stats/jsonl.ts
1105
+ const DEFAULT_RETENTION_MONTHS = 12;
1106
+ const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
1107
+ function createJsonlStatsStore(options) {
1108
+ const now = options.clock ?? Date.now;
1109
+ const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
1110
+ return {
1111
+ query: (filter) => query(options.dir, filter ?? {}, now),
1112
+ record: (input) => record(options.dir, input, now(), retentionMonths)
1113
+ };
1114
+ }
1115
+ function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
1116
+ const row = rows.get(key) ?? {
1117
+ inTok: 0,
1118
+ key,
1119
+ outTok: 0,
1120
+ runs: 0,
1121
+ totalTok: 0
1122
+ };
1123
+ row.inTok += inTok;
1124
+ row.outTok += outTok;
1125
+ row.totalTok += totalTok;
1126
+ if (countRun) row.runs += 1;
1127
+ rows.set(key, row);
1128
+ }
1129
+ function isEnoent(error) {
1130
+ return error instanceof Error && error.code === "ENOENT";
1131
+ }
1132
+ function monthIndexOf(ts) {
1133
+ const date = new Date(ts);
1134
+ return date.getUTCFullYear() * 12 + date.getUTCMonth();
1135
+ }
1136
+ function parseRunLine(line) {
1137
+ try {
1138
+ const value = JSON.parse(line);
1139
+ if (typeof value.ts === "number" && typeof value.cwd === "string" && value.usage != null) return value;
1140
+ return;
1141
+ } catch {
1142
+ return;
1143
+ }
1144
+ }
1145
+ async function prune(dir, ts, retentionMonths) {
1146
+ if (retentionMonths <= 0) return;
1147
+ const cutoff = monthIndexOf(ts) - (retentionMonths - 1);
1148
+ const files = await statsFiles(dir);
1149
+ for (const { month, name } of files) if (month < cutoff) await rm(join(dir, name));
1150
+ }
1151
+ async function query(dir, filter, now) {
1152
+ const dimension = filter.by ?? "model";
1153
+ const since = parseSince(filter.since, now());
1154
+ const runs = await readRuns(dir);
1155
+ const rows = /* @__PURE__ */ new Map();
1156
+ let totalRuns = 0;
1157
+ let totalIn = 0;
1158
+ let totalOut = 0;
1159
+ let totalTok = 0;
1160
+ for (const run of runs) {
1161
+ if (since !== void 0 && run.ts < since) continue;
1162
+ if (filter.cwd !== void 0 && run.cwd !== filter.cwd) continue;
1163
+ totalRuns += 1;
1164
+ totalIn += run.usage.inTok;
1165
+ totalOut += run.usage.outTok;
1166
+ totalTok += run.usage.totalTok;
1167
+ if (dimension === "dir") {
1168
+ accumulate(rows, run.cwd, run.usage.inTok, run.usage.outTok, run.usage.totalTok, true);
1169
+ continue;
1170
+ }
1171
+ const seenKeys = /* @__PURE__ */ new Set();
1172
+ for (const usageRecord of run.usage.records) {
1173
+ const key = recordKey(usageRecord, dimension);
1174
+ accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
1175
+ seenKeys.add(key);
1176
+ }
1177
+ }
1178
+ return {
1179
+ dimension,
1180
+ rows: [...rows.values()].sort((left, right) => right.totalTok - left.totalTok),
1181
+ totalIn,
1182
+ totalOut,
1183
+ totalRuns,
1184
+ totalTok
1185
+ };
1186
+ }
1187
+ async function readRuns(dir) {
1188
+ const files = await statsFiles(dir);
1189
+ const runs = [];
1190
+ for (const { name } of files) {
1191
+ const content = await readFile(join(dir, name), "utf8");
1192
+ for (const line of content.split("\n")) {
1193
+ if (line.trim() === "") continue;
1194
+ const run = parseRunLine(line);
1195
+ if (run) runs.push(run);
1196
+ }
1197
+ }
1198
+ return runs;
1199
+ }
1200
+ async function record(dir, input, ts, retentionMonths) {
1201
+ const stat = {
1202
+ ts,
1203
+ ...input
1204
+ };
1205
+ await mkdir(dir, { recursive: true });
1206
+ await appendFile(join(dir, statsFileName(ts)), `${JSON.stringify(stat)}\n`, "utf8");
1207
+ await prune(dir, ts, retentionMonths);
1208
+ }
1209
+ function recordKey(record, dimension) {
1210
+ if (dimension === "rule") return record.ruleId;
1211
+ if (dimension === "operation") return record.operation ?? record.ruleId;
1212
+ return `${record.providerId}/${record.modelId}`;
1213
+ }
1214
+ function statsFileName(ts) {
1215
+ const date = new Date(ts);
1216
+ return `stats-${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}.jsonl`;
1217
+ }
1218
+ async function statsFiles(dir) {
1219
+ let entries;
1220
+ try {
1221
+ entries = await readdir(dir);
1222
+ } catch (error) {
1223
+ if (isEnoent(error)) return [];
1224
+ throw error;
1225
+ }
1226
+ return entries.map((name) => {
1227
+ const match = STATS_FILE_PATTERN.exec(name);
1228
+ return match ? {
1229
+ month: Number(match[1]) * 12 + (Number(match[2]) - 1),
1230
+ name
1231
+ } : void 0;
1232
+ }).filter((entry) => entry !== void 0).sort((left, right) => left.month - right.month);
1233
+ }
1234
+ //#endregion
1235
+ //#region src/cli/stats/record.ts
1236
+ function createRunStat(input) {
1237
+ return {
1238
+ cwd: input.cwd,
1239
+ durationMs: input.durationMs,
1240
+ ruleCounts: input.ruleCounts,
1241
+ usage: {
1242
+ inTok: input.usage.inputTokens,
1243
+ outTok: input.usage.outputTokens,
1244
+ records: input.usage.records.map(toStatsUsageRecord),
1245
+ totalTok: input.usage.totalTokens
1246
+ }
1247
+ };
1248
+ }
1249
+ function readOperation(metadata) {
1250
+ if (typeof metadata !== "object" || metadata === null || !("operation" in metadata)) return;
1251
+ const operation = metadata.operation;
1252
+ return typeof operation === "string" ? operation : void 0;
1253
+ }
1254
+ function toStatsUsageRecord(record) {
1255
+ return {
1256
+ filePath: record.filePath,
1257
+ inTok: record.inputTokens ?? 0,
1258
+ modelId: record.modelId,
1259
+ operation: readOperation(record.metadata),
1260
+ outTok: record.outputTokens ?? 0,
1261
+ providerId: record.providerId,
1262
+ ruleId: record.ruleId,
1263
+ totalTok: record.totalTokens ?? 0
1264
+ };
1265
+ }
1266
+ //#endregion
1267
+ //#region src/cli/commands/lint/stats.ts
1268
+ function createStatsCollector() {
1269
+ const counts = {
1270
+ cached: 0,
1271
+ completed: 0,
1272
+ errored: 0,
1273
+ planned: 0
1274
+ };
1275
+ let startedAt;
1276
+ let endedAt;
1277
+ return {
1278
+ counts,
1279
+ durationMs: () => startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0,
1280
+ reporter: {
1281
+ onRunEnd: (payload) => {
1282
+ counts.cached = payload.cached;
1283
+ counts.completed = payload.completed;
1284
+ counts.errored = payload.errored;
1285
+ counts.planned = payload.planned;
1286
+ endedAt = payload.endedAt;
1287
+ },
1288
+ onRunStart: (payload) => {
1289
+ startedAt = payload.startedAt;
1290
+ }
1291
+ }
1292
+ };
1293
+ }
1294
+ function mergeProgressReporters(base, extra) {
1295
+ if (!base) return extra;
1296
+ if (!extra) return base;
1297
+ return {
1298
+ onDiagnostic: (payload) => {
1299
+ base.onDiagnostic?.(payload);
1300
+ extra.onDiagnostic?.(payload);
1301
+ },
1302
+ onFileEnd: (payload) => {
1303
+ base.onFileEnd?.(payload);
1304
+ extra.onFileEnd?.(payload);
1305
+ },
1306
+ onFileStart: (payload) => {
1307
+ base.onFileStart?.(payload);
1308
+ extra.onFileStart?.(payload);
1309
+ },
1310
+ onRuleEnd: (payload) => {
1311
+ base.onRuleEnd?.(payload);
1312
+ extra.onRuleEnd?.(payload);
1313
+ },
1314
+ onRuleStart: (payload) => {
1315
+ base.onRuleStart?.(payload);
1316
+ extra.onRuleStart?.(payload);
1317
+ },
1318
+ onRunEnd: (payload) => {
1319
+ base.onRunEnd?.(payload);
1320
+ extra.onRunEnd?.(payload);
1321
+ },
1322
+ onRunStart: (payload) => {
1323
+ base.onRunStart?.(payload);
1324
+ extra.onRunStart?.(payload);
1325
+ },
1326
+ onTargetEnd: (payload) => {
1327
+ base.onTargetEnd?.(payload);
1328
+ extra.onTargetEnd?.(payload);
1329
+ },
1330
+ onTargetStart: (payload) => {
1331
+ base.onTargetStart?.(payload);
1332
+ extra.onTargetStart?.(payload);
1333
+ },
1334
+ onUsage: (payload) => {
1335
+ base.onUsage?.(payload);
1336
+ extra.onUsage?.(payload);
1337
+ }
1338
+ };
1339
+ }
1340
+ function resolveStatsWrite(stats, env) {
1341
+ if (isCi(env)) return;
1342
+ if (!(stats === false ? false : stats === void 0 || stats === true ? true : stats.enabled !== false)) return;
1343
+ const config = typeof stats === "object" ? stats : void 0;
1344
+ return {
1345
+ dir: config?.location ?? getStatsDir(env),
1346
+ retentionMonths: config?.retentionMonths
1347
+ };
1348
+ }
1349
+ async function writeRunStats(target, collector, result, cwd) {
1350
+ const store = createJsonlStatsStore({
1351
+ dir: target.dir,
1352
+ retentionMonths: target.retentionMonths
1353
+ });
1354
+ try {
1355
+ await store.record(createRunStat({
1356
+ cwd,
1357
+ durationMs: collector.durationMs(),
1358
+ ruleCounts: collector.counts,
1359
+ usage: result.usage
1360
+ }));
1361
+ } catch {}
1362
+ }
1051
1363
  //#endregion
1052
1364
  //#region src/cli/commands/lint/index.ts
1053
1365
  const lint = defineCommand({
@@ -1086,6 +1398,8 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1086
1398
  write: (chunk) => io.stderr.write(chunk)
1087
1399
  }) : void 0;
1088
1400
  const restoreProgressConsole = progress ? interceptConsoleOutput({ write: progress.write }) : void 0;
1401
+ const statsTarget = resolveStatsWrite(runner?.stats, io.env);
1402
+ const statsCollector = statsTarget ? createStatsCollector() : void 0;
1089
1403
  let result;
1090
1404
  try {
1091
1405
  result = await runAlint({
@@ -1094,7 +1408,7 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1094
1408
  files: lintFiles,
1095
1409
  modelOverride: options.model,
1096
1410
  outputLanguage: options.outputLanguage,
1097
- progress: progress?.reporter,
1411
+ progress: mergeProgressReporters(progress?.reporter, statsCollector?.reporter),
1098
1412
  runner,
1099
1413
  setupConfig
1100
1414
  });
@@ -1109,6 +1423,7 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1109
1423
  }
1110
1424
  restoreProgressConsole?.();
1111
1425
  progress?.dispose();
1426
+ if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, result, io.cwd);
1112
1427
  io.stdout.write(formatDiagnostics(options.format, result, { color: io.stdout.isTTY === true }));
1113
1428
  return result.diagnostics.length > 0 ? 1 : 0;
1114
1429
  }
@@ -1531,11 +1846,112 @@ function toArray(value) {
1531
1846
  return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
1532
1847
  }
1533
1848
  //#endregion
1849
+ //#region src/cli/commands/stats/format.ts
1850
+ const colors = createColors({ force: true });
1851
+ const DIMENSION_LABEL = {
1852
+ dir: "directory",
1853
+ model: "model",
1854
+ operation: "operation",
1855
+ rule: "rule"
1856
+ };
1857
+ function formatStatsAggregate(aggregate, options = {}) {
1858
+ const paint = (text, style) => options.color === true ? style(text) : text;
1859
+ const runLabel = aggregate.totalRuns === 1 ? "run" : "runs";
1860
+ const lines = [`${aggregate.totalRuns} ${runLabel} — ${formatTokens(aggregate.totalIn)} in / ${formatTokens(aggregate.totalOut)} out / ${paint(`${formatTokens(aggregate.totalTok)} total`, colors.cyan)}`, ""];
1861
+ if (aggregate.rows.length === 0) {
1862
+ lines.push("No stats recorded yet.");
1863
+ return `${lines.join("\n")}\n`;
1864
+ }
1865
+ const header = [
1866
+ DIMENSION_LABEL[aggregate.dimension],
1867
+ "runs",
1868
+ "input",
1869
+ "output",
1870
+ "total"
1871
+ ];
1872
+ const rows = aggregate.rows.map((row) => [
1873
+ row.key,
1874
+ String(row.runs),
1875
+ formatTokens(row.inTok),
1876
+ formatTokens(row.outTok),
1877
+ formatTokens(row.totalTok)
1878
+ ]);
1879
+ const widths = header.map((cell, index) => Math.max(cell.length, ...rows.map((row) => row[index].length)));
1880
+ const renderRow = (cells, isHeader) => cells.map((cell, index) => {
1881
+ const aligned = index === 0 ? cell.padEnd(widths[index]) : cell.padStart(widths[index]);
1882
+ return isHeader ? paint(aligned, colors.bold) : aligned;
1883
+ }).join(" ");
1884
+ lines.push(renderRow(header, true), ...rows.map((row) => renderRow(row, false)));
1885
+ return `${lines.join("\n")}\n`;
1886
+ }
1887
+ function formatTokens(value) {
1888
+ return value.toLocaleString("en-US");
1889
+ }
1890
+ //#endregion
1891
+ //#region src/cli/commands/stats/stats.ts
1892
+ const DIMENSIONS = /* @__PURE__ */ new Set([
1893
+ "dir",
1894
+ "model",
1895
+ "operation",
1896
+ "rule"
1897
+ ]);
1898
+ async function runStatsCommand(options, io) {
1899
+ try {
1900
+ const dimension = parseDimension(options.by);
1901
+ const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd)]);
1902
+ const aggregate = await createJsonlStatsStore({ dir: statsLocation(resolveConfigRunner(config)?.stats) ?? statsLocation(setupConfig.runner?.stats) ?? getStatsDir(io.env) }).query({
1903
+ by: dimension,
1904
+ cwd: options.here === true ? io.cwd : options.cwd,
1905
+ since: options.since
1906
+ });
1907
+ io.stdout.write(options.json === true ? `${JSON.stringify(aggregate, null, 2)}\n` : formatStatsAggregate(aggregate, { color: io.stdout.isTTY === true }));
1908
+ return 0;
1909
+ } catch (error) {
1910
+ io.stderr.write(`${errorMessageFrom$1(error) ?? "Failed to read stats."}\n`);
1911
+ return 2;
1912
+ }
1913
+ }
1914
+ function parseDimension(value) {
1915
+ if (value === void 0) return "model";
1916
+ if (DIMENSIONS.has(value)) return value;
1917
+ throw new Error(`Invalid --by "${value}": use rule, operation, model, or dir.`);
1918
+ }
1919
+ function statsLocation(stats) {
1920
+ return typeof stats === "object" ? stats.location : void 0;
1921
+ }
1922
+ //#endregion
1534
1923
  //#region src/cli/commands/index.ts
1535
1924
  const commandTree = [
1536
1925
  setup,
1537
1926
  config,
1538
1927
  output,
1928
+ defineCommand({
1929
+ action: (context, options) => runStatsCommand(options, context.io),
1930
+ description: "Show recorded run stats",
1931
+ name: "stats",
1932
+ options: [
1933
+ {
1934
+ description: "Group by rule, operation, model, or dir",
1935
+ flags: "--by <dimension>"
1936
+ },
1937
+ {
1938
+ description: "Only include runs since this time (e.g. 7d, 24h, 2025-01)",
1939
+ flags: "--since <time>"
1940
+ },
1941
+ {
1942
+ description: "Only include runs from this directory",
1943
+ flags: "--cwd <path>"
1944
+ },
1945
+ {
1946
+ description: "Only include runs from the current directory",
1947
+ flags: "--here"
1948
+ },
1949
+ {
1950
+ description: "Output JSON",
1951
+ flags: "--json"
1952
+ }
1953
+ ]
1954
+ }),
1539
1955
  lint
1540
1956
  ];
1541
1957
  //#endregion
@@ -1549,7 +1965,7 @@ async function executeCli(argv, io) {
1549
1965
  pendingResult = result;
1550
1966
  return result;
1551
1967
  };
1552
- cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("-l, --lang <language>", "Ask model-backed rules to write diagnostics in this language").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").help();
1968
+ cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("-l, --lang <language>", "Ask model-backed rules to write diagnostics in this language").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--no-stats", "Do not record run stats for this run").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").help();
1553
1969
  registerCommandTree(cli, commandTree, {
1554
1970
  globalOptions,
1555
1971
  interceptConsoleOutput,
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-D7cUTZnE.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-GIOYMybU.mjs";
2
2
  export { executeCli, formatDiagnostics, formatJson, formatStylish };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -25,8 +25,8 @@
25
25
  "pathe": "^2.0.3",
26
26
  "table": "^6.9.0",
27
27
  "tinyrainbow": "^3.1.0",
28
- "@alint-js/core": "0.0.8",
29
- "@alint-js/config": "0.0.8"
28
+ "@alint-js/config": "0.0.9",
29
+ "@alint-js/core": "0.0.9"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^26.0.1",