@alint-js/cli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -41
- package/dist/bin/index.mjs +1 -1
- package/dist/{cli-ChymU1Wr.mjs → cli-BsBgbtds.mjs} +1937 -1311
- package/dist/index.mjs +1 -1
- package/dist/lsp-commands.d.mts +16 -0
- package/dist/lsp-commands.mjs +16 -0
- package/dist/{server-DgJa6R5l.mjs → server-C-kuoGl1.mjs} +104 -18
- package/package.json +27 -6
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import { cac } from "cac";
|
|
4
|
-
import { appendFile, mkdir, opendir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
5
|
-
import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, isAcpModel, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, normalizeSetupConfig, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
|
|
6
|
-
import { AlintRunCancelledError, AlintRunError, benchmarkModels, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
|
|
4
|
+
import { appendFile, chmod, mkdir, opendir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, isAcpModel, loadAlintConfig, loadAlintConfigWithMetadata, loadSetupConfig, mergeSetupConfigs, normalizeSetupConfig, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, setStopGateConfig, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
|
|
6
|
+
import { AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, defaultStopGateConfig, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, normalizeRunnerCacheConfig, resolveConfigForDirectory, resolveConfigForFile, resolveStopGateConfig, runAlint, stableHash } from "@alint-js/core";
|
|
7
7
|
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
8
|
-
import { errorMessageFrom } from "@moeru/std
|
|
8
|
+
import { errorMessageFrom } from "@moeru/std";
|
|
9
|
+
import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std/error";
|
|
9
10
|
import { getBorderCharacters, table } from "table";
|
|
10
11
|
import { createColors } from "tinyrainbow";
|
|
11
12
|
import { createCommandModel, startGateway } from "@alint-js/model-adapter-acp";
|
|
12
13
|
import cliSpinners from "cli-spinners";
|
|
13
14
|
import fastStringWidth from "fast-string-width";
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
15
|
+
import { parsePatch } from "diff";
|
|
16
|
+
import { x } from "tinyexec";
|
|
17
|
+
import { Buffer } from "node:buffer";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join as join$1, relative as relative$1 } from "node:path";
|
|
21
|
+
import { isNodeErrorCode } from "@alint-js/utils/node";
|
|
17
22
|
import Gitignore from "gitignore-fs";
|
|
18
23
|
import { Minimatch, minimatch } from "minimatch";
|
|
19
|
-
import
|
|
24
|
+
import fastStringTruncatedWidth from "fast-string-truncated-width";
|
|
25
|
+
import { formatDuration, intervalToDuration } from "date-fns";
|
|
20
26
|
//#region package.json
|
|
21
|
-
var version = "0.
|
|
27
|
+
var version = "0.7.0";
|
|
22
28
|
//#endregion
|
|
23
29
|
//#region src/cli/output.ts
|
|
24
30
|
function escapeLineValue(value) {
|
|
@@ -263,6 +269,115 @@ async function runConfigInspectCommand(path, configPath, io) {
|
|
|
263
269
|
return 0;
|
|
264
270
|
}
|
|
265
271
|
//#endregion
|
|
272
|
+
//#region src/cli/commands/config/integrations/stop-gate.ts
|
|
273
|
+
const set$1 = defineCommand({
|
|
274
|
+
async action(context, options) {
|
|
275
|
+
try {
|
|
276
|
+
let target;
|
|
277
|
+
if (options.target === void 0) target = void 0;
|
|
278
|
+
else if (options.target === "all" || options.target === "dirty-files") target = options.target;
|
|
279
|
+
else throw new Error("Stop Gate target must be \"all\" or \"dirty-files\".");
|
|
280
|
+
let timeoutMs;
|
|
281
|
+
if (options.timeoutMs !== void 0) {
|
|
282
|
+
timeoutMs = Number(options.timeoutMs);
|
|
283
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > maximumStopGateTimeoutMs) throw new Error(`Stop Gate timeout must be an integer from 1 to ${maximumStopGateTimeoutMs}.`);
|
|
284
|
+
}
|
|
285
|
+
const result = await setStopGateConfig({
|
|
286
|
+
configFile: options.config,
|
|
287
|
+
cwd: context.io.cwd,
|
|
288
|
+
target,
|
|
289
|
+
timeoutMs
|
|
290
|
+
});
|
|
291
|
+
context.io.stdout.write(`config: ${result.configFile}\n`);
|
|
292
|
+
writeResolvedConfig((chunk) => context.io.stdout.write(chunk), result.config);
|
|
293
|
+
return 0;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
context.io.stderr.write(`${errorMessageFrom(error) ?? "Failed to write Stop Gate config."}\n`);
|
|
296
|
+
return 2;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
description: "Set Stop Gate config overrides in TOML",
|
|
300
|
+
exactArguments: true,
|
|
301
|
+
name: "set",
|
|
302
|
+
options: [{
|
|
303
|
+
description: "Lint target mode",
|
|
304
|
+
flags: "--target <target>"
|
|
305
|
+
}, {
|
|
306
|
+
description: "Overall Stop Gate timeout in milliseconds",
|
|
307
|
+
flags: "--timeout-ms <ms>"
|
|
308
|
+
}]
|
|
309
|
+
});
|
|
310
|
+
const enable = defineCommand({
|
|
311
|
+
action: (context) => setActivation(context, true),
|
|
312
|
+
description: "Enable Stop Gate for this repository",
|
|
313
|
+
exactArguments: true,
|
|
314
|
+
name: "enable"
|
|
315
|
+
});
|
|
316
|
+
const disable = defineCommand({
|
|
317
|
+
action: (context) => setActivation(context, false),
|
|
318
|
+
description: "Disable Stop Gate for this repository",
|
|
319
|
+
exactArguments: true,
|
|
320
|
+
name: "disable"
|
|
321
|
+
});
|
|
322
|
+
const stopGate$1 = defineCommand({
|
|
323
|
+
children: [
|
|
324
|
+
defineCommand({
|
|
325
|
+
async action(context, options) {
|
|
326
|
+
try {
|
|
327
|
+
const loaded = await loadAlintConfigWithMetadata(context.io.cwd, options.config);
|
|
328
|
+
const resolved = resolveStopGateConfig(loaded.config, context.io.cwd);
|
|
329
|
+
context.io.stdout.write(`config: ${loaded.configFile ?? "<not found>"}\n`);
|
|
330
|
+
writeResolvedConfig((chunk) => context.io.stdout.write(chunk), resolved);
|
|
331
|
+
return 0;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
context.io.stderr.write(`${errorMessageFrom(error) ?? "Failed to read Stop Gate config."}\n`);
|
|
334
|
+
return 2;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
description: "Show resolved Stop Gate config",
|
|
338
|
+
exactArguments: true,
|
|
339
|
+
name: "show"
|
|
340
|
+
}),
|
|
341
|
+
set$1,
|
|
342
|
+
enable,
|
|
343
|
+
disable
|
|
344
|
+
],
|
|
345
|
+
description: "Manage Stop Gate integration config",
|
|
346
|
+
examples: [
|
|
347
|
+
"alint config integrations stop-gate show",
|
|
348
|
+
"alint config integrations stop-gate enable",
|
|
349
|
+
"alint config integrations stop-gate disable",
|
|
350
|
+
"alint config integrations stop-gate set --target all --timeout-ms 1800000"
|
|
351
|
+
],
|
|
352
|
+
name: "stop-gate"
|
|
353
|
+
});
|
|
354
|
+
async function setActivation(context, enabled) {
|
|
355
|
+
try {
|
|
356
|
+
const result = await setStopGateConfig({
|
|
357
|
+
cwd: context.io.cwd,
|
|
358
|
+
enabled
|
|
359
|
+
});
|
|
360
|
+
context.io.stdout.write(`config: ${result.configFile}\n`);
|
|
361
|
+
writeResolvedConfig((chunk) => context.io.stdout.write(chunk), result.config);
|
|
362
|
+
return 0;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
context.io.stderr.write(`${errorMessageFrom(error) ?? "Failed to write Stop Gate activation."}\n`);
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function writeResolvedConfig(write, config) {
|
|
369
|
+
write(`enabled: ${config.enabled}${config.enabled === defaultStopGateConfig.enabled ? " (default)" : ""}\n`);
|
|
370
|
+
write(`target: ${config.target}${config.target === defaultStopGateConfig.target ? " (default)" : ""}\n`);
|
|
371
|
+
write(`timeoutMs: ${config.timeoutMs}${config.timeoutMs === defaultStopGateConfig.timeoutMs ? " (default)" : ""}\n`);
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/cli/commands/config/integrations/integrations.ts
|
|
375
|
+
const integrations$1 = defineCommand({
|
|
376
|
+
children: [stopGate$1],
|
|
377
|
+
description: "Manage external integration configuration",
|
|
378
|
+
name: "integrations"
|
|
379
|
+
});
|
|
380
|
+
//#endregion
|
|
266
381
|
//#region src/cli/reporters/progress/bar.ts
|
|
267
382
|
const minBarWidth = 4;
|
|
268
383
|
const maxBarWidth = 16;
|
|
@@ -986,7 +1101,7 @@ async function runListModelsCommand(context, options) {
|
|
|
986
1101
|
try {
|
|
987
1102
|
providerConcurrency = resolveProviderBenchmarkConcurrency(config, concurrencyOverrides);
|
|
988
1103
|
} catch (error) {
|
|
989
|
-
context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid benchmark concurrency override."}\n`);
|
|
1104
|
+
context.io.stderr.write(`${errorMessageFrom$1(error) ?? "Invalid benchmark concurrency override."}\n`);
|
|
990
1105
|
return 2;
|
|
991
1106
|
}
|
|
992
1107
|
const runtime = await startModelAdapters(config, context.io);
|
|
@@ -1030,7 +1145,7 @@ const probe$1 = defineCommand({
|
|
|
1030
1145
|
context.io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
|
|
1031
1146
|
return 0;
|
|
1032
1147
|
} catch (error) {
|
|
1033
|
-
context.io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
1148
|
+
context.io.stderr.write(`failed to probe models: ${errorMessageFrom$1(error) ?? String(error)}\n`);
|
|
1034
1149
|
return 2;
|
|
1035
1150
|
}
|
|
1036
1151
|
},
|
|
@@ -1167,7 +1282,7 @@ function applyPlans(config, plans) {
|
|
|
1167
1282
|
return plans.reduce((nextConfig, plan) => pruneProviderModels(nextConfig, plan.provider.id, plan.remoteModelIds), config);
|
|
1168
1283
|
}
|
|
1169
1284
|
function classifyProbeFailure(error) {
|
|
1170
|
-
const message = errorMessageFrom(error);
|
|
1285
|
+
const message = errorMessageFrom$1(error);
|
|
1171
1286
|
const httpStatus = message?.match(/^GET .+ returned ([1-5]\d\d)\.$/u)?.[1];
|
|
1172
1287
|
if (httpStatus !== void 0) return `provider returned HTTP ${httpStatus}`;
|
|
1173
1288
|
if (error instanceof TypeError && message === "Expected OpenAI-compatible models response with data array.") return "provider returned an invalid models response";
|
|
@@ -1377,7 +1492,7 @@ const probe = defineCommand({
|
|
|
1377
1492
|
context.io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
|
|
1378
1493
|
return 0;
|
|
1379
1494
|
} catch (error) {
|
|
1380
|
-
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
1495
|
+
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom$1(error) ?? String(error)}\n`);
|
|
1381
1496
|
return 2;
|
|
1382
1497
|
}
|
|
1383
1498
|
},
|
|
@@ -1795,7 +1910,7 @@ function createProviderEditorPrompts(prompts) {
|
|
|
1795
1910
|
}
|
|
1796
1911
|
function formatProbeModelsFailure(endpoint, error) {
|
|
1797
1912
|
const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
|
|
1798
|
-
return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
|
|
1913
|
+
return `Could not probe models: ${errorMessageFrom$1(error)}.${hint}`;
|
|
1799
1914
|
}
|
|
1800
1915
|
function isBackInput(value) {
|
|
1801
1916
|
return value.trim() === "..";
|
|
@@ -2177,7 +2292,7 @@ async function runNonInteractiveUpdate(context, options, providerEndpoint, exist
|
|
|
2177
2292
|
try {
|
|
2178
2293
|
parsedHeaders = parseHeaderList(toArray$1(options.providerHeader));
|
|
2179
2294
|
} catch (error) {
|
|
2180
|
-
context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid provider header."}\n`);
|
|
2295
|
+
context.io.stderr.write(`${errorMessageFrom$1(error) ?? "Invalid provider header."}\n`);
|
|
2181
2296
|
return 2;
|
|
2182
2297
|
}
|
|
2183
2298
|
const draft = {
|
|
@@ -2189,7 +2304,7 @@ async function runNonInteractiveUpdate(context, options, providerEndpoint, exist
|
|
|
2189
2304
|
try {
|
|
2190
2305
|
remoteModelIds = await probeModels(endpoint, draft.headers);
|
|
2191
2306
|
} catch (error) {
|
|
2192
|
-
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? "Unknown error."}\n`);
|
|
2307
|
+
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom$1(error) ?? "Unknown error."}\n`);
|
|
2193
2308
|
return 2;
|
|
2194
2309
|
}
|
|
2195
2310
|
const replaced = replaceSetupProvider(config, draft);
|
|
@@ -2222,7 +2337,7 @@ async function runUpdateProviderCommand(context, options) {
|
|
|
2222
2337
|
providerId = scalarOption(options.provider, "--provider");
|
|
2223
2338
|
providerEndpoint = scalarOption(options.providerEndpoint, "--provider-endpoint");
|
|
2224
2339
|
} catch (error) {
|
|
2225
|
-
context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid provider update option."}\n`);
|
|
2340
|
+
context.io.stderr.write(`${errorMessageFrom$1(error) ?? "Invalid provider update option."}\n`);
|
|
2226
2341
|
return 2;
|
|
2227
2342
|
}
|
|
2228
2343
|
if (providerId === void 0) {
|
|
@@ -2270,6 +2385,7 @@ function toArray$1(value) {
|
|
|
2270
2385
|
const config = defineCommand({
|
|
2271
2386
|
children: [
|
|
2272
2387
|
inspect$2,
|
|
2388
|
+
integrations$1,
|
|
2273
2389
|
models,
|
|
2274
2390
|
defineCommand({
|
|
2275
2391
|
children: [
|
|
@@ -2332,1441 +2448,1927 @@ const config = defineCommand({
|
|
|
2332
2448
|
name: "config"
|
|
2333
2449
|
});
|
|
2334
2450
|
//#endregion
|
|
2335
|
-
//#region src/cli/
|
|
2336
|
-
|
|
2337
|
-
|
|
2451
|
+
//#region src/cli/git.ts
|
|
2452
|
+
const gitTimeoutMs = 6e4;
|
|
2453
|
+
const gitlinkMode = "160000";
|
|
2454
|
+
/**
|
|
2455
|
+
* Collects staged, unstaged, and untracked changes relative to HEAD.
|
|
2456
|
+
* Deleted files and Git-controlled submodule worktrees stay outside the lint boundary.
|
|
2457
|
+
*/
|
|
2458
|
+
async function findDirtyChanges(cwd) {
|
|
2459
|
+
const [tracked, untracked, patch, stagedEntries] = await Promise.all([
|
|
2460
|
+
runGit(cwd, [
|
|
2461
|
+
"diff",
|
|
2462
|
+
"--name-only",
|
|
2463
|
+
"-z",
|
|
2464
|
+
"--diff-filter=ACMRTUXB",
|
|
2465
|
+
"--find-renames",
|
|
2466
|
+
"--ignore-submodules=none",
|
|
2467
|
+
"HEAD",
|
|
2468
|
+
"--"
|
|
2469
|
+
]),
|
|
2470
|
+
runGit(cwd, [
|
|
2471
|
+
"ls-files",
|
|
2472
|
+
"--others",
|
|
2473
|
+
"--exclude-standard",
|
|
2474
|
+
"-z"
|
|
2475
|
+
]),
|
|
2476
|
+
runGit(cwd, [
|
|
2477
|
+
"diff",
|
|
2478
|
+
"--no-color",
|
|
2479
|
+
"--no-ext-diff",
|
|
2480
|
+
"--unified=0",
|
|
2481
|
+
"--diff-filter=ACMRTUXB",
|
|
2482
|
+
"--find-renames",
|
|
2483
|
+
"--ignore-submodules=none",
|
|
2484
|
+
"--submodule=short",
|
|
2485
|
+
"HEAD",
|
|
2486
|
+
"--"
|
|
2487
|
+
]),
|
|
2488
|
+
runGit(cwd, [
|
|
2489
|
+
"ls-files",
|
|
2490
|
+
"--stage",
|
|
2491
|
+
"-z"
|
|
2492
|
+
])
|
|
2493
|
+
]);
|
|
2494
|
+
const gitlinks = parseGitlinks(stagedEntries);
|
|
2495
|
+
const trackedFiles = withoutGitlinks(parseNullSeparatedPaths(tracked), gitlinks);
|
|
2496
|
+
const untrackedFiles = withoutGitlinks(parseNullSeparatedPaths(untracked), gitlinks);
|
|
2497
|
+
const files = [.../* @__PURE__ */ new Set([...trackedFiles, ...untrackedFiles])].sort();
|
|
2498
|
+
const changedLines = changedLinesFromPatch(patch, gitlinks);
|
|
2499
|
+
await Promise.all(untrackedFiles.map(async (file) => {
|
|
2500
|
+
const content = await readFile(resolve(cwd, file), "utf8");
|
|
2501
|
+
const lineCount = content.length === 0 ? 0 : content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
2502
|
+
if (lineCount > 0) changedLines.set(file, [{
|
|
2503
|
+
endLine: lineCount,
|
|
2504
|
+
startLine: 1
|
|
2505
|
+
}]);
|
|
2506
|
+
}));
|
|
2507
|
+
return {
|
|
2508
|
+
changedLines,
|
|
2509
|
+
files
|
|
2510
|
+
};
|
|
2338
2511
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
const
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2512
|
+
async function findGitRoot(cwd) {
|
|
2513
|
+
const root = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
2514
|
+
if (root.length === 0) throw new Error("Git did not return a repository root.");
|
|
2515
|
+
return root;
|
|
2516
|
+
}
|
|
2517
|
+
function changedLinesFromPatch(patch, gitlinks) {
|
|
2518
|
+
const changedLines = /* @__PURE__ */ new Map();
|
|
2519
|
+
for (const file of parsePatch(patch)) {
|
|
2520
|
+
const path = file.newFileName === void 0 || file.newFileName === "/dev/null" ? void 0 : file.newFileName.startsWith("b/") ? file.newFileName.slice(2) : file.newFileName;
|
|
2521
|
+
if (path === void 0 || isWithinGitlink(path, gitlinks)) continue;
|
|
2522
|
+
const lines = [];
|
|
2523
|
+
for (const hunk of file.hunks) {
|
|
2524
|
+
let newLine = hunk.newStart;
|
|
2525
|
+
for (const line of hunk.lines) if (line.startsWith("+")) {
|
|
2526
|
+
lines.push(newLine);
|
|
2527
|
+
newLine += 1;
|
|
2528
|
+
} else if (!line.startsWith("-") && !line.startsWith("\\")) newLine += 1;
|
|
2353
2529
|
}
|
|
2354
|
-
|
|
2530
|
+
const ranges = lineRanges(lines);
|
|
2531
|
+
if (ranges.length > 0) changedLines.set(path, ranges);
|
|
2355
2532
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2533
|
+
return changedLines;
|
|
2534
|
+
}
|
|
2535
|
+
function isWithinGitlink(path, gitlinks) {
|
|
2536
|
+
for (const gitlink of gitlinks) if (path === gitlink || path.startsWith(`${gitlink}/`)) return true;
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
function lineRanges(lines) {
|
|
2540
|
+
const ranges = [];
|
|
2541
|
+
for (const line of lines) {
|
|
2542
|
+
const previous = ranges.at(-1);
|
|
2543
|
+
if (previous && line === previous.endLine + 1) previous.endLine = line;
|
|
2544
|
+
else ranges.push({
|
|
2545
|
+
endLine: line,
|
|
2546
|
+
startLine: line
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
return ranges;
|
|
2550
|
+
}
|
|
2551
|
+
function parseGitlinks(output) {
|
|
2552
|
+
const gitlinks = /* @__PURE__ */ new Set();
|
|
2553
|
+
for (const entry of parseNullSeparatedPaths(output)) {
|
|
2554
|
+
const separator = entry.indexOf(" ");
|
|
2555
|
+
if (separator === -1 || !entry.startsWith(`${gitlinkMode} `)) continue;
|
|
2556
|
+
gitlinks.add(entry.slice(separator + 1));
|
|
2557
|
+
}
|
|
2558
|
+
return gitlinks;
|
|
2559
|
+
}
|
|
2560
|
+
function parseNullSeparatedPaths(output) {
|
|
2561
|
+
return output.split("\0").filter((path) => path.length > 0);
|
|
2562
|
+
}
|
|
2563
|
+
async function runGit(cwd, args) {
|
|
2564
|
+
return (await x("git", args, {
|
|
2565
|
+
nodeOptions: { cwd },
|
|
2566
|
+
nodePath: false,
|
|
2567
|
+
throwOnError: true,
|
|
2568
|
+
timeout: gitTimeoutMs
|
|
2569
|
+
})).stdout;
|
|
2570
|
+
}
|
|
2571
|
+
function withoutGitlinks(paths, gitlinks) {
|
|
2572
|
+
return paths.filter((path) => !isWithinGitlink(path, gitlinks));
|
|
2573
|
+
}
|
|
2574
|
+
//#endregion
|
|
2575
|
+
//#region src/cli/commands/integrations/stop-gate/envelope.ts
|
|
2576
|
+
function writeEnvelope(write, envelope) {
|
|
2577
|
+
write(`${JSON.stringify(envelope)}\n`);
|
|
2578
|
+
}
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/cli/commands/integrations/stop-gate/findings.ts
|
|
2581
|
+
function fingerprintDiagnostics(diagnostics) {
|
|
2582
|
+
const diagnosticHashes = diagnostics.map((diagnostic) => stableHash({
|
|
2583
|
+
evidence: diagnostic.evidence,
|
|
2584
|
+
filePath: diagnostic.filePath,
|
|
2585
|
+
loc: diagnostic.loc,
|
|
2586
|
+
message: diagnostic.message,
|
|
2587
|
+
ruleId: diagnostic.ruleId,
|
|
2588
|
+
severity: diagnostic.severity
|
|
2589
|
+
})).sort();
|
|
2590
|
+
return stableHash({
|
|
2591
|
+
diagnosticHashes,
|
|
2592
|
+
schemaVersion: 1
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
//#endregion
|
|
2596
|
+
//#region src/cli/commands/integrations/stop-gate/report.ts
|
|
2597
|
+
const reportFileName = "report.json";
|
|
2598
|
+
const reportLimitBytes = 104857600;
|
|
2599
|
+
const stopGateTempRoot = join$1(tmpdir(), "alint-stop-gate");
|
|
2600
|
+
var StopGateReportTooLargeError = class extends Error {
|
|
2601
|
+
constructor() {
|
|
2602
|
+
super("Stop Gate report exceeds the 100 MB limit.");
|
|
2603
|
+
this.name = "StopGateReportTooLargeError";
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
async function removeStopGateReport(sessionId) {
|
|
2607
|
+
await rm(getReportPath(sessionId), { force: true });
|
|
2608
|
+
}
|
|
2609
|
+
async function writeStopGateReport(sessionId, report) {
|
|
2610
|
+
const content = `${JSON.stringify(report, null, 2)}\n`;
|
|
2611
|
+
if (Buffer.byteLength(content) > reportLimitBytes) throw new StopGateReportTooLargeError();
|
|
2612
|
+
const sessionDirectory = getSessionDirectory(sessionId);
|
|
2613
|
+
const reportPath = getReportPath(sessionId);
|
|
2614
|
+
const tempPath = join$1(sessionDirectory, `report-${randomUUID()}.tmp`);
|
|
2615
|
+
await mkdir(sessionDirectory, { recursive: true });
|
|
2616
|
+
await writeFile(tempPath, content, {
|
|
2617
|
+
encoding: "utf8",
|
|
2618
|
+
mode: 420
|
|
2619
|
+
});
|
|
2620
|
+
await chmod(tempPath, 420);
|
|
2621
|
+
await rename(tempPath, reportPath);
|
|
2622
|
+
await enforceReportBudget(reportPath);
|
|
2623
|
+
return reportPath;
|
|
2624
|
+
}
|
|
2625
|
+
async function enforceReportBudget(currentReportPath) {
|
|
2626
|
+
let entries;
|
|
2627
|
+
try {
|
|
2628
|
+
entries = await readdir(stopGateTempRoot, { withFileTypes: true });
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
if (isNodeErrorCode(error, "ENOENT")) return;
|
|
2631
|
+
throw error;
|
|
2632
|
+
}
|
|
2633
|
+
const reports = [];
|
|
2634
|
+
for (const entry of entries) {
|
|
2635
|
+
if (!entry.isDirectory()) continue;
|
|
2636
|
+
const path = join$1(stopGateTempRoot, entry.name, reportFileName);
|
|
2637
|
+
try {
|
|
2638
|
+
const stats = await stat(path);
|
|
2639
|
+
reports.push({
|
|
2640
|
+
mtimeMs: stats.mtimeMs,
|
|
2641
|
+
path,
|
|
2642
|
+
size: stats.size
|
|
2643
|
+
});
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
if (!isNodeErrorCode(error, "ENOENT")) throw error;
|
|
2364
2646
|
}
|
|
2365
|
-
lines.push("");
|
|
2366
2647
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2648
|
+
let total = reports.reduce((sum, report) => sum + report.size, 0);
|
|
2649
|
+
for (const report of reports.sort((left, right) => left.mtimeMs - right.mtimeMs)) {
|
|
2650
|
+
if (total <= reportLimitBytes) break;
|
|
2651
|
+
if (report.path === currentReportPath) continue;
|
|
2652
|
+
await rm(report.path, { force: true });
|
|
2653
|
+
total -= report.size;
|
|
2654
|
+
}
|
|
2369
2655
|
}
|
|
2370
|
-
function
|
|
2371
|
-
return
|
|
2656
|
+
function getReportPath(sessionId) {
|
|
2657
|
+
return join$1(getSessionDirectory(sessionId), reportFileName);
|
|
2372
2658
|
}
|
|
2373
|
-
function
|
|
2374
|
-
|
|
2659
|
+
function getSessionDirectory(sessionId) {
|
|
2660
|
+
if (sessionId === "." || sessionId === ".." || !/^[\w.-]+$/u.test(sessionId)) throw new Error("Invalid Stop hook session id.");
|
|
2661
|
+
return join$1(stopGateTempRoot, sessionId);
|
|
2375
2662
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2663
|
+
//#endregion
|
|
2664
|
+
//#region src/cli/commands/lint/discovery.ts
|
|
2665
|
+
const minimatchOptions = { dot: true };
|
|
2666
|
+
const directoryBufferSize = 256;
|
|
2667
|
+
var NoFilesFoundError = class extends Error {
|
|
2668
|
+
globInputPaths;
|
|
2669
|
+
pattern;
|
|
2670
|
+
constructor(pattern, options) {
|
|
2671
|
+
super(`No files matching "${pattern}" were found${options.globInputPaths ? "" : " (glob input is disabled)"}.`);
|
|
2672
|
+
this.name = "NoFilesFoundError";
|
|
2673
|
+
this.globInputPaths = options.globInputPaths;
|
|
2674
|
+
this.pattern = pattern;
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
async function findDirtyLintTargets(config, cwd) {
|
|
2678
|
+
const changes = await findDirtyChanges(cwd);
|
|
2679
|
+
if (changes.files.length === 0) return {
|
|
2680
|
+
changedLines: changes.changedLines,
|
|
2681
|
+
directories: [],
|
|
2682
|
+
files: []
|
|
2384
2683
|
};
|
|
2385
2684
|
return {
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2685
|
+
...await findLintTargets({
|
|
2686
|
+
config,
|
|
2687
|
+
cwd,
|
|
2688
|
+
errorOnUnmatchedPattern: false,
|
|
2689
|
+
globInputPaths: false,
|
|
2690
|
+
inputs: changes.files
|
|
2691
|
+
}),
|
|
2692
|
+
changedLines: changes.changedLines
|
|
2392
2693
|
};
|
|
2393
2694
|
}
|
|
2394
|
-
function
|
|
2395
|
-
|
|
2695
|
+
async function findLintTargets(options) {
|
|
2696
|
+
const { config, cwd, errorOnUnmatchedPattern = true, globInputPaths = true, inputs } = options;
|
|
2697
|
+
const gitignore = shouldFilterGitignoredFiles(config) ? new Gitignore() : void 0;
|
|
2698
|
+
const targets = await resolveInputFiles({
|
|
2699
|
+
config,
|
|
2700
|
+
cwd,
|
|
2701
|
+
errorOnUnmatchedPattern,
|
|
2702
|
+
gitignore,
|
|
2703
|
+
globInputPaths,
|
|
2704
|
+
inputs: inputs.length > 0 ? inputs : ["."]
|
|
2705
|
+
});
|
|
2706
|
+
if (!gitignore || targets.files.length === 0) return targets;
|
|
2707
|
+
const lintFiles = [];
|
|
2708
|
+
for (const file of targets.files) {
|
|
2709
|
+
if (await gitignore.ignores(resolve(cwd, file))) continue;
|
|
2710
|
+
lintFiles.push(file);
|
|
2711
|
+
}
|
|
2712
|
+
return {
|
|
2713
|
+
directories: targets.directories,
|
|
2714
|
+
files: lintFiles
|
|
2715
|
+
};
|
|
2396
2716
|
}
|
|
2397
|
-
function
|
|
2398
|
-
|
|
2399
|
-
return `${execution.cached}/${execution.planned} cached`;
|
|
2717
|
+
function collectGlobalIgnorePatterns(config) {
|
|
2718
|
+
return normalizeConfig(config).flatMap((item) => isGlobalIgnoreItem(item) ? [...item.ignores] : []);
|
|
2400
2719
|
}
|
|
2401
|
-
function
|
|
2402
|
-
const
|
|
2403
|
-
const
|
|
2404
|
-
const
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
return [
|
|
2411
|
-
problemSummary,
|
|
2412
|
-
style.summaryToken(tokens),
|
|
2413
|
-
execution
|
|
2414
|
-
].filter((segment) => segment !== void 0).join(" | ");
|
|
2720
|
+
function getGlobParent(input) {
|
|
2721
|
+
const segments = input.replaceAll("\\", "/").split("/");
|
|
2722
|
+
const parentSegments = [];
|
|
2723
|
+
for (const segment of segments) {
|
|
2724
|
+
if (isGlobPattern(segment)) break;
|
|
2725
|
+
parentSegments.push(segment);
|
|
2726
|
+
}
|
|
2727
|
+
const parent = parentSegments.join("/");
|
|
2728
|
+
return parent === "" ? "." : parent;
|
|
2415
2729
|
}
|
|
2416
|
-
function
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2419
|
-
return `${liveTokens} tokens${cachedTokens > 0 ? ` (${cachedTokens.toLocaleString("en-US")} cached)` : ""}`;
|
|
2730
|
+
function isGlobalIgnoreItem(item) {
|
|
2731
|
+
const keys = Object.keys(item).filter((key) => item[key] !== void 0);
|
|
2732
|
+
return item.ignores !== void 0 && keys.every((key) => key === "ignores" || key === "name");
|
|
2420
2733
|
}
|
|
2421
|
-
function
|
|
2422
|
-
return
|
|
2734
|
+
function isGlobPattern(input) {
|
|
2735
|
+
return new Minimatch(input, minimatchOptions).hasMagic();
|
|
2423
2736
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
function formatDiagnostics(format, result, options = {}) {
|
|
2427
|
-
if (format === "json") return formatJson(result);
|
|
2428
|
-
if (format === "stylish") return formatStylish(result, { color: options.color });
|
|
2429
|
-
throw new Error(`Unknown reporter "${format}".`);
|
|
2737
|
+
function matchesGlob(filePath, pattern) {
|
|
2738
|
+
return minimatch(filePath, pattern.replaceAll("\\", "/"), minimatchOptions);
|
|
2430
2739
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
function createPlainProgressReporter(options) {
|
|
2434
|
-
const writeLine = (line) => options.write(`${line}\n`);
|
|
2435
|
-
return {
|
|
2436
|
-
onJobRetry: () => {},
|
|
2437
|
-
onJobStart: (payload) => {
|
|
2438
|
-
const target = payload.job.target.name ? `${payload.job.target.kind} ${payload.job.target.name}` : payload.job.target.kind;
|
|
2439
|
-
writeLine(`scan ${payload.job.inputPath} > ${target} > ${payload.job.ruleId}`);
|
|
2440
|
-
},
|
|
2441
|
-
onPrepareEnd: ({ filesTotal }) => writeLine(`alint prepared: ${filesTotal} files`),
|
|
2442
|
-
onPrepareStart: () => writeLine("alint preparing"),
|
|
2443
|
-
onRunEnd: (payload) => {
|
|
2444
|
-
const warnCount = countDiagnostics(payload.diagnostics, "warn");
|
|
2445
|
-
const errorCount = countDiagnostics(payload.diagnostics, "error");
|
|
2446
|
-
const state = payload.execution.failed > 0 ? "failed" : "finished";
|
|
2447
|
-
const { cached, cancelled, completed, failed, skipped } = payload.execution;
|
|
2448
|
-
writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens, ${completed} completed, ${cached} cached, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped`);
|
|
2449
|
-
}
|
|
2450
|
-
};
|
|
2740
|
+
function matchesIgnoredDirectory(relativePath, patterns) {
|
|
2741
|
+
return patterns.some((pattern) => minimatch(relativePath, pattern, minimatchOptions) || minimatch(`${relativePath}/`, pattern, minimatchOptions) || minimatch(`${relativePath}/__alint__`, pattern, minimatchOptions));
|
|
2451
2742
|
}
|
|
2452
|
-
function
|
|
2453
|
-
return
|
|
2743
|
+
function normalizeGlobPattern(cwd, pattern) {
|
|
2744
|
+
return isAbsolute(pattern) ? normalizeRelativePath(cwd, pattern) : pattern.replaceAll("\\", "/");
|
|
2454
2745
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
state.totalTokens = Math.max(state.totalTokens - cachedTokens, 0);
|
|
2487
|
-
state.cachedTokens += cachedTokens;
|
|
2746
|
+
function normalizeRelativePath(cwd, filePath) {
|
|
2747
|
+
return relative(cwd, filePath).replaceAll("\\", "/");
|
|
2748
|
+
}
|
|
2749
|
+
async function resolveInputFiles(options) {
|
|
2750
|
+
const hasFilePatterns = hasDiscoveryFilePatterns(options.config);
|
|
2751
|
+
const ignoredPatterns = collectGlobalIgnorePatterns(options.config);
|
|
2752
|
+
const candidates = [];
|
|
2753
|
+
const directories = [];
|
|
2754
|
+
for (const input of options.inputs) {
|
|
2755
|
+
const path = resolve(options.cwd, input);
|
|
2756
|
+
const stats = await statPath(path);
|
|
2757
|
+
if (stats?.isFile()) {
|
|
2758
|
+
candidates.push(input);
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
if (stats?.isDirectory()) {
|
|
2762
|
+
if (await shouldPruneDirectory(path, {
|
|
2763
|
+
cwd: options.cwd,
|
|
2764
|
+
gitignore: options.gitignore,
|
|
2765
|
+
ignoredPatterns
|
|
2766
|
+
})) continue;
|
|
2767
|
+
directories.push(input);
|
|
2768
|
+
if (!hasFilePatterns) continue;
|
|
2769
|
+
const directoryFiles = [];
|
|
2770
|
+
for await (const file of walkFiles(path, {
|
|
2771
|
+
cwd: options.cwd,
|
|
2772
|
+
gitignore: options.gitignore,
|
|
2773
|
+
ignoredPatterns
|
|
2774
|
+
})) {
|
|
2775
|
+
const relativePath = normalizeRelativePath(options.cwd, file);
|
|
2776
|
+
if (hasFilePatterns && matchesDiscoveryFile(relativePath, options.config, { cwd: options.cwd })) directoryFiles.push(relativePath);
|
|
2488
2777
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2778
|
+
for (const file of directoryFiles.sort()) candidates.push(file);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
if (options.globInputPaths && isGlobPattern(input)) {
|
|
2782
|
+
const matches = await searchGlob({
|
|
2783
|
+
config: options.config,
|
|
2784
|
+
cwd: options.cwd,
|
|
2785
|
+
gitignore: options.gitignore,
|
|
2786
|
+
hasFilePatterns,
|
|
2787
|
+
ignoredPatterns,
|
|
2788
|
+
pattern: input
|
|
2500
2789
|
});
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2790
|
+
if (matches.length === 0 && options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
|
|
2791
|
+
for (const match of matches) candidates.push(match);
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
|
|
2795
|
+
}
|
|
2796
|
+
return {
|
|
2797
|
+
directories: [...new Set(directories)],
|
|
2798
|
+
files: [...new Set(candidates)]
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async function searchGlob(options) {
|
|
2802
|
+
const root = resolve(options.cwd, getGlobParent(options.pattern));
|
|
2803
|
+
const rootStats = await statPath(root);
|
|
2804
|
+
const pattern = normalizeGlobPattern(options.cwd, options.pattern);
|
|
2805
|
+
if (!rootStats?.isDirectory()) return [];
|
|
2806
|
+
const candidates = [];
|
|
2807
|
+
for await (const file of walkFiles(root, {
|
|
2808
|
+
cwd: options.cwd,
|
|
2809
|
+
gitignore: options.gitignore,
|
|
2810
|
+
ignoredPatterns: options.ignoredPatterns
|
|
2811
|
+
})) {
|
|
2812
|
+
const relativePath = normalizeRelativePath(options.cwd, file);
|
|
2813
|
+
if (matchesGlob(relativePath, pattern) && shouldIncludeGlobCandidate(relativePath, options.config, {
|
|
2814
|
+
cwd: options.cwd,
|
|
2815
|
+
hasFilePatterns: options.hasFilePatterns
|
|
2816
|
+
})) candidates.push(relativePath);
|
|
2817
|
+
}
|
|
2818
|
+
return candidates.sort();
|
|
2819
|
+
}
|
|
2820
|
+
function shouldFilterGitignoredFiles(config) {
|
|
2821
|
+
return normalizeConfig(config).some((item) => item.ignore?.gitignore === true);
|
|
2822
|
+
}
|
|
2823
|
+
function shouldIncludeGlobCandidate(filePath, config, options) {
|
|
2824
|
+
if (options.hasFilePatterns) return matchesDiscoveryFile(filePath, config, { cwd: options.cwd });
|
|
2825
|
+
return !resolveConfigForFile(filePath, config, { cwd: options.cwd }).ignored;
|
|
2826
|
+
}
|
|
2827
|
+
async function shouldPruneDirectory(path, options) {
|
|
2828
|
+
if (matchesIgnoredDirectory(normalizeRelativePath(options.cwd, path), options.ignoredPatterns)) return true;
|
|
2829
|
+
return await options.gitignore?.ignores(`${path}/`) === true;
|
|
2830
|
+
}
|
|
2831
|
+
async function statPath(path) {
|
|
2832
|
+
try {
|
|
2833
|
+
return await stat(path);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
if (isNodeErrorCode(error, "ENOENT") || isNodeErrorCode(error, "ENOTDIR")) return;
|
|
2836
|
+
throw error;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Streams directory entries so unmatched trees never accumulate into recursive child arrays.
|
|
2841
|
+
* The consumer owns result ordering because filesystem iteration order is not stable.
|
|
2842
|
+
*/
|
|
2843
|
+
async function* walkFiles(root, options) {
|
|
2844
|
+
const pendingDirectories = [root];
|
|
2845
|
+
while (pendingDirectories.length > 0) {
|
|
2846
|
+
const currentDirectory = pendingDirectories.pop();
|
|
2847
|
+
if (currentDirectory === void 0) continue;
|
|
2848
|
+
const childDirectories = [];
|
|
2849
|
+
const directory = await opendir(currentDirectory, { bufferSize: directoryBufferSize });
|
|
2850
|
+
for await (const entry of directory) {
|
|
2851
|
+
const path = resolve(currentDirectory, entry.name);
|
|
2852
|
+
if (entry.isDirectory()) {
|
|
2853
|
+
if (!await shouldPruneDirectory(path, options)) childDirectories.push(path);
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
if (entry.isFile()) yield path;
|
|
2857
|
+
}
|
|
2858
|
+
for (const path of childDirectories.reverse()) pendingDirectories.push(path);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
//#endregion
|
|
2862
|
+
//#region src/cli/runtime/runner.ts
|
|
2863
|
+
/**
|
|
2864
|
+
* Merges the setup config runner with the project config runner. No CLI flag is applied.
|
|
2865
|
+
*
|
|
2866
|
+
* `runner.cache` selects the cache file. The lint command and the language server must merge it
|
|
2867
|
+
* the same way, or the editor reads a different file and reports nothing.
|
|
2868
|
+
*/
|
|
2869
|
+
function mergeRunnerConfigs(setupRunner, configRunner) {
|
|
2870
|
+
const runner = {
|
|
2871
|
+
...setupRunner ?? {},
|
|
2872
|
+
...configRunner ?? {},
|
|
2873
|
+
cache: mergeCacheConfig(setupRunner?.cache, configRunner?.cache),
|
|
2874
|
+
ruleConcurrency: configRunner?.ruleConcurrency ?? setupRunner?.ruleConcurrency,
|
|
2875
|
+
stats: mergeStatsConfig(setupRunner?.stats, configRunner?.stats),
|
|
2876
|
+
timeoutMs: configRunner?.timeoutMs ?? setupRunner?.timeoutMs
|
|
2877
|
+
};
|
|
2878
|
+
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
2879
|
+
}
|
|
2880
|
+
/** The last `runner` block in the flattened config takes precedence, key by key. */
|
|
2881
|
+
function resolveConfigRunner(config) {
|
|
2882
|
+
return normalizeConfig(config).reduce((merged, item) => item.runner ? {
|
|
2883
|
+
...merged,
|
|
2884
|
+
...item.runner
|
|
2885
|
+
} : merged, void 0);
|
|
2886
|
+
}
|
|
2887
|
+
function mergeCacheConfig(setupCache, configCache) {
|
|
2888
|
+
if (configCache === void 0) return setupCache;
|
|
2889
|
+
if (typeof configCache === "boolean") return configCache;
|
|
2890
|
+
if (typeof setupCache === "object") return {
|
|
2891
|
+
...setupCache,
|
|
2892
|
+
...configCache
|
|
2893
|
+
};
|
|
2894
|
+
return configCache;
|
|
2895
|
+
}
|
|
2896
|
+
function mergeStatsConfig(setupStats, configStats) {
|
|
2897
|
+
if (configStats === void 0) return setupStats;
|
|
2898
|
+
if (typeof configStats === "boolean") return configStats;
|
|
2899
|
+
if (typeof setupStats === "object") return {
|
|
2900
|
+
...setupStats,
|
|
2901
|
+
...configStats
|
|
2902
|
+
};
|
|
2903
|
+
return configStats;
|
|
2904
|
+
}
|
|
2905
|
+
//#endregion
|
|
2906
|
+
//#region src/cli/runtime/session.ts
|
|
2907
|
+
/**
|
|
2908
|
+
* Loads config, setup config, and model adapters once, then runs any number of times.
|
|
2909
|
+
*
|
|
2910
|
+
* The lint command and the language server both use this function. A second implementation would
|
|
2911
|
+
* resolve config or cwd differently, and the editor would then read a different cache file.
|
|
2912
|
+
*/
|
|
2913
|
+
async function createRunSession(io, options = {}) {
|
|
2914
|
+
const [{ defaultModel, setupConfig: fileSetupConfig }, config] = await Promise.all([loadRunSetupConfig(io), options.config ?? loadAlintConfig(io.cwd, options.configPath)]);
|
|
2915
|
+
const runtime = await (options.startModelAdapters ?? startModelAdapters)(fileSetupConfig, io);
|
|
2916
|
+
const { setupConfig } = runtime;
|
|
2917
|
+
let shutdown;
|
|
2918
|
+
const runner = mergeRunnerConfigs(setupConfig.runner, resolveConfigRunner(config));
|
|
2919
|
+
return {
|
|
2920
|
+
cache: normalizeRunnerCacheConfig(runner?.cache, io.cwd),
|
|
2921
|
+
config,
|
|
2922
|
+
cwd: io.cwd,
|
|
2923
|
+
defaultModel,
|
|
2924
|
+
run: async (runOptions) => {
|
|
2925
|
+
const targets = runOptions.targets ?? await findLintTargets({
|
|
2926
|
+
config,
|
|
2927
|
+
cwd: io.cwd,
|
|
2928
|
+
errorOnUnmatchedPattern: true,
|
|
2929
|
+
globInputPaths: true,
|
|
2930
|
+
inputs: runOptions.inputs ?? []
|
|
2514
2931
|
});
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2932
|
+
return runAlint({
|
|
2933
|
+
cacheOnly: runOptions.cacheOnly,
|
|
2934
|
+
config,
|
|
2935
|
+
cwd: io.cwd,
|
|
2936
|
+
defaultModel,
|
|
2937
|
+
directories: targets.directories,
|
|
2938
|
+
files: targets.files,
|
|
2939
|
+
modelOverride: runOptions.modelOverride,
|
|
2940
|
+
outputLanguage: runOptions.outputLanguage,
|
|
2941
|
+
progress: runOptions.progress,
|
|
2942
|
+
projectTargets: runOptions.projectTargets,
|
|
2943
|
+
runner: runOptions.runner,
|
|
2944
|
+
setupConfig,
|
|
2945
|
+
signal: runOptions.signal
|
|
2522
2946
|
});
|
|
2523
2947
|
},
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2948
|
+
runner,
|
|
2949
|
+
setupConfig,
|
|
2950
|
+
shutdown: () => shutdown ??= runtime.shutdown()
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
//#endregion
|
|
2954
|
+
//#region src/cli/commands/lint/changed-lines.ts
|
|
2955
|
+
/** Keeps file-wide diagnostics and diagnostics whose locations intersect the current Git change. */
|
|
2956
|
+
function filterResultToChangedLines(result, options) {
|
|
2957
|
+
return {
|
|
2958
|
+
...result,
|
|
2959
|
+
diagnostics: result.diagnostics.filter((diagnostic) => {
|
|
2960
|
+
if (diagnostic.loc === void 0) return true;
|
|
2961
|
+
const changedLinesPath = (isAbsolute(diagnostic.filePath) ? relative(options.cwd, diagnostic.filePath) : diagnostic.filePath).replaceAll("\\", "/");
|
|
2962
|
+
const ranges = options.changedLines.get(changedLinesPath);
|
|
2963
|
+
if (ranges === void 0) return false;
|
|
2964
|
+
const startLine = diagnostic.loc.start.line;
|
|
2965
|
+
const endLine = diagnostic.loc.end?.line ?? startLine;
|
|
2966
|
+
return ranges.some((range) => startLine <= range.endLine && endLine >= range.startLine);
|
|
2967
|
+
})
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
//#endregion
|
|
2971
|
+
//#region src/cli/commands/lint/runner.ts
|
|
2972
|
+
/**
|
|
2973
|
+
* Applies this invocation's flags on top of the project's resolved runner.
|
|
2974
|
+
*
|
|
2975
|
+
* The setup/config merge itself lives in `runtime/runner.ts` because the language server needs the
|
|
2976
|
+
* same base without any flags; only the overrides below are CLI-shaped.
|
|
2977
|
+
*/
|
|
2978
|
+
function resolveRunnerConfig(baseRunner, options) {
|
|
2979
|
+
const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
|
|
2980
|
+
const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
|
|
2981
|
+
const runner = {
|
|
2982
|
+
...baseRunner ?? {},
|
|
2983
|
+
cache: resolveCacheOption(baseRunner?.cache, options),
|
|
2984
|
+
ruleConcurrency: ruleConcurrency ?? baseRunner?.ruleConcurrency,
|
|
2985
|
+
stats: options.stats === false ? false : baseRunner?.stats,
|
|
2986
|
+
timeoutMs: timeoutMs ?? baseRunner?.timeoutMs
|
|
2987
|
+
};
|
|
2988
|
+
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
2989
|
+
}
|
|
2990
|
+
function parsePositiveIntegerOption(value, label) {
|
|
2991
|
+
if (value === void 0) return;
|
|
2992
|
+
const parsed = Number(value);
|
|
2993
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
|
|
2994
|
+
return parsed;
|
|
2995
|
+
}
|
|
2996
|
+
function resolveCacheOption(baseCache, options) {
|
|
2997
|
+
if (options.cache === false) return false;
|
|
2998
|
+
if (options.cacheLocation !== void 0) return typeof baseCache === "object" ? {
|
|
2999
|
+
...baseCache,
|
|
3000
|
+
location: options.cacheLocation
|
|
3001
|
+
} : { location: options.cacheLocation };
|
|
3002
|
+
return baseCache;
|
|
3003
|
+
}
|
|
3004
|
+
//#endregion
|
|
3005
|
+
//#region src/cli/stats/ci.ts
|
|
3006
|
+
function isCi(env = process.env) {
|
|
3007
|
+
const ci = env.CI;
|
|
3008
|
+
if (ci !== void 0 && ci !== "" && ci !== "0" && ci !== "false") return true;
|
|
3009
|
+
return Boolean(env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.BUILDKITE || env.TF_BUILD);
|
|
3010
|
+
}
|
|
3011
|
+
//#endregion
|
|
3012
|
+
//#region src/cli/stats/since.ts
|
|
3013
|
+
/**
|
|
3014
|
+
* Parse a `--since` time into an inclusive lower-bound timestamp.
|
|
3015
|
+
*
|
|
3016
|
+
* Supported formats:
|
|
3017
|
+
* - `Nh`/`Nd`/`Nw`/`Nm`/`Ny` — now minus N hours/days/weeks/months/years.
|
|
3018
|
+
* Weeks, months, and years are fixed approximations (7/30/365 days). `m` is
|
|
3019
|
+
* months here, not minutes — a day is the finest window this tool tracks.
|
|
3020
|
+
* - `YYYY-MM`/`YYYY-MM-DD` — start of that UTC month/day.
|
|
3021
|
+
*
|
|
3022
|
+
* Edge cases:
|
|
3023
|
+
* - An empty time means "no lower bound".
|
|
3024
|
+
* - An unrecognized time throws.
|
|
3025
|
+
*/
|
|
3026
|
+
const HOUR_MS = 36e5;
|
|
3027
|
+
const UNIT_MS = {
|
|
3028
|
+
d: 24 * HOUR_MS,
|
|
3029
|
+
h: HOUR_MS,
|
|
3030
|
+
m: 720 * HOUR_MS,
|
|
3031
|
+
w: 168 * HOUR_MS,
|
|
3032
|
+
y: 8760 * HOUR_MS
|
|
3033
|
+
};
|
|
3034
|
+
function parseSince(time, now) {
|
|
3035
|
+
if (time === void 0 || time === "") return;
|
|
3036
|
+
const relative = /^(\d+)([dhmwy])$/u.exec(time);
|
|
3037
|
+
if (relative) return now - Number(relative[1]) * UNIT_MS[relative[2]];
|
|
3038
|
+
const calendar = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/u.exec(time);
|
|
3039
|
+
if (calendar) {
|
|
3040
|
+
const year = Number(calendar[1]);
|
|
3041
|
+
const month = Number(calendar[2]);
|
|
3042
|
+
const day = calendar[3] === void 0 ? 1 : Number(calendar[3]);
|
|
3043
|
+
return Date.UTC(year, month - 1, day);
|
|
3044
|
+
}
|
|
3045
|
+
throw new Error(`Invalid --since "${time}": use e.g. 24h, 7d, 2w, 3m, 1y, 2025-01, or 2025-01-15.`);
|
|
3046
|
+
}
|
|
3047
|
+
//#endregion
|
|
3048
|
+
//#region src/cli/stats/jsonl.ts
|
|
3049
|
+
const DEFAULT_RETENTION_MONTHS = 12;
|
|
3050
|
+
const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
|
|
3051
|
+
const DAY_MS = 864e5;
|
|
3052
|
+
const MONTH_LABELS$1 = [
|
|
3053
|
+
"Jan",
|
|
3054
|
+
"Feb",
|
|
3055
|
+
"Mar",
|
|
3056
|
+
"Apr",
|
|
3057
|
+
"May",
|
|
3058
|
+
"Jun",
|
|
3059
|
+
"Jul",
|
|
3060
|
+
"Aug",
|
|
3061
|
+
"Sep",
|
|
3062
|
+
"Oct",
|
|
3063
|
+
"Nov",
|
|
3064
|
+
"Dec"
|
|
3065
|
+
];
|
|
3066
|
+
function createJsonlStatsStore(options) {
|
|
3067
|
+
const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
|
|
3068
|
+
return {
|
|
3069
|
+
query: (filter) => query(options.dir, filter ?? {}, Date.now),
|
|
3070
|
+
querySeries: (filter) => querySeries(options.dir, filter ?? {}, Date.now),
|
|
3071
|
+
record: (input) => record(options.dir, input, Date.now(), retentionMonths)
|
|
2571
3072
|
};
|
|
2572
3073
|
}
|
|
2573
|
-
function
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
3074
|
+
function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
|
|
3075
|
+
const row = rows.get(key) ?? {
|
|
3076
|
+
inTok: 0,
|
|
3077
|
+
key,
|
|
3078
|
+
outTok: 0,
|
|
3079
|
+
runs: 0,
|
|
3080
|
+
totalTok: 0
|
|
3081
|
+
};
|
|
3082
|
+
row.inTok += inTok;
|
|
3083
|
+
row.outTok += outTok;
|
|
3084
|
+
row.totalTok += totalTok;
|
|
3085
|
+
if (countRun) row.runs += 1;
|
|
3086
|
+
rows.set(key, row);
|
|
3087
|
+
return row;
|
|
2579
3088
|
}
|
|
2580
|
-
function
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
return left.job.index - right.job.index;
|
|
3089
|
+
function bucketLabel(startMs, interval) {
|
|
3090
|
+
const date = new Date(startMs);
|
|
3091
|
+
if (interval === "month") return MONTH_LABELS$1[date.getUTCMonth()];
|
|
3092
|
+
return `${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
2585
3093
|
}
|
|
2586
|
-
function
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
3094
|
+
function bucketStart(ts, interval) {
|
|
3095
|
+
const date = new Date(ts);
|
|
3096
|
+
if (interval === "month") return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
|
|
3097
|
+
const dayStart = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
3098
|
+
if (interval === "week") return dayStart - (new Date(dayStart).getUTCDay() + 6) % 7 * DAY_MS;
|
|
3099
|
+
return dayStart;
|
|
3100
|
+
}
|
|
3101
|
+
function chooseInterval(since, nowMs, timestamps) {
|
|
3102
|
+
if (since !== void 0) return intervalForDuration(nowMs - since);
|
|
3103
|
+
if (timestamps.length < 2) return "day";
|
|
3104
|
+
return intervalForDuration(Math.max(...timestamps) - Math.min(...timestamps));
|
|
3105
|
+
}
|
|
3106
|
+
function fillBuckets(buckets, interval) {
|
|
3107
|
+
if (buckets.size === 0) return [];
|
|
3108
|
+
const starts = [...buckets.keys()].sort((left, right) => left - right);
|
|
3109
|
+
const last = starts.at(-1);
|
|
3110
|
+
const filled = [];
|
|
3111
|
+
for (let startMs = starts[0]; startMs <= last; startMs = nextBucketStart(startMs, interval)) filled.push(buckets.get(startMs) ?? {
|
|
3112
|
+
inTok: 0,
|
|
3113
|
+
key: bucketLabel(startMs, interval),
|
|
3114
|
+
outTok: 0,
|
|
3115
|
+
runs: 0,
|
|
3116
|
+
startMs,
|
|
3117
|
+
totalTok: 0
|
|
3118
|
+
});
|
|
3119
|
+
return filled;
|
|
3120
|
+
}
|
|
3121
|
+
function intervalForDuration(ms) {
|
|
3122
|
+
const days = ms / DAY_MS;
|
|
3123
|
+
if (days <= 21) return "day";
|
|
3124
|
+
if (days <= 120) return "week";
|
|
3125
|
+
return "month";
|
|
3126
|
+
}
|
|
3127
|
+
function isEnoent(error) {
|
|
3128
|
+
return error instanceof Error && error.code === "ENOENT";
|
|
3129
|
+
}
|
|
3130
|
+
function monthIndexOf(ts) {
|
|
3131
|
+
const date = new Date(ts);
|
|
3132
|
+
return date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
3133
|
+
}
|
|
3134
|
+
function nextBucketStart(startMs, interval) {
|
|
3135
|
+
if (interval === "month") {
|
|
3136
|
+
const date = new Date(startMs);
|
|
3137
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
|
|
2592
3138
|
}
|
|
2593
|
-
return
|
|
3139
|
+
return startMs + (interval === "week" ? 7 * DAY_MS : DAY_MS);
|
|
2594
3140
|
}
|
|
2595
|
-
function
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
running: 0,
|
|
2604
|
-
skipped: 0
|
|
2605
|
-
};
|
|
3141
|
+
function parseRunLine(line) {
|
|
3142
|
+
try {
|
|
3143
|
+
const value = JSON.parse(line);
|
|
3144
|
+
if (typeof value.ts === "number" && typeof value.cwd === "string" && value.usage != null) return value;
|
|
3145
|
+
return;
|
|
3146
|
+
} catch {
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
2606
3149
|
}
|
|
2607
|
-
function
|
|
2608
|
-
return
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
errorCount: 0,
|
|
2613
|
-
execution: createCounts(),
|
|
2614
|
-
jobTokens: /* @__PURE__ */ new Map(),
|
|
2615
|
-
progress: createProgress(),
|
|
2616
|
-
ruleGroups: /* @__PURE__ */ new Map(),
|
|
2617
|
-
totalTokens: 0,
|
|
2618
|
-
warnCount: 0
|
|
2619
|
-
};
|
|
3150
|
+
async function prune(dir, ts, retentionMonths) {
|
|
3151
|
+
if (retentionMonths <= 0) return;
|
|
3152
|
+
const cutoff = monthIndexOf(ts) - (retentionMonths - 1);
|
|
3153
|
+
const files = await statsFiles(dir);
|
|
3154
|
+
for (const { month, name } of files) if (month < cutoff) await rm(join(dir, name));
|
|
2620
3155
|
}
|
|
2621
|
-
function
|
|
3156
|
+
async function query(dir, filter, now) {
|
|
3157
|
+
const dimension = filter.by ?? "model";
|
|
3158
|
+
const since = parseSince(filter.since, now());
|
|
3159
|
+
const rules = ruleSet(filter.rules);
|
|
3160
|
+
const runs = await readRuns(dir);
|
|
3161
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3162
|
+
let totalRuns = 0;
|
|
3163
|
+
let totalIn = 0;
|
|
3164
|
+
let totalOut = 0;
|
|
3165
|
+
let totalTok = 0;
|
|
3166
|
+
for (const run of runs) {
|
|
3167
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
3168
|
+
const slice = sliceRun(run, rules);
|
|
3169
|
+
if (!slice.touched) continue;
|
|
3170
|
+
totalRuns += 1;
|
|
3171
|
+
totalIn += slice.inTok;
|
|
3172
|
+
totalOut += slice.outTok;
|
|
3173
|
+
totalTok += slice.totalTok;
|
|
3174
|
+
if (dimension === "dir") {
|
|
3175
|
+
accumulate(rows, run.cwd, slice.inTok, slice.outTok, slice.totalTok, true);
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
3179
|
+
for (const usageRecord of slice.usageRecords) {
|
|
3180
|
+
const key = recordKey(usageRecord, dimension);
|
|
3181
|
+
accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
|
|
3182
|
+
seenKeys.add(key);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
2622
3185
|
return {
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
planningComplete: false
|
|
3186
|
+
dimension,
|
|
3187
|
+
rows: [...rows.values()].sort((left, right) => right.totalTok - left.totalTok),
|
|
3188
|
+
totalIn,
|
|
3189
|
+
totalOut,
|
|
3190
|
+
totalRuns,
|
|
3191
|
+
totalTok
|
|
2630
3192
|
};
|
|
2631
3193
|
}
|
|
2632
|
-
function
|
|
2633
|
-
|
|
2634
|
-
const
|
|
2635
|
-
const
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}
|
|
2645
|
-
|
|
3194
|
+
async function querySeries(dir, filter, now) {
|
|
3195
|
+
const nowMs = now();
|
|
3196
|
+
const since = parseSince(filter.since, nowMs);
|
|
3197
|
+
const rules = ruleSet(filter.rules);
|
|
3198
|
+
const runs = await readRuns(dir);
|
|
3199
|
+
const sliced = [];
|
|
3200
|
+
for (const run of runs) {
|
|
3201
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
3202
|
+
const slice = sliceRun(run, rules);
|
|
3203
|
+
if (slice.touched) sliced.push({
|
|
3204
|
+
slice,
|
|
3205
|
+
ts: run.ts
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
const interval = filter.interval ?? chooseInterval(since, nowMs, sliced.map((entry) => entry.ts));
|
|
3209
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3210
|
+
let totalRuns = 0;
|
|
3211
|
+
let totalIn = 0;
|
|
3212
|
+
let totalOut = 0;
|
|
3213
|
+
let totalTok = 0;
|
|
3214
|
+
for (const { slice, ts } of sliced) {
|
|
3215
|
+
const startMs = bucketStart(ts, interval);
|
|
3216
|
+
const bucket = buckets.get(startMs) ?? {
|
|
3217
|
+
inTok: 0,
|
|
3218
|
+
key: bucketLabel(startMs, interval),
|
|
3219
|
+
outTok: 0,
|
|
3220
|
+
runs: 0,
|
|
3221
|
+
startMs,
|
|
3222
|
+
totalTok: 0
|
|
3223
|
+
};
|
|
3224
|
+
bucket.runs += 1;
|
|
3225
|
+
bucket.inTok += slice.inTok;
|
|
3226
|
+
bucket.outTok += slice.outTok;
|
|
3227
|
+
bucket.totalTok += slice.totalTok;
|
|
3228
|
+
buckets.set(startMs, bucket);
|
|
3229
|
+
totalRuns += 1;
|
|
3230
|
+
totalIn += slice.inTok;
|
|
3231
|
+
totalOut += slice.outTok;
|
|
3232
|
+
totalTok += slice.totalTok;
|
|
3233
|
+
}
|
|
2646
3234
|
return {
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
ruleId: job.ruleId,
|
|
2654
|
-
skipped: 0,
|
|
2655
|
-
terminalDurationMs: 0
|
|
3235
|
+
buckets: fillBuckets(buckets, interval),
|
|
3236
|
+
interval,
|
|
3237
|
+
totalIn,
|
|
3238
|
+
totalOut,
|
|
3239
|
+
totalRuns,
|
|
3240
|
+
totalTok
|
|
2656
3241
|
};
|
|
2657
3242
|
}
|
|
2658
|
-
function
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
return `eta ~${formatDuration$1(durationMs * (planned - terminal) / terminal)}`;
|
|
2669
|
-
}
|
|
2670
|
-
function fitRow(row, columns) {
|
|
2671
|
-
if (columns <= 0) return "";
|
|
2672
|
-
if (fastStringWidth(row) <= columns) return row;
|
|
2673
|
-
let limit = columns;
|
|
2674
|
-
while (limit > 0) {
|
|
2675
|
-
const result = fastStringTruncatedWidth(row, {
|
|
2676
|
-
ellipsis: "…",
|
|
2677
|
-
limit
|
|
2678
|
-
});
|
|
2679
|
-
const index = completeGraphemeBoundary(row, result.index);
|
|
2680
|
-
const visible = row.slice(0, index);
|
|
2681
|
-
const fitted = `${visible}${visible.includes("\x1B") ? "\x1B[0m" : ""}${result.ellipsed ? "…" : ""}`;
|
|
2682
|
-
const overflow = fastStringWidth(fitted) - columns;
|
|
2683
|
-
if (overflow <= 0) return fitted;
|
|
2684
|
-
limit -= overflow;
|
|
3243
|
+
async function readRuns(dir) {
|
|
3244
|
+
const files = await statsFiles(dir);
|
|
3245
|
+
const runs = [];
|
|
3246
|
+
for (const { name } of files) {
|
|
3247
|
+
const content = await readFile(join(dir, name), "utf8");
|
|
3248
|
+
for (const line of content.split("\n")) {
|
|
3249
|
+
if (line.trim() === "") continue;
|
|
3250
|
+
const run = parseRunLine(line);
|
|
3251
|
+
if (run) runs.push(run);
|
|
3252
|
+
}
|
|
2685
3253
|
}
|
|
2686
|
-
return
|
|
3254
|
+
return runs;
|
|
2687
3255
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
3256
|
+
async function record(dir, input, ts, retentionMonths) {
|
|
3257
|
+
const stat = {
|
|
3258
|
+
ts,
|
|
3259
|
+
...input
|
|
3260
|
+
};
|
|
3261
|
+
await mkdir(dir, { recursive: true });
|
|
3262
|
+
await appendFile(join(dir, statsFileName(ts)), `${JSON.stringify(stat)}\n`, "utf8");
|
|
3263
|
+
await prune(dir, ts, retentionMonths);
|
|
2690
3264
|
}
|
|
2691
|
-
function
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
if (contentBudget === 1) {
|
|
2696
|
-
const failedGroup = renderableGroups.find((group) => group.failed > 0);
|
|
2697
|
-
if (failedGroup) return [formatRuleRow(failedGroup, state, options)];
|
|
2698
|
-
if (runningCount > 0) return [formatCollapsedRunningRow(runningCount, options)];
|
|
2699
|
-
}
|
|
2700
|
-
for (const [groupIndex, group] of renderableGroups.entries()) {
|
|
2701
|
-
if (rows.length >= contentBudget) break;
|
|
2702
|
-
const groupStart = rows.length;
|
|
2703
|
-
rows.push(formatRuleRow(group, state, options));
|
|
2704
|
-
const runningJobs = group.jobs.filter((job) => job.state === "running").sort(compareRunningJobs);
|
|
2705
|
-
const remainingGroups = renderableGroups.length - groupIndex - 1;
|
|
2706
|
-
for (const [index, job] of runningJobs.entries()) {
|
|
2707
|
-
const rowsReservedForLaterGroups = Math.min(remainingGroups, Math.max(contentBudget - rows.length, 0));
|
|
2708
|
-
if (rows.length >= contentBudget) {
|
|
2709
|
-
const hidden = runningJobs.length - index;
|
|
2710
|
-
if (rows.length > groupStart + 1 && hidden > 0) rows[rows.length - 1] = formatCollapsedRunningRow(hidden, options);
|
|
2711
|
-
break;
|
|
2712
|
-
}
|
|
2713
|
-
if (rows.length + 1 + rowsReservedForLaterGroups >= contentBudget) {
|
|
2714
|
-
if (runningJobs.length > 1) rows.push(formatCollapsedRunningRow(runningJobs.length - index, options));
|
|
2715
|
-
break;
|
|
2716
|
-
}
|
|
2717
|
-
rows.push(formatTargetRow(job, options, now, index === runningJobs.length - 1));
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
|
-
return rows;
|
|
3265
|
+
function recordKey(record, dimension) {
|
|
3266
|
+
if (dimension === "rule") return record.ruleId;
|
|
3267
|
+
if (dimension === "operation") return record.operation ?? record.ruleId;
|
|
3268
|
+
return `${record.providerId}/${record.modelId}`;
|
|
2721
3269
|
}
|
|
2722
|
-
function
|
|
2723
|
-
|
|
2724
|
-
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
2725
|
-
return formatDuration(intervalToDuration({
|
|
2726
|
-
end: durationMs,
|
|
2727
|
-
start: 0
|
|
2728
|
-
}));
|
|
3270
|
+
function ruleSet(rules) {
|
|
3271
|
+
return rules && rules.length > 0 ? new Set(rules) : void 0;
|
|
2729
3272
|
}
|
|
2730
|
-
function
|
|
2731
|
-
|
|
2732
|
-
const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
|
|
2733
|
-
const progressBar = state.progress.planningComplete ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
|
|
2734
|
-
const eta = state.progress.planningComplete ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
|
|
2735
|
-
const projectedTokens = state.progress.planningComplete && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
2736
|
-
const discovering = state.progress.planningComplete ? "" : " jobs (discovering)";
|
|
2737
|
-
return [
|
|
2738
|
-
fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration$1(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
|
|
2739
|
-
fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
|
|
2740
|
-
fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
|
|
2741
|
-
];
|
|
3273
|
+
function runInWindow(run, since, cwd) {
|
|
3274
|
+
return (since === void 0 || run.ts >= since) && (cwd === void 0 || run.cwd === cwd);
|
|
2742
3275
|
}
|
|
2743
|
-
function
|
|
2744
|
-
if (
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
}
|
|
3276
|
+
function sliceRun(run, rules) {
|
|
3277
|
+
if (rules === void 0) return {
|
|
3278
|
+
inTok: run.usage.inTok,
|
|
3279
|
+
outTok: run.usage.outTok,
|
|
3280
|
+
totalTok: run.usage.totalTok,
|
|
3281
|
+
touched: true,
|
|
3282
|
+
usageRecords: run.usage.records
|
|
3283
|
+
};
|
|
3284
|
+
const usageRecords = run.usage.records.filter((record) => rules.has(record.ruleId));
|
|
3285
|
+
return {
|
|
3286
|
+
inTok: sumBy(usageRecords, (record) => record.inTok),
|
|
3287
|
+
outTok: sumBy(usageRecords, (record) => record.outTok),
|
|
3288
|
+
totalTok: sumBy(usageRecords, (record) => record.totalTok),
|
|
3289
|
+
touched: usageRecords.length > 0,
|
|
3290
|
+
usageRecords
|
|
3291
|
+
};
|
|
2751
3292
|
}
|
|
2752
|
-
function
|
|
2753
|
-
const
|
|
2754
|
-
|
|
2755
|
-
const percent = group.planned > 0 ? Math.floor(terminal / group.planned * 100) : 0;
|
|
2756
|
-
const bar = formatMiniBarSegment(terminal, group.planned, state.animationTick, options);
|
|
2757
|
-
const running = group.jobs.filter((job) => job.state === "running").length;
|
|
2758
|
-
const failed = group.failed > 0 ? ` ${group.failed} failed` : "";
|
|
2759
|
-
const eta = estimateEta(group.terminalDurationMs, terminal, group.planned);
|
|
2760
|
-
return fitRow(`${spinner} ${group.ruleId} ${terminal}/${group.planned} ${percent}%${bar} ${eta} ${running} running${failed}`, options.columns);
|
|
3293
|
+
function statsFileName(ts) {
|
|
3294
|
+
const date = new Date(ts);
|
|
3295
|
+
return `stats-${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}.jsonl`;
|
|
2761
3296
|
}
|
|
2762
|
-
function
|
|
2763
|
-
|
|
3297
|
+
async function statsFiles(dir) {
|
|
3298
|
+
let entries;
|
|
3299
|
+
try {
|
|
3300
|
+
entries = await readdir(dir);
|
|
3301
|
+
} catch (error) {
|
|
3302
|
+
if (isEnoent(error)) return [];
|
|
3303
|
+
throw error;
|
|
3304
|
+
}
|
|
3305
|
+
return entries.map((name) => {
|
|
3306
|
+
const match = STATS_FILE_PATTERN.exec(name);
|
|
3307
|
+
return match ? {
|
|
3308
|
+
month: Number(match[1]) * 12 + (Number(match[2]) - 1),
|
|
3309
|
+
name
|
|
3310
|
+
} : void 0;
|
|
3311
|
+
}).filter((entry) => entry !== void 0).sort((left, right) => left.month - right.month);
|
|
2764
3312
|
}
|
|
2765
|
-
function
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
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);
|
|
3313
|
+
function sumBy(items, select) {
|
|
3314
|
+
let total = 0;
|
|
3315
|
+
for (const item of items) total += select(item);
|
|
3316
|
+
return total;
|
|
2770
3317
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
3318
|
+
//#endregion
|
|
3319
|
+
//#region src/cli/stats/record.ts
|
|
3320
|
+
function createRunStat(input) {
|
|
3321
|
+
return {
|
|
3322
|
+
cwd: input.cwd,
|
|
3323
|
+
durationMs: input.durationMs,
|
|
3324
|
+
ruleCounts: input.ruleCounts,
|
|
3325
|
+
usage: {
|
|
3326
|
+
inTok: input.usage.inputTokens,
|
|
3327
|
+
outTok: input.usage.outputTokens,
|
|
3328
|
+
records: input.usage.records.map(toStatsUsageRecord),
|
|
3329
|
+
totalTok: input.usage.totalTokens
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
2776
3332
|
}
|
|
2777
|
-
function
|
|
2778
|
-
|
|
3333
|
+
function readOperation(metadata) {
|
|
3334
|
+
if (typeof metadata !== "object" || metadata === null || !("operation" in metadata)) return;
|
|
3335
|
+
const operation = metadata.operation;
|
|
3336
|
+
return typeof operation === "string" ? operation : void 0;
|
|
2779
3337
|
}
|
|
2780
|
-
function
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
3338
|
+
function toStatsUsageRecord(record) {
|
|
3339
|
+
return {
|
|
3340
|
+
filePath: record.filePath,
|
|
3341
|
+
inTok: record.inputTokens ?? 0,
|
|
3342
|
+
modelId: record.modelId,
|
|
3343
|
+
operation: readOperation(record.metadata),
|
|
3344
|
+
outTok: record.outputTokens ?? 0,
|
|
3345
|
+
providerId: record.providerId,
|
|
3346
|
+
ruleId: record.ruleId,
|
|
3347
|
+
totalTok: record.totalTokens ?? 0
|
|
2785
3348
|
};
|
|
2786
3349
|
}
|
|
2787
3350
|
//#endregion
|
|
2788
|
-
//#region src/cli/
|
|
2789
|
-
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
let sequence = clearCurrentLine;
|
|
2797
|
-
for (let row = 1; row < previousRows; row += 1) sequence += clearPreviousLine;
|
|
2798
|
-
options.write(sequence);
|
|
2799
|
-
previousRows = 0;
|
|
2800
|
-
};
|
|
2801
|
-
const render = () => {
|
|
2802
|
-
clearPreviousFrame();
|
|
2803
|
-
const rows = options.getRows();
|
|
2804
|
-
if (rows.length === 0) return;
|
|
2805
|
-
options.write(rows.join("\n"));
|
|
2806
|
-
previousRows = rows.length;
|
|
2807
|
-
};
|
|
2808
|
-
const write = (chunk) => {
|
|
2809
|
-
const wasRendering = interval !== void 0;
|
|
2810
|
-
clearPreviousFrame();
|
|
2811
|
-
options.write(chunk);
|
|
2812
|
-
if (wasRendering) render();
|
|
3351
|
+
//#region src/cli/commands/lint/stats.ts
|
|
3352
|
+
function createStatsCollector() {
|
|
3353
|
+
const counts = {
|
|
3354
|
+
cached: 0,
|
|
3355
|
+
cancelled: 0,
|
|
3356
|
+
completed: 0,
|
|
3357
|
+
failed: 0,
|
|
3358
|
+
planned: 0
|
|
2813
3359
|
};
|
|
3360
|
+
let startedAt;
|
|
3361
|
+
let endedAt;
|
|
2814
3362
|
return {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3363
|
+
counts,
|
|
3364
|
+
durationMs: () => startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0,
|
|
3365
|
+
reporter: {
|
|
3366
|
+
onPrepareStart: (payload) => {
|
|
3367
|
+
startedAt = payload.startedAt;
|
|
3368
|
+
},
|
|
3369
|
+
onRunEnd: (payload) => {
|
|
3370
|
+
counts.cached = payload.execution.cached;
|
|
3371
|
+
counts.cancelled = payload.execution.cancelled;
|
|
3372
|
+
counts.completed = payload.execution.completed;
|
|
3373
|
+
counts.failed = payload.execution.failed;
|
|
3374
|
+
counts.planned = payload.execution.planned;
|
|
3375
|
+
endedAt = payload.endedAt;
|
|
2827
3376
|
}
|
|
2828
|
-
|
|
2829
|
-
},
|
|
2830
|
-
write
|
|
3377
|
+
}
|
|
2831
3378
|
};
|
|
2832
3379
|
}
|
|
2833
|
-
function
|
|
2834
|
-
if (
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
function createCliProgressReporter(options) {
|
|
2840
|
-
if (!options.isTty) return {
|
|
2841
|
-
dispose: () => {},
|
|
2842
|
-
reporter: createPlainProgressReporter({ write: options.write }),
|
|
2843
|
-
write: options.write
|
|
3380
|
+
function mergeProgressReporters(base, extra) {
|
|
3381
|
+
if (!base) return extra;
|
|
3382
|
+
if (!extra) return base;
|
|
3383
|
+
const baseState = {
|
|
3384
|
+
disabled: false,
|
|
3385
|
+
reporter: base
|
|
2844
3386
|
};
|
|
2845
|
-
const
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
const reporter = createRenderingProgressReporter(summary, renderer);
|
|
2863
|
-
return {
|
|
2864
|
-
dispose: renderer.finish,
|
|
2865
|
-
reporter,
|
|
2866
|
-
write: renderer.write
|
|
3387
|
+
const extraState = {
|
|
3388
|
+
disabled: false,
|
|
3389
|
+
reporter: extra
|
|
3390
|
+
};
|
|
3391
|
+
let firstFailure;
|
|
3392
|
+
const deliver = (state, callback) => {
|
|
3393
|
+
if (state.disabled) return;
|
|
3394
|
+
try {
|
|
3395
|
+
callback(state.reporter);
|
|
3396
|
+
} catch (error) {
|
|
3397
|
+
state.disabled = true;
|
|
3398
|
+
firstFailure ??= normalizeReporterError(error);
|
|
3399
|
+
}
|
|
3400
|
+
};
|
|
3401
|
+
const deliverBoth = (callback) => {
|
|
3402
|
+
deliver(baseState, callback);
|
|
3403
|
+
deliver(extraState, callback);
|
|
2867
3404
|
};
|
|
2868
|
-
}
|
|
2869
|
-
function createRenderingProgressReporter(summary, renderer) {
|
|
2870
3405
|
return {
|
|
2871
3406
|
onDiagnostic: (payload) => {
|
|
2872
|
-
|
|
2873
|
-
renderer.render();
|
|
3407
|
+
deliverBoth((reporter) => reporter.onDiagnostic?.(payload));
|
|
2874
3408
|
},
|
|
2875
3409
|
onExecuteEnd: (payload) => {
|
|
2876
|
-
|
|
2877
|
-
renderer.render();
|
|
3410
|
+
deliverBoth((reporter) => reporter.onExecuteEnd?.(payload));
|
|
2878
3411
|
},
|
|
2879
3412
|
onExecuteStart: (payload) => {
|
|
2880
|
-
|
|
2881
|
-
renderer.render();
|
|
3413
|
+
deliverBoth((reporter) => reporter.onExecuteStart?.(payload));
|
|
2882
3414
|
},
|
|
2883
3415
|
onFileReady: (payload) => {
|
|
2884
|
-
|
|
2885
|
-
renderer.render();
|
|
3416
|
+
deliverBoth((reporter) => reporter.onFileReady?.(payload));
|
|
2886
3417
|
},
|
|
2887
3418
|
onJobEnd: (payload) => {
|
|
2888
|
-
|
|
2889
|
-
renderer.render();
|
|
3419
|
+
deliverBoth((reporter) => reporter.onJobEnd?.(payload));
|
|
2890
3420
|
},
|
|
2891
3421
|
onJobQueued: (payload) => {
|
|
2892
|
-
|
|
2893
|
-
renderer.render();
|
|
3422
|
+
deliverBoth((reporter) => reporter.onJobQueued?.(payload));
|
|
2894
3423
|
},
|
|
2895
3424
|
onJobRetry: (payload) => {
|
|
2896
|
-
|
|
2897
|
-
renderer.render();
|
|
3425
|
+
deliverBoth((reporter) => reporter.onJobRetry?.(payload));
|
|
2898
3426
|
},
|
|
2899
3427
|
onJobStart: (payload) => {
|
|
2900
|
-
|
|
2901
|
-
|
|
3428
|
+
deliverBoth((reporter) => reporter.onJobStart?.(payload));
|
|
3429
|
+
},
|
|
3430
|
+
onPlanningEnd: (payload) => {
|
|
3431
|
+
deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
|
|
2902
3432
|
},
|
|
2903
3433
|
onPrepareEnd: (payload) => {
|
|
2904
|
-
|
|
2905
|
-
renderer.render();
|
|
3434
|
+
deliverBoth((reporter) => reporter.onPrepareEnd?.(payload));
|
|
2906
3435
|
},
|
|
2907
3436
|
onPrepareStart: (payload) => {
|
|
2908
|
-
|
|
2909
|
-
renderer.start();
|
|
3437
|
+
deliverBoth((reporter) => reporter.onPrepareStart?.(payload));
|
|
2910
3438
|
},
|
|
2911
3439
|
onRunEnd: (payload) => {
|
|
2912
|
-
|
|
2913
|
-
|
|
3440
|
+
deliverBoth((reporter) => reporter.onRunEnd?.(payload));
|
|
3441
|
+
if (firstFailure) throw firstFailure;
|
|
2914
3442
|
},
|
|
2915
3443
|
onUsage: (payload) => {
|
|
2916
|
-
|
|
2917
|
-
renderer.render();
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
//#endregion
|
|
2922
|
-
//#region src/cli/commands/lint/discovery.ts
|
|
2923
|
-
const minimatchOptions = { dot: true };
|
|
2924
|
-
const directoryBufferSize = 256;
|
|
2925
|
-
var NoFilesFoundError = class extends Error {
|
|
2926
|
-
globInputPaths;
|
|
2927
|
-
pattern;
|
|
2928
|
-
constructor(pattern, options) {
|
|
2929
|
-
super(`No files matching "${pattern}" were found${options.globInputPaths ? "" : " (glob input is disabled)"}.`);
|
|
2930
|
-
this.name = "NoFilesFoundError";
|
|
2931
|
-
this.globInputPaths = options.globInputPaths;
|
|
2932
|
-
this.pattern = pattern;
|
|
2933
|
-
}
|
|
2934
|
-
};
|
|
2935
|
-
async function findLintTargets(options) {
|
|
2936
|
-
const { config, cwd, errorOnUnmatchedPattern = true, globInputPaths = true, inputs } = options;
|
|
2937
|
-
const gitignore = shouldFilterGitignoredFiles(config) ? new Gitignore() : void 0;
|
|
2938
|
-
const targets = await resolveInputFiles({
|
|
2939
|
-
config,
|
|
2940
|
-
cwd,
|
|
2941
|
-
errorOnUnmatchedPattern,
|
|
2942
|
-
gitignore,
|
|
2943
|
-
globInputPaths,
|
|
2944
|
-
inputs: inputs.length > 0 ? inputs : ["."]
|
|
2945
|
-
});
|
|
2946
|
-
if (!gitignore || targets.files.length === 0) return targets;
|
|
2947
|
-
const lintFiles = [];
|
|
2948
|
-
for (const file of targets.files) {
|
|
2949
|
-
if (await gitignore.ignores(resolve(cwd, file))) continue;
|
|
2950
|
-
lintFiles.push(file);
|
|
2951
|
-
}
|
|
2952
|
-
return {
|
|
2953
|
-
directories: targets.directories,
|
|
2954
|
-
files: lintFiles
|
|
2955
|
-
};
|
|
2956
|
-
}
|
|
2957
|
-
function collectGlobalIgnorePatterns(config) {
|
|
2958
|
-
return normalizeConfig(config).flatMap((item) => isGlobalIgnoreItem(item) ? [...item.ignores] : []);
|
|
2959
|
-
}
|
|
2960
|
-
function getGlobParent(input) {
|
|
2961
|
-
const segments = input.replaceAll("\\", "/").split("/");
|
|
2962
|
-
const parentSegments = [];
|
|
2963
|
-
for (const segment of segments) {
|
|
2964
|
-
if (isGlobPattern(segment)) break;
|
|
2965
|
-
parentSegments.push(segment);
|
|
2966
|
-
}
|
|
2967
|
-
const parent = parentSegments.join("/");
|
|
2968
|
-
return parent === "" ? "." : parent;
|
|
2969
|
-
}
|
|
2970
|
-
function isGlobalIgnoreItem(item) {
|
|
2971
|
-
const keys = Object.keys(item).filter((key) => item[key] !== void 0);
|
|
2972
|
-
return item.ignores !== void 0 && keys.every((key) => key === "ignores" || key === "name");
|
|
2973
|
-
}
|
|
2974
|
-
function isGlobPattern(input) {
|
|
2975
|
-
return new Minimatch(input, minimatchOptions).hasMagic();
|
|
2976
|
-
}
|
|
2977
|
-
function isNodeError$1(error) {
|
|
2978
|
-
return error instanceof Error && "code" in error;
|
|
2979
|
-
}
|
|
2980
|
-
function matchesGlob(filePath, pattern) {
|
|
2981
|
-
return minimatch(filePath, pattern.replaceAll("\\", "/"), minimatchOptions);
|
|
2982
|
-
}
|
|
2983
|
-
function matchesIgnoredDirectory(relativePath, patterns) {
|
|
2984
|
-
return patterns.some((pattern) => minimatch(relativePath, pattern, minimatchOptions) || minimatch(`${relativePath}/`, pattern, minimatchOptions) || minimatch(`${relativePath}/__alint__`, pattern, minimatchOptions));
|
|
2985
|
-
}
|
|
2986
|
-
function normalizeGlobPattern(cwd, pattern) {
|
|
2987
|
-
return isAbsolute(pattern) ? normalizeRelativePath(cwd, pattern) : pattern.replaceAll("\\", "/");
|
|
2988
|
-
}
|
|
2989
|
-
function normalizeRelativePath(cwd, filePath) {
|
|
2990
|
-
return relative(cwd, filePath).replaceAll("\\", "/");
|
|
2991
|
-
}
|
|
2992
|
-
async function resolveInputFiles(options) {
|
|
2993
|
-
const hasFilePatterns = hasDiscoveryFilePatterns(options.config);
|
|
2994
|
-
const ignoredPatterns = collectGlobalIgnorePatterns(options.config);
|
|
2995
|
-
const candidates = [];
|
|
2996
|
-
const directories = [];
|
|
2997
|
-
for (const input of options.inputs) {
|
|
2998
|
-
const path = resolve(options.cwd, input);
|
|
2999
|
-
const stats = await statPath(path);
|
|
3000
|
-
if (stats?.isFile()) {
|
|
3001
|
-
candidates.push(input);
|
|
3002
|
-
continue;
|
|
3003
|
-
}
|
|
3004
|
-
if (stats?.isDirectory()) {
|
|
3005
|
-
if (await shouldPruneDirectory(path, {
|
|
3006
|
-
cwd: options.cwd,
|
|
3007
|
-
gitignore: options.gitignore,
|
|
3008
|
-
ignoredPatterns
|
|
3009
|
-
})) continue;
|
|
3010
|
-
directories.push(input);
|
|
3011
|
-
if (!hasFilePatterns) continue;
|
|
3012
|
-
const directoryFiles = [];
|
|
3013
|
-
for await (const file of walkFiles(path, {
|
|
3014
|
-
cwd: options.cwd,
|
|
3015
|
-
gitignore: options.gitignore,
|
|
3016
|
-
ignoredPatterns
|
|
3017
|
-
})) {
|
|
3018
|
-
const relativePath = normalizeRelativePath(options.cwd, file);
|
|
3019
|
-
if (hasFilePatterns && matchesDiscoveryFile(relativePath, options.config, { cwd: options.cwd })) directoryFiles.push(relativePath);
|
|
3020
|
-
}
|
|
3021
|
-
for (const file of directoryFiles.sort()) candidates.push(file);
|
|
3022
|
-
continue;
|
|
3023
|
-
}
|
|
3024
|
-
if (options.globInputPaths && isGlobPattern(input)) {
|
|
3025
|
-
const matches = await searchGlob({
|
|
3026
|
-
config: options.config,
|
|
3027
|
-
cwd: options.cwd,
|
|
3028
|
-
gitignore: options.gitignore,
|
|
3029
|
-
hasFilePatterns,
|
|
3030
|
-
ignoredPatterns,
|
|
3031
|
-
pattern: input
|
|
3032
|
-
});
|
|
3033
|
-
if (matches.length === 0 && options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
|
|
3034
|
-
for (const match of matches) candidates.push(match);
|
|
3035
|
-
continue;
|
|
3444
|
+
deliverBoth((reporter) => reporter.onUsage?.(payload));
|
|
3036
3445
|
}
|
|
3037
|
-
|
|
3038
|
-
}
|
|
3039
|
-
return {
|
|
3040
|
-
directories: [...new Set(directories)],
|
|
3041
|
-
files: [...new Set(candidates)]
|
|
3042
|
-
};
|
|
3043
|
-
}
|
|
3044
|
-
async function searchGlob(options) {
|
|
3045
|
-
const root = resolve(options.cwd, getGlobParent(options.pattern));
|
|
3046
|
-
const rootStats = await statPath(root);
|
|
3047
|
-
const pattern = normalizeGlobPattern(options.cwd, options.pattern);
|
|
3048
|
-
if (!rootStats?.isDirectory()) return [];
|
|
3049
|
-
const candidates = [];
|
|
3050
|
-
for await (const file of walkFiles(root, {
|
|
3051
|
-
cwd: options.cwd,
|
|
3052
|
-
gitignore: options.gitignore,
|
|
3053
|
-
ignoredPatterns: options.ignoredPatterns
|
|
3054
|
-
})) {
|
|
3055
|
-
const relativePath = normalizeRelativePath(options.cwd, file);
|
|
3056
|
-
if (matchesGlob(relativePath, pattern) && shouldIncludeGlobCandidate(relativePath, options.config, {
|
|
3057
|
-
cwd: options.cwd,
|
|
3058
|
-
hasFilePatterns: options.hasFilePatterns
|
|
3059
|
-
})) candidates.push(relativePath);
|
|
3060
|
-
}
|
|
3061
|
-
return candidates.sort();
|
|
3446
|
+
};
|
|
3062
3447
|
}
|
|
3063
|
-
function
|
|
3064
|
-
|
|
3448
|
+
function resolveStatsWrite(stats, env) {
|
|
3449
|
+
if (isCi(env)) return;
|
|
3450
|
+
if (!(stats === false ? false : stats === void 0 || stats === true ? true : stats.enabled !== false)) return;
|
|
3451
|
+
const config = typeof stats === "object" ? stats : void 0;
|
|
3452
|
+
return {
|
|
3453
|
+
dir: config?.location ?? getStatsDir(env),
|
|
3454
|
+
retentionMonths: config?.retentionMonths
|
|
3455
|
+
};
|
|
3065
3456
|
}
|
|
3066
|
-
function
|
|
3067
|
-
|
|
3068
|
-
|
|
3457
|
+
async function writeRunStats(target, collector, result, cwd) {
|
|
3458
|
+
const store = createJsonlStatsStore({
|
|
3459
|
+
dir: target.dir,
|
|
3460
|
+
retentionMonths: target.retentionMonths
|
|
3461
|
+
});
|
|
3462
|
+
try {
|
|
3463
|
+
await store.record(createRunStat({
|
|
3464
|
+
cwd,
|
|
3465
|
+
durationMs: collector.durationMs(),
|
|
3466
|
+
ruleCounts: {
|
|
3467
|
+
cached: result.execution.cached,
|
|
3468
|
+
cancelled: result.execution.cancelled,
|
|
3469
|
+
completed: result.execution.completed,
|
|
3470
|
+
failed: result.execution.failed,
|
|
3471
|
+
planned: result.execution.planned
|
|
3472
|
+
},
|
|
3473
|
+
usage: result.usage
|
|
3474
|
+
}));
|
|
3475
|
+
} catch {}
|
|
3069
3476
|
}
|
|
3070
|
-
|
|
3071
|
-
if (
|
|
3072
|
-
return
|
|
3477
|
+
function normalizeReporterError(error) {
|
|
3478
|
+
if (error instanceof Error) return error;
|
|
3479
|
+
return new Error(error != null ? String(error) : "Unknown progress reporter error.");
|
|
3073
3480
|
}
|
|
3074
|
-
|
|
3481
|
+
//#endregion
|
|
3482
|
+
//#region src/cli/commands/lint/execution.ts
|
|
3483
|
+
/** Runs one session while keeping CLI flags and stats behavior identical for every caller. */
|
|
3484
|
+
async function executeLint(options) {
|
|
3485
|
+
const runner = resolveRunnerConfig(options.session.runner, options.runnerOptions);
|
|
3486
|
+
const statsTarget = resolveStatsWrite(runner?.stats, options.io.env);
|
|
3487
|
+
const statsCollector = statsTarget ? createStatsCollector() : void 0;
|
|
3488
|
+
const persistStats = async (result) => {
|
|
3489
|
+
if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, result, options.session.cwd);
|
|
3490
|
+
};
|
|
3491
|
+
const targetSelection = options.targets === void 0 ? { inputs: options.inputs } : { targets: options.targets };
|
|
3492
|
+
let result;
|
|
3075
3493
|
try {
|
|
3076
|
-
|
|
3494
|
+
result = await options.session.run({
|
|
3495
|
+
...targetSelection,
|
|
3496
|
+
cacheOnly: options.cacheOnly,
|
|
3497
|
+
modelOverride: options.modelOverride,
|
|
3498
|
+
outputLanguage: options.outputLanguage,
|
|
3499
|
+
progress: mergeProgressReporters(options.progress, statsCollector?.reporter),
|
|
3500
|
+
runner,
|
|
3501
|
+
signal: options.createSignal?.()
|
|
3502
|
+
});
|
|
3077
3503
|
} catch (error) {
|
|
3078
|
-
if (
|
|
3504
|
+
if (error instanceof AlintRunError || error instanceof AlintRunCancelledError || error instanceof AlintCachePersistenceError) await persistStats(error.result);
|
|
3079
3505
|
throw error;
|
|
3080
3506
|
}
|
|
3507
|
+
await persistStats(result);
|
|
3508
|
+
return result;
|
|
3081
3509
|
}
|
|
3510
|
+
//#endregion
|
|
3511
|
+
//#region src/cli/commands/integrations/stop-gate/timeout.ts
|
|
3512
|
+
const maximumTimerChunkMs = 2147483647;
|
|
3082
3513
|
/**
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3514
|
+
* Node shortens an overflowing timer to one millisecond. The public Stop Gate limit is currently
|
|
3515
|
+
* below that boundary, while chunking here keeps this process-level safety helper correct if the
|
|
3516
|
+
* host budget changes independently later.
|
|
3085
3517
|
*/
|
|
3086
|
-
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
const
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
continue;
|
|
3518
|
+
function createTimeoutSignal(timeoutMs) {
|
|
3519
|
+
const controller = new AbortController();
|
|
3520
|
+
let remainingMs = timeoutMs;
|
|
3521
|
+
let timer;
|
|
3522
|
+
const schedule = () => {
|
|
3523
|
+
const delay = Math.min(remainingMs, maximumTimerChunkMs);
|
|
3524
|
+
timer = setTimeout(() => {
|
|
3525
|
+
remainingMs -= delay;
|
|
3526
|
+
if (remainingMs > 0) {
|
|
3527
|
+
schedule();
|
|
3528
|
+
return;
|
|
3098
3529
|
}
|
|
3099
|
-
|
|
3530
|
+
controller.abort(new DOMException("Stop Gate timed out.", "TimeoutError"));
|
|
3531
|
+
}, delay);
|
|
3532
|
+
};
|
|
3533
|
+
schedule();
|
|
3534
|
+
return {
|
|
3535
|
+
dispose: () => clearTimeout(timer),
|
|
3536
|
+
signal: controller.signal
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
//#endregion
|
|
3540
|
+
//#region src/cli/commands/integrations/stop-gate/run.ts
|
|
3541
|
+
async function runStopGateLint(options) {
|
|
3542
|
+
const dirtyTargets = options.stopGate.target === "dirty-files" ? await findDirtyLintTargets(options.config, options.cwd) : void 0;
|
|
3543
|
+
const targets = dirtyTargets ?? await findLintTargets({
|
|
3544
|
+
config: options.config,
|
|
3545
|
+
cwd: options.cwd,
|
|
3546
|
+
errorOnUnmatchedPattern: true,
|
|
3547
|
+
globInputPaths: true,
|
|
3548
|
+
inputs: ["."]
|
|
3549
|
+
});
|
|
3550
|
+
if (options.stopGate.target === "dirty-files" && targets.files.length === 0) return;
|
|
3551
|
+
const session = await createRunSession({
|
|
3552
|
+
...options.io,
|
|
3553
|
+
cwd: options.cwd
|
|
3554
|
+
}, { config: options.config });
|
|
3555
|
+
let timeout;
|
|
3556
|
+
try {
|
|
3557
|
+
let result;
|
|
3558
|
+
let runError;
|
|
3559
|
+
try {
|
|
3560
|
+
result = await executeLint({
|
|
3561
|
+
createSignal: () => {
|
|
3562
|
+
timeout = createTimeoutSignal(options.stopGate.timeoutMs);
|
|
3563
|
+
return timeout.signal;
|
|
3564
|
+
},
|
|
3565
|
+
io: options.io,
|
|
3566
|
+
runnerOptions: { format: "json" },
|
|
3567
|
+
session,
|
|
3568
|
+
targets
|
|
3569
|
+
});
|
|
3570
|
+
} catch (error) {
|
|
3571
|
+
if (!(error instanceof AlintRunError)) throw error;
|
|
3572
|
+
runError = error;
|
|
3573
|
+
result = error.result;
|
|
3100
3574
|
}
|
|
3101
|
-
|
|
3575
|
+
if (dirtyTargets !== void 0) result = filterResultToChangedLines(result, {
|
|
3576
|
+
changedLines: dirtyTargets.changedLines,
|
|
3577
|
+
cwd: options.cwd
|
|
3578
|
+
});
|
|
3579
|
+
return runError === void 0 ? {
|
|
3580
|
+
files: targets.files,
|
|
3581
|
+
result
|
|
3582
|
+
} : {
|
|
3583
|
+
error: runError,
|
|
3584
|
+
files: targets.files,
|
|
3585
|
+
result
|
|
3586
|
+
};
|
|
3587
|
+
} finally {
|
|
3588
|
+
timeout?.dispose();
|
|
3589
|
+
await session.shutdown();
|
|
3102
3590
|
}
|
|
3103
3591
|
}
|
|
3104
3592
|
//#endregion
|
|
3105
|
-
//#region src/cli/
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3593
|
+
//#region src/cli/commands/integrations/stop-gate/stop-gate.ts
|
|
3594
|
+
const stopGate = defineCommand({
|
|
3595
|
+
action: (context, options) => runStopGateCommand(options, context.io),
|
|
3596
|
+
description: "Run the repository Stop Gate integration",
|
|
3597
|
+
exactArguments: true,
|
|
3598
|
+
name: "stop-gate",
|
|
3599
|
+
options: [{
|
|
3600
|
+
description: "Codex session id",
|
|
3601
|
+
flags: "--session-id <id>"
|
|
3602
|
+
}]
|
|
3603
|
+
});
|
|
3604
|
+
async function runStopGateCommand(options, io) {
|
|
3605
|
+
if (!options.sessionId) {
|
|
3606
|
+
io.stderr.write("Stop Gate requires --session-id.\n");
|
|
3607
|
+
return 2;
|
|
3608
|
+
}
|
|
3609
|
+
try {
|
|
3610
|
+
const cwd = await findGitRoot(io.cwd);
|
|
3611
|
+
const loaded = await loadAlintConfigWithMetadata(cwd);
|
|
3612
|
+
if (loaded.configFile === void 0) {
|
|
3613
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3614
|
+
errorCount: 0,
|
|
3615
|
+
schemaVersion: 2,
|
|
3616
|
+
status: "inactive",
|
|
3617
|
+
warningCount: 0
|
|
3618
|
+
});
|
|
3619
|
+
return 0;
|
|
3620
|
+
}
|
|
3621
|
+
const stopGate = resolveStopGateConfig(loaded.config, cwd);
|
|
3622
|
+
if (!stopGate.enabled) {
|
|
3623
|
+
await removeStopGateReport(options.sessionId);
|
|
3624
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3625
|
+
errorCount: 0,
|
|
3626
|
+
schemaVersion: 2,
|
|
3627
|
+
status: "inactive",
|
|
3628
|
+
warningCount: 0
|
|
3629
|
+
});
|
|
3630
|
+
return 0;
|
|
3631
|
+
}
|
|
3632
|
+
const lint = await runStopGateLint({
|
|
3633
|
+
config: loaded.config,
|
|
3634
|
+
cwd,
|
|
3635
|
+
io,
|
|
3636
|
+
stopGate
|
|
3637
|
+
});
|
|
3638
|
+
if (lint === void 0) {
|
|
3639
|
+
await removeStopGateReport(options.sessionId);
|
|
3640
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3641
|
+
errorCount: 0,
|
|
3642
|
+
schemaVersion: 2,
|
|
3643
|
+
status: "no-dirty-files",
|
|
3644
|
+
warningCount: 0
|
|
3645
|
+
});
|
|
3646
|
+
return 0;
|
|
3647
|
+
}
|
|
3648
|
+
if (lint.error !== void 0) {
|
|
3649
|
+
const reportPath = await writeStopGateReport(options.sessionId, {
|
|
3650
|
+
cwd,
|
|
3651
|
+
diagnostics: lint.result.diagnostics,
|
|
3652
|
+
execution: lint.result.execution,
|
|
3653
|
+
failure: {
|
|
3654
|
+
details: lint.error.failures,
|
|
3655
|
+
message: lint.error.message
|
|
3656
|
+
},
|
|
3657
|
+
files: lint.files,
|
|
3658
|
+
schemaVersion: 1,
|
|
3659
|
+
target: stopGate.target,
|
|
3660
|
+
usage: lint.result.usage
|
|
3661
|
+
});
|
|
3662
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3663
|
+
errorCount: 0,
|
|
3664
|
+
message: lint.error.message,
|
|
3665
|
+
reportPath,
|
|
3666
|
+
schemaVersion: 2,
|
|
3667
|
+
status: "runtime-error",
|
|
3668
|
+
warningCount: 0
|
|
3669
|
+
});
|
|
3670
|
+
return 1;
|
|
3671
|
+
}
|
|
3672
|
+
const errorCount = lint.result.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
3673
|
+
const warningCount = lint.result.diagnostics.length - errorCount;
|
|
3674
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
3675
|
+
await removeStopGateReport(options.sessionId);
|
|
3676
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3677
|
+
errorCount,
|
|
3678
|
+
schemaVersion: 2,
|
|
3679
|
+
status: "clean",
|
|
3680
|
+
warningCount
|
|
3681
|
+
});
|
|
3682
|
+
return 0;
|
|
3683
|
+
}
|
|
3684
|
+
const reportPath = await writeStopGateReport(options.sessionId, {
|
|
3685
|
+
cwd,
|
|
3686
|
+
diagnostics: lint.result.diagnostics,
|
|
3687
|
+
execution: lint.result.execution,
|
|
3688
|
+
files: lint.files,
|
|
3689
|
+
schemaVersion: 1,
|
|
3690
|
+
target: stopGate.target,
|
|
3691
|
+
usage: lint.result.usage
|
|
3692
|
+
});
|
|
3693
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3694
|
+
errorCount,
|
|
3695
|
+
findingsHash: fingerprintDiagnostics(lint.result.diagnostics),
|
|
3696
|
+
reportPath,
|
|
3697
|
+
schemaVersion: 2,
|
|
3698
|
+
status: errorCount > 0 ? "errors" : "warnings",
|
|
3699
|
+
warningCount
|
|
3700
|
+
});
|
|
3701
|
+
return 0;
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3704
|
+
errorCount: 0,
|
|
3705
|
+
message: error instanceof StopGateReportTooLargeError ? error.message : error instanceof AlintRunCancelledError ? "Stop Gate timed out before lint completed." : errorMessageFrom(error) ?? "unknown error",
|
|
3706
|
+
schemaVersion: 2,
|
|
3707
|
+
status: "runtime-error",
|
|
3708
|
+
warningCount: 0
|
|
3709
|
+
});
|
|
3710
|
+
return 1;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
//#endregion
|
|
3714
|
+
//#region src/cli/commands/integrations/integrations.ts
|
|
3715
|
+
const integrations = defineCommand({
|
|
3716
|
+
children: [stopGate],
|
|
3717
|
+
description: "Run external integrations",
|
|
3718
|
+
name: "integrations"
|
|
3719
|
+
});
|
|
3720
|
+
//#endregion
|
|
3721
|
+
//#region src/cli/reporters/json.ts
|
|
3722
|
+
function formatJson(result) {
|
|
3723
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
3724
|
+
}
|
|
3725
|
+
//#endregion
|
|
3726
|
+
//#region src/cli/reporters/stylish.ts
|
|
3727
|
+
const colors$3 = createColors({ force: true });
|
|
3728
|
+
function formatStylish(input, options = {}) {
|
|
3729
|
+
const diagnostics = Array.isArray(input) ? input : input.diagnostics;
|
|
3730
|
+
const result = Array.isArray(input) ? void 0 : input;
|
|
3731
|
+
const style = createStyle(options.color === true);
|
|
3732
|
+
if (diagnostics.length === 0) return result?.execution?.cached ? `${formatSummary(diagnostics, result, style)}\n` : "";
|
|
3733
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
3734
|
+
for (const diagnostic of diagnostics) {
|
|
3735
|
+
const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
|
|
3736
|
+
if (fileDiagnostics) {
|
|
3737
|
+
fileDiagnostics.push(diagnostic);
|
|
3738
|
+
continue;
|
|
3739
|
+
}
|
|
3740
|
+
diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
|
|
3741
|
+
}
|
|
3742
|
+
const lines = [];
|
|
3743
|
+
for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
|
|
3744
|
+
lines.push(style.file(filePath));
|
|
3745
|
+
for (const diagnostic of fileDiagnostics) {
|
|
3746
|
+
const line = diagnostic.loc?.start.line ?? 0;
|
|
3747
|
+
const column = diagnostic.loc?.start.column ?? 0;
|
|
3748
|
+
const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
|
|
3749
|
+
lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
|
|
3750
|
+
}
|
|
3751
|
+
lines.push("");
|
|
3752
|
+
}
|
|
3753
|
+
lines.push("", formatSummary(diagnostics, result, style));
|
|
3754
|
+
return `${lines.join("\n")}\n`;
|
|
3755
|
+
}
|
|
3756
|
+
function countCachedDiagnostics(diagnostics, severity) {
|
|
3757
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity && diagnostic.cached === true).length;
|
|
3758
|
+
}
|
|
3759
|
+
function countDiagnostics$1(diagnostics, severity) {
|
|
3760
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
3761
|
+
}
|
|
3762
|
+
function createStyle(color) {
|
|
3763
|
+
if (!color) return {
|
|
3764
|
+
error: identity,
|
|
3765
|
+
file: identity,
|
|
3766
|
+
location: identity,
|
|
3767
|
+
ruleId: identity,
|
|
3768
|
+
summaryToken: identity,
|
|
3769
|
+
warning: identity
|
|
3770
|
+
};
|
|
3771
|
+
return {
|
|
3772
|
+
error: colors$3.red,
|
|
3773
|
+
file: colors$3.underline,
|
|
3774
|
+
location: colors$3.dim,
|
|
3775
|
+
ruleId: colors$3.dim,
|
|
3776
|
+
summaryToken: colors$3.cyan,
|
|
3777
|
+
warning: colors$3.yellow
|
|
3120
3778
|
};
|
|
3121
|
-
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
3122
3779
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
return normalizeConfig(config).reduce((merged, item) => item.runner ? {
|
|
3126
|
-
...merged,
|
|
3127
|
-
...item.runner
|
|
3128
|
-
} : merged, void 0);
|
|
3780
|
+
function formatDiagnosticCount(total, cached, label) {
|
|
3781
|
+
return `${total} ${label}${cached > 0 ? ` (${cached} cached)` : ""}`;
|
|
3129
3782
|
}
|
|
3130
|
-
function
|
|
3131
|
-
if (
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3783
|
+
function formatExecutionSummary(execution) {
|
|
3784
|
+
if (!execution || execution.cached === 0) return;
|
|
3785
|
+
return `${execution.cached}/${execution.planned} cached`;
|
|
3786
|
+
}
|
|
3787
|
+
function formatSummary(diagnostics, result, style) {
|
|
3788
|
+
const warnCount = countDiagnostics$1(diagnostics, "warn");
|
|
3789
|
+
const errorCount = countDiagnostics$1(diagnostics, "error");
|
|
3790
|
+
const cachedWarnCount = countCachedDiagnostics(diagnostics, "warn");
|
|
3791
|
+
const cachedErrorCount = countCachedDiagnostics(diagnostics, "error");
|
|
3792
|
+
const tokens = result === void 0 ? void 0 : formatTokenSummary(result);
|
|
3793
|
+
const problemSummary = [style.warning(formatDiagnosticCount(warnCount, cachedWarnCount, "warn")), style.error(formatDiagnosticCount(errorCount, cachedErrorCount, "error"))].join(" / ");
|
|
3794
|
+
if (tokens === void 0) return problemSummary;
|
|
3795
|
+
const execution = formatExecutionSummary(result?.execution);
|
|
3796
|
+
return [
|
|
3797
|
+
problemSummary,
|
|
3798
|
+
style.summaryToken(tokens),
|
|
3799
|
+
execution
|
|
3800
|
+
].filter((segment) => segment !== void 0).join(" | ");
|
|
3801
|
+
}
|
|
3802
|
+
function formatTokenSummary(result) {
|
|
3803
|
+
const liveTokens = result.usage.totalTokens.toLocaleString("en-US");
|
|
3804
|
+
const cachedTokens = result.usage.cached?.totalTokens ?? 0;
|
|
3805
|
+
return `${liveTokens} tokens${cachedTokens > 0 ? ` (${cachedTokens.toLocaleString("en-US")} cached)` : ""}`;
|
|
3806
|
+
}
|
|
3807
|
+
function identity(value) {
|
|
3808
|
+
return value;
|
|
3809
|
+
}
|
|
3810
|
+
//#endregion
|
|
3811
|
+
//#region src/cli/reporters/index.ts
|
|
3812
|
+
function formatDiagnostics(format, result, options = {}) {
|
|
3813
|
+
if (format === "json") return formatJson(result);
|
|
3814
|
+
if (format === "stylish") return formatStylish(result, { color: options.color });
|
|
3815
|
+
throw new Error(`Unknown reporter "${format}".`);
|
|
3816
|
+
}
|
|
3817
|
+
//#endregion
|
|
3818
|
+
//#region src/cli/reporters/progress/plain.ts
|
|
3819
|
+
function createPlainProgressReporter(options) {
|
|
3820
|
+
const writeLine = (line) => options.write(`${line}\n`);
|
|
3821
|
+
return {
|
|
3822
|
+
onJobRetry: () => {},
|
|
3823
|
+
onJobStart: (payload) => {
|
|
3824
|
+
const target = payload.job.target.name ? `${payload.job.target.kind} ${payload.job.target.name}` : payload.job.target.kind;
|
|
3825
|
+
writeLine(`scan ${payload.job.inputPath} > ${target} > ${payload.job.ruleId}`);
|
|
3826
|
+
},
|
|
3827
|
+
onPrepareEnd: ({ filesTotal }) => writeLine(`alint prepared: ${filesTotal} files`),
|
|
3828
|
+
onPrepareStart: () => writeLine("alint preparing"),
|
|
3829
|
+
onRunEnd: (payload) => {
|
|
3830
|
+
const warnCount = countDiagnostics(payload.diagnostics, "warn");
|
|
3831
|
+
const errorCount = countDiagnostics(payload.diagnostics, "error");
|
|
3832
|
+
const state = payload.execution.failed > 0 ? "failed" : "finished";
|
|
3833
|
+
const { cached, cancelled, completed, failed, skipped } = payload.execution;
|
|
3834
|
+
writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens, ${completed} completed, ${cached} cached, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped`);
|
|
3835
|
+
}
|
|
3136
3836
|
};
|
|
3137
|
-
return configCache;
|
|
3138
3837
|
}
|
|
3139
|
-
function
|
|
3140
|
-
|
|
3141
|
-
if (typeof configStats === "boolean") return configStats;
|
|
3142
|
-
if (typeof setupStats === "object") return {
|
|
3143
|
-
...setupStats,
|
|
3144
|
-
...configStats
|
|
3145
|
-
};
|
|
3146
|
-
return configStats;
|
|
3838
|
+
function countDiagnostics(diagnostics, severity) {
|
|
3839
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
3147
3840
|
}
|
|
3148
3841
|
//#endregion
|
|
3149
|
-
//#region src/cli/
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
*/
|
|
3156
|
-
async function createRunSession(io, options = {}) {
|
|
3157
|
-
const [{ defaultModel, setupConfig: fileSetupConfig }, config] = await Promise.all([loadRunSetupConfig(io), loadAlintConfig(io.cwd, options.configPath)]);
|
|
3158
|
-
const runtime = await (options.startModelAdapters ?? startModelAdapters)(fileSetupConfig, io);
|
|
3159
|
-
const { setupConfig } = runtime;
|
|
3160
|
-
let shutdown;
|
|
3842
|
+
//#region src/cli/reporters/progress/summary.ts
|
|
3843
|
+
const colors$2 = createColors({ force: true });
|
|
3844
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3845
|
+
function createSummaryProgressReporter(options) {
|
|
3846
|
+
const now = Date.now;
|
|
3847
|
+
const state = createInitialState();
|
|
3161
3848
|
return {
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3849
|
+
getRows: () => createRows(state, options, now()),
|
|
3850
|
+
onDiagnostic: ({ diagnostic, progress }) => {
|
|
3851
|
+
updateProgress(state, progress);
|
|
3852
|
+
if (diagnostic.severity === "warn") state.warnCount += 1;
|
|
3853
|
+
else state.errorCount += 1;
|
|
3854
|
+
},
|
|
3855
|
+
onExecuteEnd: ({ progress }) => {
|
|
3856
|
+
updateProgress(state, progress);
|
|
3857
|
+
},
|
|
3858
|
+
onExecuteStart: ({ progress, startedAt }) => {
|
|
3859
|
+
updateProgress(state, progress);
|
|
3860
|
+
state.runStartedAt = startedAt ?? state.runStartedAt ?? now();
|
|
3861
|
+
},
|
|
3862
|
+
onFileReady: ({ progress }) => updateProgress(state, progress),
|
|
3863
|
+
onJobEnd: (payload) => {
|
|
3864
|
+
updateProgress(state, payload.progress);
|
|
3865
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
3866
|
+
const group = state.ruleGroups.get(payload.job.ruleId);
|
|
3867
|
+
if (!group) throw new Error(`Cannot finish unqueued progress job "${payload.job.id}".`);
|
|
3868
|
+
group[payload.state] += 1;
|
|
3869
|
+
if (payload.endedAt !== void 0 && current?.startedAt !== void 0 && payload.state !== "cached") group.terminalDurationMs += Math.max(payload.endedAt - current.startedAt, 0);
|
|
3870
|
+
if (payload.state === "cached") {
|
|
3871
|
+
const cachedTokens = state.jobTokens.get(payload.job.id) ?? 0;
|
|
3872
|
+
state.totalTokens = Math.max(state.totalTokens - cachedTokens, 0);
|
|
3873
|
+
state.cachedTokens += cachedTokens;
|
|
3874
|
+
}
|
|
3875
|
+
state.activeJobs.delete(payload.job.id);
|
|
3876
|
+
state.jobTokens.delete(payload.job.id);
|
|
3877
|
+
},
|
|
3878
|
+
onJobQueued: ({ job, progress }) => {
|
|
3879
|
+
updateProgress(state, progress);
|
|
3880
|
+
const group = state.ruleGroups.get(job.ruleId) ?? createRuleGroup(job);
|
|
3881
|
+
group.planned += 1;
|
|
3882
|
+
state.ruleGroups.set(job.ruleId, group);
|
|
3883
|
+
state.activeJobs.set(job.id, {
|
|
3884
|
+
job,
|
|
3885
|
+
state: "queued"
|
|
3172
3886
|
});
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
signal: runOptions.signal
|
|
3887
|
+
},
|
|
3888
|
+
onJobRetry: (payload) => {
|
|
3889
|
+
updateProgress(state, payload.progress);
|
|
3890
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
3891
|
+
state.activeJobs.set(payload.job.id, {
|
|
3892
|
+
job: payload.job,
|
|
3893
|
+
retry: {
|
|
3894
|
+
attempt: payload.attempt,
|
|
3895
|
+
maxAttempts: payload.maxAttempts,
|
|
3896
|
+
startedAt: payload.startedAt
|
|
3897
|
+
},
|
|
3898
|
+
startedAt: current?.startedAt ?? payload.startedAt ?? now(),
|
|
3899
|
+
state: current?.state ?? "running"
|
|
3187
3900
|
});
|
|
3188
3901
|
},
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
lines.push(`${failLabel} ${ruleId} ${group.length} ${targetLabel}`);
|
|
3225
|
-
for (const failure of group) {
|
|
3226
|
-
lines.push(` ${formatTarget(failure)}`);
|
|
3227
|
-
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
3228
|
-
for (const cause of failure.causes) {
|
|
3229
|
-
const code = cause.code === void 0 ? "" : `[${cause.code}] `;
|
|
3230
|
-
lines.push(` Caused by: ${code}${cause.message}`);
|
|
3902
|
+
onJobStart: (payload) => {
|
|
3903
|
+
updateProgress(state, payload.progress);
|
|
3904
|
+
state.activeJobs.set(payload.job.id, {
|
|
3905
|
+
job: payload.job,
|
|
3906
|
+
startedAt: payload.startedAt ?? now(),
|
|
3907
|
+
state: "running"
|
|
3908
|
+
});
|
|
3909
|
+
},
|
|
3910
|
+
onPlanningEnd: ({ progress }) => updateProgress(state, progress),
|
|
3911
|
+
onPrepareStart: ({ startedAt }) => {
|
|
3912
|
+
state.activeJobs.clear();
|
|
3913
|
+
state.errorCount = 0;
|
|
3914
|
+
state.execution = createCounts();
|
|
3915
|
+
state.progress = createProgress();
|
|
3916
|
+
state.ruleGroups.clear();
|
|
3917
|
+
state.runStartedAt = startedAt ?? now();
|
|
3918
|
+
state.animationTick = 0;
|
|
3919
|
+
state.cachedTokens = 0;
|
|
3920
|
+
state.jobTokens.clear();
|
|
3921
|
+
state.totalTokens = 0;
|
|
3922
|
+
state.warnCount = 0;
|
|
3923
|
+
},
|
|
3924
|
+
onRunEnd: (payload) => {
|
|
3925
|
+
state.warnCount = payload.diagnostics.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
3926
|
+
state.errorCount = payload.diagnostics.length - state.warnCount;
|
|
3927
|
+
updateProgress(state, payload.progress);
|
|
3928
|
+
state.totalTokens = payload.usage.totalTokens;
|
|
3929
|
+
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
3930
|
+
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
3931
|
+
},
|
|
3932
|
+
onUsage: ({ job, progress, record }) => {
|
|
3933
|
+
updateProgress(state, progress);
|
|
3934
|
+
if (record.totalTokens != null && Number.isFinite(record.totalTokens)) {
|
|
3935
|
+
state.jobTokens.set(job.id, (state.jobTokens.get(job.id) ?? 0) + record.totalTokens);
|
|
3936
|
+
state.totalTokens += record.totalTokens;
|
|
3231
3937
|
}
|
|
3938
|
+
},
|
|
3939
|
+
/**
|
|
3940
|
+
* Advances the summary animation on each renderer interval.
|
|
3941
|
+
*
|
|
3942
|
+
* Triggering workflow:
|
|
3943
|
+
*
|
|
3944
|
+
* `createTtyProgressRenderer`
|
|
3945
|
+
* -> `createCliProgressReporter` `createInterval` callback
|
|
3946
|
+
* -> `SummaryProgressReporter.tick` (this handler)
|
|
3947
|
+
*
|
|
3948
|
+
* Upstream:
|
|
3949
|
+
* - `createCliProgressReporter` `createInterval` callback registered by `createTtyProgressRenderer`
|
|
3950
|
+
*
|
|
3951
|
+
* Downstream:
|
|
3952
|
+
* - Increments `state.animationTick`, consumed by {@link createRows}, {@link formatSpinnerFrame}, and {@link formatMiniBarSegment}.
|
|
3953
|
+
*/
|
|
3954
|
+
tick: () => {
|
|
3955
|
+
state.animationTick += 1;
|
|
3232
3956
|
}
|
|
3233
|
-
lines.push("");
|
|
3234
|
-
}
|
|
3235
|
-
return lines;
|
|
3236
|
-
}
|
|
3237
|
-
function formatFileFailures(failures, color) {
|
|
3238
|
-
const lines = [];
|
|
3239
|
-
for (const failure of failures) {
|
|
3240
|
-
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
3241
|
-
lines.push(`${failLabel} ${failure.file.path}`);
|
|
3242
|
-
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
3243
|
-
lines.push("");
|
|
3244
|
-
}
|
|
3245
|
-
return lines;
|
|
3246
|
-
}
|
|
3247
|
-
function formatTarget(failure) {
|
|
3248
|
-
const target = failure.job.target.name ? `${failure.job.target.kind} ${failure.job.target.name}` : failure.job.target.kind;
|
|
3249
|
-
return `${failure.job.inputPath} > ${target}`;
|
|
3250
|
-
}
|
|
3251
|
-
function groupFailuresByRule(failures) {
|
|
3252
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3253
|
-
for (const failure of failures) {
|
|
3254
|
-
const group = groups.get(failure.job.ruleId) ?? [];
|
|
3255
|
-
group.push(failure);
|
|
3256
|
-
groups.set(failure.job.ruleId, group);
|
|
3257
|
-
}
|
|
3258
|
-
return [...groups];
|
|
3259
|
-
}
|
|
3260
|
-
//#endregion
|
|
3261
|
-
//#region src/cli/commands/lint/runner.ts
|
|
3262
|
-
/**
|
|
3263
|
-
* Applies this invocation's flags on top of the project's resolved runner.
|
|
3264
|
-
*
|
|
3265
|
-
* The setup/config merge itself lives in `runtime/runner.ts` because the language server needs the
|
|
3266
|
-
* same base without any flags; only the overrides below are CLI-shaped.
|
|
3267
|
-
*/
|
|
3268
|
-
function resolveRunnerConfig(baseRunner, options) {
|
|
3269
|
-
const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
|
|
3270
|
-
const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
|
|
3271
|
-
const runner = {
|
|
3272
|
-
...baseRunner ?? {},
|
|
3273
|
-
cache: resolveCacheOption(baseRunner?.cache, options),
|
|
3274
|
-
ruleConcurrency: ruleConcurrency ?? baseRunner?.ruleConcurrency,
|
|
3275
|
-
stats: options.stats === false ? false : baseRunner?.stats,
|
|
3276
|
-
timeoutMs: timeoutMs ?? baseRunner?.timeoutMs
|
|
3277
3957
|
};
|
|
3278
|
-
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
3279
|
-
}
|
|
3280
|
-
function parsePositiveIntegerOption(value, label) {
|
|
3281
|
-
if (value === void 0) return;
|
|
3282
|
-
const parsed = Number(value);
|
|
3283
|
-
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
|
|
3284
|
-
return parsed;
|
|
3285
3958
|
}
|
|
3286
|
-
function
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
return baseCache;
|
|
3959
|
+
function compareRuleGroups(left, right) {
|
|
3960
|
+
const leftActive = left.jobs.some((job) => job.state === "running") ? 1 : 0;
|
|
3961
|
+
const rightActive = right.jobs.some((job) => job.state === "running") ? 1 : 0;
|
|
3962
|
+
if (leftActive !== rightActive) return rightActive - leftActive;
|
|
3963
|
+
if (left.failed !== right.failed) return right.failed - left.failed;
|
|
3964
|
+
return left.firstJobIndex - right.firstJobIndex;
|
|
3293
3965
|
}
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
return Boolean(env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.BUILDKITE || env.TF_BUILD);
|
|
3966
|
+
function compareRunningJobs(left, right) {
|
|
3967
|
+
const leftStartedAt = left.startedAt ?? Number.POSITIVE_INFINITY;
|
|
3968
|
+
const rightStartedAt = right.startedAt ?? Number.POSITIVE_INFINITY;
|
|
3969
|
+
if (leftStartedAt !== rightStartedAt) return leftStartedAt - rightStartedAt;
|
|
3970
|
+
return left.job.index - right.job.index;
|
|
3300
3971
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
* - `Nh`/`Nd`/`Nw`/`Nm`/`Ny` — now minus N hours/days/weeks/months/years.
|
|
3308
|
-
* Weeks, months, and years are fixed approximations (7/30/365 days). `m` is
|
|
3309
|
-
* months here, not minutes — a day is the finest window this tool tracks.
|
|
3310
|
-
* - `YYYY-MM`/`YYYY-MM-DD` — start of that UTC month/day.
|
|
3311
|
-
*
|
|
3312
|
-
* Edge cases:
|
|
3313
|
-
* - An empty time means "no lower bound".
|
|
3314
|
-
* - An unrecognized time throws.
|
|
3315
|
-
*/
|
|
3316
|
-
const HOUR_MS = 36e5;
|
|
3317
|
-
const UNIT_MS = {
|
|
3318
|
-
d: 24 * HOUR_MS,
|
|
3319
|
-
h: HOUR_MS,
|
|
3320
|
-
m: 720 * HOUR_MS,
|
|
3321
|
-
w: 168 * HOUR_MS,
|
|
3322
|
-
y: 8760 * HOUR_MS
|
|
3323
|
-
};
|
|
3324
|
-
function parseSince(time, now) {
|
|
3325
|
-
if (time === void 0 || time === "") return;
|
|
3326
|
-
const relative = /^(\d+)([dhmwy])$/u.exec(time);
|
|
3327
|
-
if (relative) return now - Number(relative[1]) * UNIT_MS[relative[2]];
|
|
3328
|
-
const calendar = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/u.exec(time);
|
|
3329
|
-
if (calendar) {
|
|
3330
|
-
const year = Number(calendar[1]);
|
|
3331
|
-
const month = Number(calendar[2]);
|
|
3332
|
-
const day = calendar[3] === void 0 ? 1 : Number(calendar[3]);
|
|
3333
|
-
return Date.UTC(year, month - 1, day);
|
|
3972
|
+
function completeGraphemeBoundary(input, maximumIndex) {
|
|
3973
|
+
let boundary = 0;
|
|
3974
|
+
for (const segment of graphemeSegmenter.segment(input)) {
|
|
3975
|
+
const end = segment.index + segment.segment.length;
|
|
3976
|
+
if (end > maximumIndex) break;
|
|
3977
|
+
boundary = end;
|
|
3334
3978
|
}
|
|
3335
|
-
|
|
3979
|
+
return boundary;
|
|
3336
3980
|
}
|
|
3337
|
-
|
|
3338
|
-
//#region src/cli/stats/jsonl.ts
|
|
3339
|
-
const DEFAULT_RETENTION_MONTHS = 12;
|
|
3340
|
-
const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
|
|
3341
|
-
const DAY_MS = 864e5;
|
|
3342
|
-
const MONTH_LABELS$1 = [
|
|
3343
|
-
"Jan",
|
|
3344
|
-
"Feb",
|
|
3345
|
-
"Mar",
|
|
3346
|
-
"Apr",
|
|
3347
|
-
"May",
|
|
3348
|
-
"Jun",
|
|
3349
|
-
"Jul",
|
|
3350
|
-
"Aug",
|
|
3351
|
-
"Sep",
|
|
3352
|
-
"Oct",
|
|
3353
|
-
"Nov",
|
|
3354
|
-
"Dec"
|
|
3355
|
-
];
|
|
3356
|
-
function createJsonlStatsStore(options) {
|
|
3357
|
-
const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
|
|
3981
|
+
function createCounts(planned = 0) {
|
|
3358
3982
|
return {
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3983
|
+
cached: 0,
|
|
3984
|
+
cancelled: 0,
|
|
3985
|
+
completed: 0,
|
|
3986
|
+
failed: 0,
|
|
3987
|
+
planned,
|
|
3988
|
+
queued: 0,
|
|
3989
|
+
running: 0,
|
|
3990
|
+
skipped: 0
|
|
3362
3991
|
};
|
|
3363
3992
|
}
|
|
3364
|
-
function
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3993
|
+
function createInitialState() {
|
|
3994
|
+
return {
|
|
3995
|
+
activeJobs: /* @__PURE__ */ new Map(),
|
|
3996
|
+
animationTick: 0,
|
|
3997
|
+
cachedTokens: 0,
|
|
3998
|
+
errorCount: 0,
|
|
3999
|
+
execution: createCounts(),
|
|
4000
|
+
jobTokens: /* @__PURE__ */ new Map(),
|
|
4001
|
+
progress: createProgress(),
|
|
4002
|
+
ruleGroups: /* @__PURE__ */ new Map(),
|
|
4003
|
+
totalTokens: 0,
|
|
4004
|
+
warnCount: 0
|
|
3371
4005
|
};
|
|
3372
|
-
row.inTok += inTok;
|
|
3373
|
-
row.outTok += outTok;
|
|
3374
|
-
row.totalTok += totalTok;
|
|
3375
|
-
if (countRun) row.runs += 1;
|
|
3376
|
-
rows.set(key, row);
|
|
3377
|
-
return row;
|
|
3378
|
-
}
|
|
3379
|
-
function bucketLabel(startMs, interval) {
|
|
3380
|
-
const date = new Date(startMs);
|
|
3381
|
-
if (interval === "month") return MONTH_LABELS$1[date.getUTCMonth()];
|
|
3382
|
-
return `${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
3383
|
-
}
|
|
3384
|
-
function bucketStart(ts, interval) {
|
|
3385
|
-
const date = new Date(ts);
|
|
3386
|
-
if (interval === "month") return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
|
|
3387
|
-
const dayStart = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
3388
|
-
if (interval === "week") return dayStart - (new Date(dayStart).getUTCDay() + 6) % 7 * DAY_MS;
|
|
3389
|
-
return dayStart;
|
|
3390
|
-
}
|
|
3391
|
-
function chooseInterval(since, nowMs, timestamps) {
|
|
3392
|
-
if (since !== void 0) return intervalForDuration(nowMs - since);
|
|
3393
|
-
if (timestamps.length < 2) return "day";
|
|
3394
|
-
return intervalForDuration(Math.max(...timestamps) - Math.min(...timestamps));
|
|
3395
|
-
}
|
|
3396
|
-
function fillBuckets(buckets, interval) {
|
|
3397
|
-
if (buckets.size === 0) return [];
|
|
3398
|
-
const starts = [...buckets.keys()].sort((left, right) => left - right);
|
|
3399
|
-
const last = starts.at(-1);
|
|
3400
|
-
const filled = [];
|
|
3401
|
-
for (let startMs = starts[0]; startMs <= last; startMs = nextBucketStart(startMs, interval)) filled.push(buckets.get(startMs) ?? {
|
|
3402
|
-
inTok: 0,
|
|
3403
|
-
key: bucketLabel(startMs, interval),
|
|
3404
|
-
outTok: 0,
|
|
3405
|
-
runs: 0,
|
|
3406
|
-
startMs,
|
|
3407
|
-
totalTok: 0
|
|
3408
|
-
});
|
|
3409
|
-
return filled;
|
|
3410
|
-
}
|
|
3411
|
-
function intervalForDuration(ms) {
|
|
3412
|
-
const days = ms / DAY_MS;
|
|
3413
|
-
if (days <= 21) return "day";
|
|
3414
|
-
if (days <= 120) return "week";
|
|
3415
|
-
return "month";
|
|
3416
|
-
}
|
|
3417
|
-
function isEnoent(error) {
|
|
3418
|
-
return error instanceof Error && error.code === "ENOENT";
|
|
3419
|
-
}
|
|
3420
|
-
function monthIndexOf(ts) {
|
|
3421
|
-
const date = new Date(ts);
|
|
3422
|
-
return date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
3423
|
-
}
|
|
3424
|
-
function nextBucketStart(startMs, interval) {
|
|
3425
|
-
if (interval === "month") {
|
|
3426
|
-
const date = new Date(startMs);
|
|
3427
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
|
|
3428
|
-
}
|
|
3429
|
-
return startMs + (interval === "week" ? 7 * DAY_MS : DAY_MS);
|
|
3430
4006
|
}
|
|
3431
|
-
function
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
4007
|
+
function createProgress() {
|
|
4008
|
+
return {
|
|
4009
|
+
execution: createCounts(),
|
|
4010
|
+
filesPlanned: 0,
|
|
4011
|
+
filesTotal: 0,
|
|
4012
|
+
jobsCompleted: 0,
|
|
4013
|
+
jobsStarted: 0,
|
|
4014
|
+
jobsTotal: 0,
|
|
4015
|
+
planningComplete: false
|
|
4016
|
+
};
|
|
3439
4017
|
}
|
|
3440
|
-
|
|
3441
|
-
if (
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
3444
|
-
|
|
4018
|
+
function createRows(state, options, now) {
|
|
4019
|
+
if (options.rows !== void 0 && options.rows <= 0) return [];
|
|
4020
|
+
const groups = createRuleGroups(state);
|
|
4021
|
+
const footerRows = options.rows === void 0 ? 3 : Math.min(options.rows, 3);
|
|
4022
|
+
const separatorRows = options.rows === void 0 || options.rows - footerRows > 1 ? 1 : 0;
|
|
4023
|
+
const visible = formatContentRows(groups, state, options, now, options.rows === void 0 ? Number.POSITIVE_INFINITY : Math.max(options.rows - footerRows - separatorRows, 0));
|
|
4024
|
+
const footer = formatFooters(state, options, now);
|
|
4025
|
+
const actualSeparatorRows = visible.length > 0 ? separatorRows : 0;
|
|
4026
|
+
const visibleFooters = options.rows === void 0 ? footer : footer.slice(0, Math.max(options.rows - visible.length - actualSeparatorRows, 0));
|
|
4027
|
+
if (actualSeparatorRows === 1) visible.push("");
|
|
4028
|
+
visible.push(...visibleFooters);
|
|
4029
|
+
return options.color ? visible.map((row) => styleRow(row, state, state.warnCount, state.errorCount, options)) : visible;
|
|
3445
4030
|
}
|
|
3446
|
-
|
|
3447
|
-
const dimension = filter.by ?? "model";
|
|
3448
|
-
const since = parseSince(filter.since, now());
|
|
3449
|
-
const rules = ruleSet(filter.rules);
|
|
3450
|
-
const runs = await readRuns(dir);
|
|
3451
|
-
const rows = /* @__PURE__ */ new Map();
|
|
3452
|
-
let totalRuns = 0;
|
|
3453
|
-
let totalIn = 0;
|
|
3454
|
-
let totalOut = 0;
|
|
3455
|
-
let totalTok = 0;
|
|
3456
|
-
for (const run of runs) {
|
|
3457
|
-
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
3458
|
-
const slice = sliceRun(run, rules);
|
|
3459
|
-
if (!slice.touched) continue;
|
|
3460
|
-
totalRuns += 1;
|
|
3461
|
-
totalIn += slice.inTok;
|
|
3462
|
-
totalOut += slice.outTok;
|
|
3463
|
-
totalTok += slice.totalTok;
|
|
3464
|
-
if (dimension === "dir") {
|
|
3465
|
-
accumulate(rows, run.cwd, slice.inTok, slice.outTok, slice.totalTok, true);
|
|
3466
|
-
continue;
|
|
3467
|
-
}
|
|
3468
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
3469
|
-
for (const usageRecord of slice.usageRecords) {
|
|
3470
|
-
const key = recordKey(usageRecord, dimension);
|
|
3471
|
-
accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
|
|
3472
|
-
seenKeys.add(key);
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
4031
|
+
function createRuleGroup(job) {
|
|
3475
4032
|
return {
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
4033
|
+
cached: 0,
|
|
4034
|
+
cancelled: 0,
|
|
4035
|
+
completed: 0,
|
|
4036
|
+
failed: 0,
|
|
4037
|
+
firstJobIndex: job.index,
|
|
4038
|
+
planned: 0,
|
|
4039
|
+
ruleId: job.ruleId,
|
|
4040
|
+
skipped: 0,
|
|
4041
|
+
terminalDurationMs: 0
|
|
3482
4042
|
};
|
|
3483
4043
|
}
|
|
3484
|
-
|
|
3485
|
-
const
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
const
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
4044
|
+
function createRuleGroups(state) {
|
|
4045
|
+
const groups = new Map([...state.ruleGroups].map(([ruleId, group]) => [ruleId, {
|
|
4046
|
+
...group,
|
|
4047
|
+
jobs: []
|
|
4048
|
+
}]));
|
|
4049
|
+
for (const job of state.activeJobs.values()) groups.get(job.job.ruleId)?.jobs.push(job);
|
|
4050
|
+
return [...groups.values()].sort(compareRuleGroups);
|
|
4051
|
+
}
|
|
4052
|
+
function estimateEta(durationMs, terminal, planned) {
|
|
4053
|
+
if (terminal <= 0 || planned <= 0 || durationMs <= 0) return "eta ?";
|
|
4054
|
+
return `eta ~${formatDuration$1(durationMs * (planned - terminal) / terminal)}`;
|
|
4055
|
+
}
|
|
4056
|
+
function fitRow(row, columns) {
|
|
4057
|
+
if (columns <= 0) return "";
|
|
4058
|
+
if (fastStringWidth(row) <= columns) return row;
|
|
4059
|
+
let limit = columns;
|
|
4060
|
+
while (limit > 0) {
|
|
4061
|
+
const result = fastStringTruncatedWidth(row, {
|
|
4062
|
+
ellipsis: "…",
|
|
4063
|
+
limit
|
|
3496
4064
|
});
|
|
4065
|
+
const index = completeGraphemeBoundary(row, result.index);
|
|
4066
|
+
const visible = row.slice(0, index);
|
|
4067
|
+
const fitted = `${visible}${visible.includes("\x1B") ? "\x1B[0m" : ""}${result.ellipsed ? "…" : ""}`;
|
|
4068
|
+
const overflow = fastStringWidth(fitted) - columns;
|
|
4069
|
+
if (overflow <= 0) return fitted;
|
|
4070
|
+
limit -= overflow;
|
|
3497
4071
|
}
|
|
3498
|
-
|
|
3499
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
3500
|
-
let totalRuns = 0;
|
|
3501
|
-
let totalIn = 0;
|
|
3502
|
-
let totalOut = 0;
|
|
3503
|
-
let totalTok = 0;
|
|
3504
|
-
for (const { slice, ts } of sliced) {
|
|
3505
|
-
const startMs = bucketStart(ts, interval);
|
|
3506
|
-
const bucket = buckets.get(startMs) ?? {
|
|
3507
|
-
inTok: 0,
|
|
3508
|
-
key: bucketLabel(startMs, interval),
|
|
3509
|
-
outTok: 0,
|
|
3510
|
-
runs: 0,
|
|
3511
|
-
startMs,
|
|
3512
|
-
totalTok: 0
|
|
3513
|
-
};
|
|
3514
|
-
bucket.runs += 1;
|
|
3515
|
-
bucket.inTok += slice.inTok;
|
|
3516
|
-
bucket.outTok += slice.outTok;
|
|
3517
|
-
bucket.totalTok += slice.totalTok;
|
|
3518
|
-
buckets.set(startMs, bucket);
|
|
3519
|
-
totalRuns += 1;
|
|
3520
|
-
totalIn += slice.inTok;
|
|
3521
|
-
totalOut += slice.outTok;
|
|
3522
|
-
totalTok += slice.totalTok;
|
|
3523
|
-
}
|
|
3524
|
-
return {
|
|
3525
|
-
buckets: fillBuckets(buckets, interval),
|
|
3526
|
-
interval,
|
|
3527
|
-
totalIn,
|
|
3528
|
-
totalOut,
|
|
3529
|
-
totalRuns,
|
|
3530
|
-
totalTok
|
|
3531
|
-
};
|
|
4072
|
+
return "";
|
|
3532
4073
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
4074
|
+
function formatCollapsedRunningRow(hidden, options) {
|
|
4075
|
+
return fitRow(` └─ ${hidden} more running`, options.columns);
|
|
4076
|
+
}
|
|
4077
|
+
function formatContentRows(groups, state, options, now, contentBudget) {
|
|
4078
|
+
const rows = [];
|
|
4079
|
+
const renderableGroups = groups.filter((group) => group.jobs.some((job) => job.state === "running") || group.failed > 0);
|
|
4080
|
+
const runningCount = renderableGroups.reduce((sum, group) => sum + group.jobs.filter((job) => job.state === "running").length, 0);
|
|
4081
|
+
if (contentBudget === 1) {
|
|
4082
|
+
const failedGroup = renderableGroups.find((group) => group.failed > 0);
|
|
4083
|
+
if (failedGroup) return [formatRuleRow(failedGroup, state, options)];
|
|
4084
|
+
if (runningCount > 0) return [formatCollapsedRunningRow(runningCount, options)];
|
|
4085
|
+
}
|
|
4086
|
+
for (const [groupIndex, group] of renderableGroups.entries()) {
|
|
4087
|
+
if (rows.length >= contentBudget) break;
|
|
4088
|
+
const groupStart = rows.length;
|
|
4089
|
+
rows.push(formatRuleRow(group, state, options));
|
|
4090
|
+
const runningJobs = group.jobs.filter((job) => job.state === "running").sort(compareRunningJobs);
|
|
4091
|
+
const remainingGroups = renderableGroups.length - groupIndex - 1;
|
|
4092
|
+
for (const [index, job] of runningJobs.entries()) {
|
|
4093
|
+
const rowsReservedForLaterGroups = Math.min(remainingGroups, Math.max(contentBudget - rows.length, 0));
|
|
4094
|
+
if (rows.length >= contentBudget) {
|
|
4095
|
+
const hidden = runningJobs.length - index;
|
|
4096
|
+
if (rows.length > groupStart + 1 && hidden > 0) rows[rows.length - 1] = formatCollapsedRunningRow(hidden, options);
|
|
4097
|
+
break;
|
|
4098
|
+
}
|
|
4099
|
+
if (rows.length + 1 + rowsReservedForLaterGroups >= contentBudget) {
|
|
4100
|
+
if (runningJobs.length > 1) rows.push(formatCollapsedRunningRow(runningJobs.length - index, options));
|
|
4101
|
+
break;
|
|
4102
|
+
}
|
|
4103
|
+
rows.push(formatTargetRow(job, options, now, index === runningJobs.length - 1));
|
|
3542
4104
|
}
|
|
3543
4105
|
}
|
|
3544
|
-
return
|
|
4106
|
+
return rows;
|
|
3545
4107
|
}
|
|
3546
|
-
|
|
3547
|
-
const
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
await prune(dir, ts, retentionMonths);
|
|
4108
|
+
function formatDuration$1(ms) {
|
|
4109
|
+
const durationMs = Math.max(ms, 0);
|
|
4110
|
+
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
4111
|
+
return formatDuration(intervalToDuration({
|
|
4112
|
+
end: durationMs,
|
|
4113
|
+
start: 0
|
|
4114
|
+
}));
|
|
3554
4115
|
}
|
|
3555
|
-
function
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
4116
|
+
function formatFooters(state, options, now) {
|
|
4117
|
+
const terminal = terminalCount(state.execution);
|
|
4118
|
+
const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
|
|
4119
|
+
const progressBar = state.progress.planningComplete ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
|
|
4120
|
+
const eta = state.progress.planningComplete ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
|
|
4121
|
+
const projectedTokens = state.progress.planningComplete && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
4122
|
+
const discovering = state.progress.planningComplete ? "" : " jobs (discovering)";
|
|
4123
|
+
return [
|
|
4124
|
+
fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration$1(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
|
|
4125
|
+
fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
|
|
4126
|
+
fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
|
|
4127
|
+
];
|
|
3559
4128
|
}
|
|
3560
|
-
function
|
|
3561
|
-
|
|
4129
|
+
function formatMiniBarSegment(completed, planned, tick, options) {
|
|
4130
|
+
if (options.columns < 60) return "";
|
|
4131
|
+
return ` ${formatMiniBar({
|
|
4132
|
+
completed,
|
|
4133
|
+
planned,
|
|
4134
|
+
tick,
|
|
4135
|
+
width: 10
|
|
4136
|
+
})}`;
|
|
3562
4137
|
}
|
|
3563
|
-
function
|
|
3564
|
-
|
|
4138
|
+
function formatRuleRow(group, state, options) {
|
|
4139
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
4140
|
+
const terminal = group.completed + group.cached + group.failed + group.cancelled + group.skipped;
|
|
4141
|
+
const percent = group.planned > 0 ? Math.floor(terminal / group.planned * 100) : 0;
|
|
4142
|
+
const bar = formatMiniBarSegment(terminal, group.planned, state.animationTick, options);
|
|
4143
|
+
const running = group.jobs.filter((job) => job.state === "running").length;
|
|
4144
|
+
const failed = group.failed > 0 ? ` ${group.failed} failed` : "";
|
|
4145
|
+
const eta = estimateEta(group.terminalDurationMs, terminal, group.planned);
|
|
4146
|
+
return fitRow(`${spinner} ${group.ruleId} ${terminal}/${group.planned} ${percent}%${bar} ${eta} ${running} running${failed}`, options.columns);
|
|
3565
4147
|
}
|
|
3566
|
-
function
|
|
3567
|
-
|
|
3568
|
-
inTok: run.usage.inTok,
|
|
3569
|
-
outTok: run.usage.outTok,
|
|
3570
|
-
totalTok: run.usage.totalTok,
|
|
3571
|
-
touched: true,
|
|
3572
|
-
usageRecords: run.usage.records
|
|
3573
|
-
};
|
|
3574
|
-
const usageRecords = run.usage.records.filter((record) => rules.has(record.ruleId));
|
|
3575
|
-
return {
|
|
3576
|
-
inTok: sumBy(usageRecords, (record) => record.inTok),
|
|
3577
|
-
outTok: sumBy(usageRecords, (record) => record.outTok),
|
|
3578
|
-
totalTok: sumBy(usageRecords, (record) => record.totalTok),
|
|
3579
|
-
touched: usageRecords.length > 0,
|
|
3580
|
-
usageRecords
|
|
3581
|
-
};
|
|
4148
|
+
function formatSpinnerFrame(animationTick, spinnerFrames) {
|
|
4149
|
+
return spinnerFrames[animationTick % Math.max(spinnerFrames.length, 1)] ?? "";
|
|
3582
4150
|
}
|
|
3583
|
-
function
|
|
3584
|
-
const
|
|
3585
|
-
|
|
4151
|
+
function formatTargetRow(jobState, options, now, last) {
|
|
4152
|
+
const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
|
|
4153
|
+
const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
|
|
4154
|
+
const startedAt = jobState.retry?.startedAt ?? jobState.startedAt ?? now;
|
|
4155
|
+
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);
|
|
3586
4156
|
}
|
|
3587
|
-
|
|
3588
|
-
let
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
}
|
|
3592
|
-
if (isEnoent(error)) return [];
|
|
3593
|
-
throw error;
|
|
3594
|
-
}
|
|
3595
|
-
return entries.map((name) => {
|
|
3596
|
-
const match = STATS_FILE_PATTERN.exec(name);
|
|
3597
|
-
return match ? {
|
|
3598
|
-
month: Number(match[1]) * 12 + (Number(match[2]) - 1),
|
|
3599
|
-
name
|
|
3600
|
-
} : void 0;
|
|
3601
|
-
}).filter((entry) => entry !== void 0).sort((left, right) => left.month - right.month);
|
|
4157
|
+
function styleRow(row, state, warnCount, errorCount, options) {
|
|
4158
|
+
let styledRow = row;
|
|
4159
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
4160
|
+
if (spinner) styledRow = styledRow.replace(spinner, colors$2.cyan(spinner));
|
|
4161
|
+
return styledRow.replace(`${state.execution.running} concurrency`, colors$2.cyan(`${state.execution.running} concurrency`)).replace(`${state.totalTokens.toLocaleString("en-US")} tokens`, colors$2.cyan(`${state.totalTokens.toLocaleString("en-US")} tokens`)).replace(`(${state.cachedTokens.toLocaleString("en-US")} cached)`, colors$2.dim(`(${state.cachedTokens.toLocaleString("en-US")} cached)`)).replace(` / ${state.execution.cached} cached / `, ` / ${colors$2.dim(`${state.execution.cached} cached`)} / `).replace(`${warnCount} warn`, colors$2.yellow(`${warnCount} warn`)).replace(`${errorCount} error`, (errorCount > 0 ? colors$2.red : colors$2.gray)(`${errorCount} error`)).replace(`${state.execution.failed} failed`, (state.execution.failed > 0 ? colors$2.red : colors$2.gray)(`${state.execution.failed} failed`)).replace(/\[([█▓░]*?)(░+)\]/g, (_, active, pending) => `[${active}${colors$2.gray(pending)}]`).replace(/\//g, colors$2.gray("/"));
|
|
3602
4162
|
}
|
|
3603
|
-
function
|
|
3604
|
-
|
|
3605
|
-
for (const item of items) total += select(item);
|
|
3606
|
-
return total;
|
|
4163
|
+
function terminalCount(counts) {
|
|
4164
|
+
return counts.completed + counts.failed + counts.cached + counts.skipped + counts.cancelled;
|
|
3607
4165
|
}
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
durationMs: input.durationMs,
|
|
3614
|
-
ruleCounts: input.ruleCounts,
|
|
3615
|
-
usage: {
|
|
3616
|
-
inTok: input.usage.inputTokens,
|
|
3617
|
-
outTok: input.usage.outputTokens,
|
|
3618
|
-
records: input.usage.records.map(toStatsUsageRecord),
|
|
3619
|
-
totalTok: input.usage.totalTokens
|
|
3620
|
-
}
|
|
4166
|
+
function updateProgress(state, progress) {
|
|
4167
|
+
state.execution = { ...progress.execution };
|
|
4168
|
+
state.progress = {
|
|
4169
|
+
...progress,
|
|
4170
|
+
execution: { ...progress.execution }
|
|
3621
4171
|
};
|
|
3622
4172
|
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
4173
|
+
//#endregion
|
|
4174
|
+
//#region src/cli/reporters/progress/tty.ts
|
|
4175
|
+
const clearCurrentLine = "\r\x1B[K";
|
|
4176
|
+
const clearPreviousLine = "\r\x1B[1A\x1B[K";
|
|
4177
|
+
function createTtyProgressRenderer(options) {
|
|
4178
|
+
let interval;
|
|
4179
|
+
let previousRows = 0;
|
|
4180
|
+
const clearPreviousFrame = () => {
|
|
4181
|
+
if (previousRows === 0) return;
|
|
4182
|
+
let sequence = clearCurrentLine;
|
|
4183
|
+
for (let row = 1; row < previousRows; row += 1) sequence += clearPreviousLine;
|
|
4184
|
+
options.write(sequence);
|
|
4185
|
+
previousRows = 0;
|
|
4186
|
+
};
|
|
4187
|
+
const render = () => {
|
|
4188
|
+
clearPreviousFrame();
|
|
4189
|
+
const rows = options.getRows();
|
|
4190
|
+
if (rows.length === 0) return;
|
|
4191
|
+
options.write(rows.join("\n"));
|
|
4192
|
+
previousRows = rows.length;
|
|
3638
4193
|
};
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
cached: 0,
|
|
3645
|
-
cancelled: 0,
|
|
3646
|
-
completed: 0,
|
|
3647
|
-
failed: 0,
|
|
3648
|
-
planned: 0
|
|
4194
|
+
const write = (chunk) => {
|
|
4195
|
+
const wasRendering = interval !== void 0;
|
|
4196
|
+
clearPreviousFrame();
|
|
4197
|
+
options.write(chunk);
|
|
4198
|
+
if (wasRendering) render();
|
|
3649
4199
|
};
|
|
3650
|
-
let startedAt;
|
|
3651
|
-
let endedAt;
|
|
3652
4200
|
return {
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
startedAt = payload.startedAt;
|
|
3658
|
-
},
|
|
3659
|
-
onRunEnd: (payload) => {
|
|
3660
|
-
counts.cached = payload.execution.cached;
|
|
3661
|
-
counts.cancelled = payload.execution.cancelled;
|
|
3662
|
-
counts.completed = payload.execution.completed;
|
|
3663
|
-
counts.failed = payload.execution.failed;
|
|
3664
|
-
counts.planned = payload.execution.planned;
|
|
3665
|
-
endedAt = payload.endedAt;
|
|
4201
|
+
finish: () => {
|
|
4202
|
+
if (interval) {
|
|
4203
|
+
options.clearInterval(interval);
|
|
4204
|
+
interval = void 0;
|
|
3666
4205
|
}
|
|
3667
|
-
|
|
4206
|
+
clearPreviousFrame();
|
|
4207
|
+
},
|
|
4208
|
+
render,
|
|
4209
|
+
start: () => {
|
|
4210
|
+
if (!interval) {
|
|
4211
|
+
interval = options.createInterval(render, options.intervalMs);
|
|
4212
|
+
if (isUnrefableInterval(interval)) interval.unref();
|
|
4213
|
+
}
|
|
4214
|
+
render();
|
|
4215
|
+
},
|
|
4216
|
+
write
|
|
3668
4217
|
};
|
|
3669
4218
|
}
|
|
3670
|
-
function
|
|
3671
|
-
if (!
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
reporter:
|
|
3680
|
-
|
|
3681
|
-
let firstFailure;
|
|
3682
|
-
const deliver = (state, callback) => {
|
|
3683
|
-
if (state.disabled) return;
|
|
3684
|
-
try {
|
|
3685
|
-
callback(state.reporter);
|
|
3686
|
-
} catch (error) {
|
|
3687
|
-
state.disabled = true;
|
|
3688
|
-
firstFailure ??= normalizeReporterError(error);
|
|
3689
|
-
}
|
|
4219
|
+
function isUnrefableInterval(interval) {
|
|
4220
|
+
if (typeof interval !== "object" || interval === null || !("unref" in interval)) return false;
|
|
4221
|
+
return typeof interval.unref === "function";
|
|
4222
|
+
}
|
|
4223
|
+
//#endregion
|
|
4224
|
+
//#region src/cli/reporters/progress/index.ts
|
|
4225
|
+
function createCliProgressReporter(options) {
|
|
4226
|
+
if (!options.isTty) return {
|
|
4227
|
+
dispose: () => {},
|
|
4228
|
+
reporter: createPlainProgressReporter({ write: options.write }),
|
|
4229
|
+
write: options.write
|
|
3690
4230
|
};
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3693
|
-
|
|
4231
|
+
const summary = createSummaryProgressReporter({
|
|
4232
|
+
color: options.color,
|
|
4233
|
+
columns: options.columns,
|
|
4234
|
+
cwd: options.cwd,
|
|
4235
|
+
rows: options.rows,
|
|
4236
|
+
spinnerFrames: cliSpinners.dots.frames
|
|
4237
|
+
});
|
|
4238
|
+
const renderer = createTtyProgressRenderer({
|
|
4239
|
+
clearInterval: (handle) => globalThis.clearInterval(handle),
|
|
4240
|
+
createInterval: (callback, intervalMs) => globalThis.setInterval(() => {
|
|
4241
|
+
summary.tick();
|
|
4242
|
+
callback();
|
|
4243
|
+
}, intervalMs),
|
|
4244
|
+
getRows: summary.getRows,
|
|
4245
|
+
intervalMs: 120,
|
|
4246
|
+
write: options.write
|
|
4247
|
+
});
|
|
4248
|
+
const reporter = createRenderingProgressReporter(summary, renderer);
|
|
4249
|
+
return {
|
|
4250
|
+
dispose: renderer.finish,
|
|
4251
|
+
reporter,
|
|
4252
|
+
write: renderer.write
|
|
3694
4253
|
};
|
|
4254
|
+
}
|
|
4255
|
+
function createRenderingProgressReporter(summary, renderer) {
|
|
3695
4256
|
return {
|
|
3696
4257
|
onDiagnostic: (payload) => {
|
|
3697
|
-
|
|
4258
|
+
summary.onDiagnostic?.(payload);
|
|
4259
|
+
renderer.render();
|
|
3698
4260
|
},
|
|
3699
4261
|
onExecuteEnd: (payload) => {
|
|
3700
|
-
|
|
4262
|
+
summary.onExecuteEnd?.(payload);
|
|
4263
|
+
renderer.render();
|
|
3701
4264
|
},
|
|
3702
4265
|
onExecuteStart: (payload) => {
|
|
3703
|
-
|
|
4266
|
+
summary.onExecuteStart?.(payload);
|
|
4267
|
+
renderer.render();
|
|
3704
4268
|
},
|
|
3705
4269
|
onFileReady: (payload) => {
|
|
3706
|
-
|
|
4270
|
+
summary.onFileReady?.(payload);
|
|
4271
|
+
renderer.render();
|
|
3707
4272
|
},
|
|
3708
4273
|
onJobEnd: (payload) => {
|
|
3709
|
-
|
|
4274
|
+
summary.onJobEnd?.(payload);
|
|
4275
|
+
renderer.render();
|
|
3710
4276
|
},
|
|
3711
4277
|
onJobQueued: (payload) => {
|
|
3712
|
-
|
|
4278
|
+
summary.onJobQueued?.(payload);
|
|
4279
|
+
renderer.render();
|
|
3713
4280
|
},
|
|
3714
4281
|
onJobRetry: (payload) => {
|
|
3715
|
-
|
|
4282
|
+
summary.onJobRetry?.(payload);
|
|
4283
|
+
renderer.render();
|
|
3716
4284
|
},
|
|
3717
4285
|
onJobStart: (payload) => {
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
onPlanningEnd: (payload) => {
|
|
3721
|
-
deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
|
|
4286
|
+
summary.onJobStart?.(payload);
|
|
4287
|
+
renderer.render();
|
|
3722
4288
|
},
|
|
3723
4289
|
onPrepareEnd: (payload) => {
|
|
3724
|
-
|
|
4290
|
+
summary.onPrepareEnd?.(payload);
|
|
4291
|
+
renderer.render();
|
|
3725
4292
|
},
|
|
3726
4293
|
onPrepareStart: (payload) => {
|
|
3727
|
-
|
|
4294
|
+
summary.onPrepareStart?.(payload);
|
|
4295
|
+
renderer.start();
|
|
3728
4296
|
},
|
|
3729
4297
|
onRunEnd: (payload) => {
|
|
3730
|
-
|
|
3731
|
-
|
|
4298
|
+
summary.onRunEnd?.(payload);
|
|
4299
|
+
renderer.render();
|
|
3732
4300
|
},
|
|
3733
4301
|
onUsage: (payload) => {
|
|
3734
|
-
|
|
4302
|
+
summary.onUsage?.(payload);
|
|
4303
|
+
renderer.render();
|
|
3735
4304
|
}
|
|
3736
4305
|
};
|
|
3737
4306
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
return {
|
|
3743
|
-
dir: config?.location ?? getStatsDir(env),
|
|
3744
|
-
retentionMonths: config?.retentionMonths
|
|
3745
|
-
};
|
|
4307
|
+
//#endregion
|
|
4308
|
+
//#region src/cli/commands/lint/errors.ts
|
|
4309
|
+
const colors$1 = createColors({ force: true });
|
|
4310
|
+
function formatCancelledError(error, color) {
|
|
4311
|
+
return `${color ? colors$1.red("error") : "error"} ${error.message}\n`;
|
|
3746
4312
|
}
|
|
3747
|
-
|
|
3748
|
-
const
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
} catch {}
|
|
4313
|
+
function formatRunError(error, color) {
|
|
4314
|
+
const label = color ? colors$1.red("error") : "error";
|
|
4315
|
+
const fileFailures = error.failures.filter((failure) => "file" in failure);
|
|
4316
|
+
const groups = groupFailuresByRule(error.failures.filter((failure) => "job" in failure));
|
|
4317
|
+
return [
|
|
4318
|
+
`${label} ${error.message}`,
|
|
4319
|
+
"",
|
|
4320
|
+
...fileFailures.length > 0 ? [
|
|
4321
|
+
`Failed Files ${fileFailures.length}`,
|
|
4322
|
+
"",
|
|
4323
|
+
...formatFileFailures(fileFailures, color)
|
|
4324
|
+
] : [],
|
|
4325
|
+
...groups.length > 0 ? [
|
|
4326
|
+
`Failed Rules ${groups.length}`,
|
|
4327
|
+
"",
|
|
4328
|
+
...formatFailureGroups(groups, color)
|
|
4329
|
+
] : []
|
|
4330
|
+
].join("\n");
|
|
3766
4331
|
}
|
|
3767
|
-
function
|
|
3768
|
-
|
|
3769
|
-
|
|
4332
|
+
function formatFailureGroups(groups, color) {
|
|
4333
|
+
const lines = [];
|
|
4334
|
+
for (const [ruleId, group] of groups) {
|
|
4335
|
+
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
4336
|
+
const targetLabel = group.length === 1 ? "target" : "targets";
|
|
4337
|
+
lines.push(`${failLabel} ${ruleId} ${group.length} ${targetLabel}`);
|
|
4338
|
+
for (const failure of group) {
|
|
4339
|
+
lines.push(` ${formatTarget(failure)}`);
|
|
4340
|
+
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
4341
|
+
for (const cause of failure.causes) {
|
|
4342
|
+
const code = cause.code === void 0 ? "" : `[${cause.code}] `;
|
|
4343
|
+
lines.push(` Caused by: ${code}${cause.message}`);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
lines.push("");
|
|
4347
|
+
}
|
|
4348
|
+
return lines;
|
|
4349
|
+
}
|
|
4350
|
+
function formatFileFailures(failures, color) {
|
|
4351
|
+
const lines = [];
|
|
4352
|
+
for (const failure of failures) {
|
|
4353
|
+
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
4354
|
+
lines.push(`${failLabel} ${failure.file.path}`);
|
|
4355
|
+
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
4356
|
+
lines.push("");
|
|
4357
|
+
}
|
|
4358
|
+
return lines;
|
|
4359
|
+
}
|
|
4360
|
+
function formatTarget(failure) {
|
|
4361
|
+
const target = failure.job.target.name ? `${failure.job.target.kind} ${failure.job.target.name}` : failure.job.target.kind;
|
|
4362
|
+
return `${failure.job.inputPath} > ${target}`;
|
|
4363
|
+
}
|
|
4364
|
+
function groupFailuresByRule(failures) {
|
|
4365
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4366
|
+
for (const failure of failures) {
|
|
4367
|
+
const group = groups.get(failure.job.ruleId) ?? [];
|
|
4368
|
+
group.push(failure);
|
|
4369
|
+
groups.set(failure.job.ruleId, group);
|
|
4370
|
+
}
|
|
4371
|
+
return [...groups];
|
|
3770
4372
|
}
|
|
3771
4373
|
//#endregion
|
|
3772
4374
|
//#region src/cli/commands/lint/index.ts
|
|
@@ -3779,78 +4381,101 @@ const lint = defineCommand({
|
|
|
3779
4381
|
arguments: "[...files]",
|
|
3780
4382
|
default: true,
|
|
3781
4383
|
description: "Run alint",
|
|
3782
|
-
name: "lint"
|
|
4384
|
+
name: "lint",
|
|
4385
|
+
options: [{
|
|
4386
|
+
description: "Lint only staged, unstaged, and untracked files",
|
|
4387
|
+
flags: "--dirty"
|
|
4388
|
+
}]
|
|
3783
4389
|
});
|
|
3784
4390
|
async function assertConfigExists(cwd, configPath) {
|
|
3785
4391
|
const resolvedConfigPath = resolve(cwd, configPath);
|
|
3786
4392
|
try {
|
|
3787
4393
|
if (!(await stat(resolvedConfigPath)).isFile()) throw new Error(`Config file "${configPath}" is not a file.`);
|
|
3788
4394
|
} catch (error) {
|
|
3789
|
-
if (
|
|
4395
|
+
if (isNodeErrorCode(error, "ENOENT")) throw new Error(`Config file "${configPath}" does not exist.`);
|
|
3790
4396
|
throw error;
|
|
3791
4397
|
}
|
|
3792
4398
|
}
|
|
3793
|
-
function isNodeError(error) {
|
|
3794
|
-
return error instanceof Error && "code" in error;
|
|
3795
|
-
}
|
|
3796
4399
|
async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
3797
|
-
if (options.
|
|
3798
|
-
|
|
4400
|
+
if (options.dirty && files.length > 0) {
|
|
4401
|
+
io.stderr.write("The --dirty option does not accept file arguments.\n");
|
|
4402
|
+
return 2;
|
|
4403
|
+
}
|
|
4404
|
+
const cwd = options.dirty ? await findGitRoot(io.cwd) : io.cwd;
|
|
4405
|
+
const runIo = cwd === io.cwd ? io : {
|
|
4406
|
+
...io,
|
|
4407
|
+
cwd
|
|
4408
|
+
};
|
|
4409
|
+
if (options.config) await assertConfigExists(cwd, options.config);
|
|
4410
|
+
let changedLines;
|
|
4411
|
+
let config;
|
|
4412
|
+
let targets;
|
|
4413
|
+
if (options.dirty) {
|
|
4414
|
+
config = await loadAlintConfig(cwd, options.config);
|
|
4415
|
+
const dirtyTargets = await findDirtyLintTargets(config, cwd);
|
|
4416
|
+
changedLines = dirtyTargets.changedLines;
|
|
4417
|
+
targets = dirtyTargets;
|
|
4418
|
+
if (targets.files.length === 0) return 0;
|
|
4419
|
+
}
|
|
4420
|
+
const session = await createRunSession(runIo, config === void 0 ? { configPath: options.config } : { config });
|
|
3799
4421
|
try {
|
|
3800
|
-
const
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
write: (chunk) => io.stderr.write(chunk)
|
|
4422
|
+
const progress = shouldEnableProgress(options, runIo) ? createCliProgressReporter({
|
|
4423
|
+
color: runIo.stderr.isTTY === true,
|
|
4424
|
+
columns: runIo.stderr.columns ?? 80,
|
|
4425
|
+
cwd,
|
|
4426
|
+
isTty: runIo.stderr.isTTY === true,
|
|
4427
|
+
rows: runIo.stderr.rows,
|
|
4428
|
+
write: (chunk) => runIo.stderr.write(chunk)
|
|
3808
4429
|
}) : void 0;
|
|
3809
4430
|
const restoreProgressConsole = progress ? interceptConsoleOutput({ write: progress.write }) : void 0;
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
};
|
|
4431
|
+
const visibleResult = (runResult) => changedLines === void 0 ? runResult : filterResultToChangedLines(runResult, {
|
|
4432
|
+
changedLines,
|
|
4433
|
+
cwd
|
|
4434
|
+
});
|
|
3815
4435
|
const writeResult = (runResult) => {
|
|
3816
|
-
|
|
4436
|
+
runIo.stdout.write(formatDiagnostics(options.format, visibleResult(runResult), { color: runIo.stdout.isTTY === true }));
|
|
3817
4437
|
};
|
|
4438
|
+
const targetSelection = targets === void 0 ? { inputs: files } : { targets };
|
|
3818
4439
|
let result;
|
|
3819
4440
|
try {
|
|
3820
|
-
result = await
|
|
4441
|
+
result = await executeLint({
|
|
4442
|
+
...targetSelection,
|
|
3821
4443
|
cacheOnly: options.cacheOnly,
|
|
3822
|
-
|
|
4444
|
+
io: runIo,
|
|
3823
4445
|
modelOverride: options.model,
|
|
3824
4446
|
outputLanguage: options.outputLanguage,
|
|
3825
|
-
progress:
|
|
3826
|
-
|
|
4447
|
+
progress: progress?.reporter,
|
|
4448
|
+
runnerOptions: options,
|
|
4449
|
+
session
|
|
3827
4450
|
});
|
|
3828
4451
|
} catch (error) {
|
|
3829
|
-
restoreProgressConsole?.();
|
|
3830
|
-
progress?.dispose();
|
|
3831
4452
|
if (error instanceof NoFilesFoundError) {
|
|
3832
|
-
|
|
4453
|
+
runIo.stderr.write(`${error.message}\n`);
|
|
3833
4454
|
return 2;
|
|
3834
4455
|
}
|
|
4456
|
+
if (error instanceof AlintCachePersistenceError) {
|
|
4457
|
+
writeResult(error.result);
|
|
4458
|
+
const message = errorMessageFrom(error.cause) ?? error.message;
|
|
4459
|
+
runIo.stderr.write(`Cache persistence failed: ${message}\n`);
|
|
4460
|
+
return 1;
|
|
4461
|
+
}
|
|
3835
4462
|
if (error instanceof AlintRunError) {
|
|
3836
|
-
await persistStats(error.result);
|
|
3837
4463
|
writeResult(error.result);
|
|
3838
|
-
|
|
4464
|
+
runIo.stderr.write(formatRunError(error, runIo.stderr.isTTY === true));
|
|
3839
4465
|
return 2;
|
|
3840
4466
|
}
|
|
3841
4467
|
if (error instanceof AlintRunCancelledError) {
|
|
3842
|
-
await persistStats(error.result);
|
|
3843
4468
|
writeResult(error.result);
|
|
3844
|
-
|
|
4469
|
+
runIo.stderr.write(formatCancelledError(error, runIo.stderr.isTTY === true));
|
|
3845
4470
|
return 2;
|
|
3846
4471
|
}
|
|
3847
4472
|
throw error;
|
|
4473
|
+
} finally {
|
|
4474
|
+
restoreProgressConsole?.();
|
|
4475
|
+
progress?.dispose();
|
|
3848
4476
|
}
|
|
3849
|
-
restoreProgressConsole?.();
|
|
3850
|
-
progress?.dispose();
|
|
3851
|
-
await persistStats(result);
|
|
3852
4477
|
writeResult(result);
|
|
3853
|
-
return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
4478
|
+
return visibleResult(result).diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
3854
4479
|
} finally {
|
|
3855
4480
|
await session.shutdown();
|
|
3856
4481
|
}
|
|
@@ -3862,10 +4487,10 @@ function shouldEnableProgress(options, io) {
|
|
|
3862
4487
|
//#endregion
|
|
3863
4488
|
//#region src/cli/commands/lsp/index.ts
|
|
3864
4489
|
const lsp = defineCommand({
|
|
3865
|
-
action: async (context) => (await import("./server-
|
|
4490
|
+
action: async (context) => (await import("./server-C-kuoGl1.mjs")).startLspServer(context.io),
|
|
3866
4491
|
description: "Run alint as a language server over stdio",
|
|
3867
4492
|
examples: [["# Point an editor at the project's own alint", "alint lsp"].join("\n")],
|
|
3868
|
-
help: ["Serve alint diagnostics over the Language Server Protocol on stdin/stdout.", "The server is cache-first
|
|
4493
|
+
help: ["Serve alint diagnostics over the Language Server Protocol on stdin/stdout.", "The server is cache-first. It publishes diagnostics that are already cached, and it never calls a model on its own. Runs that spend tokens happen only through the `alint.runFile` and `alint.runWorkspace` commands."].join("\n\n"),
|
|
3869
4494
|
name: "lsp",
|
|
3870
4495
|
options: [{
|
|
3871
4496
|
description: "Use stdio transport (default; accepted for client compatibility)",
|
|
@@ -3901,14 +4526,14 @@ async function inspectOutputFile(io, file, options) {
|
|
|
3901
4526
|
try {
|
|
3902
4527
|
text = await readFile(filePath, "utf8");
|
|
3903
4528
|
} catch (error) {
|
|
3904
|
-
io.stderr.write(`Could not read output file "${file}": ${errorMessageFrom
|
|
4529
|
+
io.stderr.write(`Could not read output file "${file}": ${errorMessageFrom(error)}\n`);
|
|
3905
4530
|
return 2;
|
|
3906
4531
|
}
|
|
3907
4532
|
let parsed;
|
|
3908
4533
|
try {
|
|
3909
4534
|
parsed = JSON.parse(text);
|
|
3910
4535
|
} catch (error) {
|
|
3911
|
-
io.stderr.write(`Could not parse output file "${file}": ${errorMessageFrom
|
|
4536
|
+
io.stderr.write(`Could not parse output file "${file}": ${errorMessageFrom(error)}\n`);
|
|
3912
4537
|
return 2;
|
|
3913
4538
|
}
|
|
3914
4539
|
if (!isRunResult(parsed)) {
|
|
@@ -3918,7 +4543,7 @@ async function inspectOutputFile(io, file, options) {
|
|
|
3918
4543
|
try {
|
|
3919
4544
|
io.stdout.write(formatDiagnostics(options.format, parsed, { color: io.stdout.isTTY === true }));
|
|
3920
4545
|
} catch (error) {
|
|
3921
|
-
io.stderr.write(`${errorMessageFrom
|
|
4546
|
+
io.stderr.write(`${errorMessageFrom(error)}\n`);
|
|
3922
4547
|
return 2;
|
|
3923
4548
|
}
|
|
3924
4549
|
return parsed.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
@@ -4443,7 +5068,7 @@ async function runStatsCommand(options, io) {
|
|
|
4443
5068
|
}));
|
|
4444
5069
|
return 0;
|
|
4445
5070
|
} catch (error) {
|
|
4446
|
-
io.stderr.write(`${errorMessageFrom
|
|
5071
|
+
io.stderr.write(`${errorMessageFrom(error) ?? "Failed to read stats."}\n`);
|
|
4447
5072
|
return 2;
|
|
4448
5073
|
}
|
|
4449
5074
|
}
|
|
@@ -4479,6 +5104,7 @@ function statsLocation(stats) {
|
|
|
4479
5104
|
const commandTree = [
|
|
4480
5105
|
setup,
|
|
4481
5106
|
config,
|
|
5107
|
+
integrations,
|
|
4482
5108
|
plugin,
|
|
4483
5109
|
output,
|
|
4484
5110
|
defineCommand({
|
|
@@ -4619,4 +5245,4 @@ function shouldCaptureHelp(argv) {
|
|
|
4619
5245
|
return argv.includes("--help") || argv.includes("-h");
|
|
4620
5246
|
}
|
|
4621
5247
|
//#endregion
|
|
4622
|
-
export {
|
|
5248
|
+
export { createRunSession as a, formatJson as i, formatDiagnostics as n, formatStylish as r, executeCli as t };
|