@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.
- package/dist/bin/index.mjs +1 -1
- package/dist/{cli-D7cUTZnE.mjs → cli-GIOYMybU.mjs} +431 -15
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
package/dist/bin/index.mjs
CHANGED
|
@@ -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$
|
|
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$
|
|
545
|
-
file: colors$
|
|
546
|
-
location: colors$
|
|
547
|
-
ruleId: colors$
|
|
548
|
-
summaryToken: colors$
|
|
549
|
-
warning: colors$
|
|
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-
|
|
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.
|
|
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/
|
|
29
|
-
"@alint-js/
|
|
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",
|