@alint-js/cli 0.5.0 → 0.6.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 +91 -24
- package/dist/bin/index.mjs +1 -1
- package/dist/{cli-ChymU1Wr.mjs → cli-cQihuHqx.mjs} +1930 -1306
- package/dist/index.mjs +1 -1
- package/dist/{server-DgJa6R5l.mjs → server-xx0w7UQL.mjs} +5 -5
- package/package.json +23 -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, 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.6.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,1925 @@ 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 wins, 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
|
+
return {
|
|
2919
|
+
config,
|
|
2920
|
+
cwd: io.cwd,
|
|
2921
|
+
defaultModel,
|
|
2922
|
+
run: async (runOptions) => {
|
|
2923
|
+
const targets = runOptions.targets ?? await findLintTargets({
|
|
2924
|
+
config,
|
|
2925
|
+
cwd: io.cwd,
|
|
2926
|
+
errorOnUnmatchedPattern: true,
|
|
2927
|
+
globInputPaths: true,
|
|
2928
|
+
inputs: runOptions.inputs ?? []
|
|
2514
2929
|
});
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2930
|
+
return runAlint({
|
|
2931
|
+
cacheOnly: runOptions.cacheOnly,
|
|
2932
|
+
config,
|
|
2933
|
+
cwd: io.cwd,
|
|
2934
|
+
defaultModel,
|
|
2935
|
+
directories: targets.directories,
|
|
2936
|
+
files: targets.files,
|
|
2937
|
+
modelOverride: runOptions.modelOverride,
|
|
2938
|
+
outputLanguage: runOptions.outputLanguage,
|
|
2939
|
+
progress: runOptions.progress,
|
|
2940
|
+
projectTargets: runOptions.projectTargets,
|
|
2941
|
+
runner: runOptions.runner,
|
|
2942
|
+
setupConfig,
|
|
2943
|
+
signal: runOptions.signal
|
|
2522
2944
|
});
|
|
2523
2945
|
},
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
state.errorCount = 0;
|
|
2528
|
-
state.execution = createCounts();
|
|
2529
|
-
state.progress = createProgress();
|
|
2530
|
-
state.ruleGroups.clear();
|
|
2531
|
-
state.runStartedAt = startedAt ?? now();
|
|
2532
|
-
state.animationTick = 0;
|
|
2533
|
-
state.cachedTokens = 0;
|
|
2534
|
-
state.jobTokens.clear();
|
|
2535
|
-
state.totalTokens = 0;
|
|
2536
|
-
state.warnCount = 0;
|
|
2537
|
-
},
|
|
2538
|
-
onRunEnd: (payload) => {
|
|
2539
|
-
state.warnCount = payload.diagnostics.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
2540
|
-
state.errorCount = payload.diagnostics.length - state.warnCount;
|
|
2541
|
-
updateProgress(state, payload.progress);
|
|
2542
|
-
state.totalTokens = payload.usage.totalTokens;
|
|
2543
|
-
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
2544
|
-
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
2545
|
-
},
|
|
2546
|
-
onUsage: ({ job, progress, record }) => {
|
|
2547
|
-
updateProgress(state, progress);
|
|
2548
|
-
if (record.totalTokens != null && Number.isFinite(record.totalTokens)) {
|
|
2549
|
-
state.jobTokens.set(job.id, (state.jobTokens.get(job.id) ?? 0) + record.totalTokens);
|
|
2550
|
-
state.totalTokens += record.totalTokens;
|
|
2551
|
-
}
|
|
2552
|
-
},
|
|
2553
|
-
/**
|
|
2554
|
-
* Advances the summary animation on each renderer interval.
|
|
2555
|
-
*
|
|
2556
|
-
* Triggering workflow:
|
|
2557
|
-
*
|
|
2558
|
-
* `createTtyProgressRenderer`
|
|
2559
|
-
* -> `createCliProgressReporter` `createInterval` callback
|
|
2560
|
-
* -> `SummaryProgressReporter.tick` (this handler)
|
|
2561
|
-
*
|
|
2562
|
-
* Upstream:
|
|
2563
|
-
* - `createCliProgressReporter` `createInterval` callback registered by `createTtyProgressRenderer`
|
|
2564
|
-
*
|
|
2565
|
-
* Downstream:
|
|
2566
|
-
* - Increments `state.animationTick`, consumed by {@link createRows}, {@link formatSpinnerFrame}, and {@link formatMiniBarSegment}.
|
|
2567
|
-
*/
|
|
2568
|
-
tick: () => {
|
|
2569
|
-
state.animationTick += 1;
|
|
2570
|
-
}
|
|
2946
|
+
runner: mergeRunnerConfigs(setupConfig.runner, resolveConfigRunner(config)),
|
|
2947
|
+
setupConfig,
|
|
2948
|
+
shutdown: () => shutdown ??= runtime.shutdown()
|
|
2571
2949
|
};
|
|
2572
2950
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2951
|
+
//#endregion
|
|
2952
|
+
//#region src/cli/commands/lint/changed-lines.ts
|
|
2953
|
+
/** Keeps file-wide diagnostics and diagnostics whose locations intersect the current Git change. */
|
|
2954
|
+
function filterResultToChangedLines(result, options) {
|
|
2955
|
+
return {
|
|
2956
|
+
...result,
|
|
2957
|
+
diagnostics: result.diagnostics.filter((diagnostic) => {
|
|
2958
|
+
if (diagnostic.loc === void 0) return true;
|
|
2959
|
+
const changedLinesPath = (isAbsolute(diagnostic.filePath) ? relative(options.cwd, diagnostic.filePath) : diagnostic.filePath).replaceAll("\\", "/");
|
|
2960
|
+
const ranges = options.changedLines.get(changedLinesPath);
|
|
2961
|
+
if (ranges === void 0) return false;
|
|
2962
|
+
const startLine = diagnostic.loc.start.line;
|
|
2963
|
+
const endLine = diagnostic.loc.end?.line ?? startLine;
|
|
2964
|
+
return ranges.some((range) => startLine <= range.endLine && endLine >= range.startLine);
|
|
2965
|
+
})
|
|
2966
|
+
};
|
|
2579
2967
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2968
|
+
//#endregion
|
|
2969
|
+
//#region src/cli/commands/lint/runner.ts
|
|
2970
|
+
/**
|
|
2971
|
+
* Applies this invocation's flags on top of the project's resolved runner.
|
|
2972
|
+
*
|
|
2973
|
+
* The setup/config merge itself lives in `runtime/runner.ts` because the language server needs the
|
|
2974
|
+
* same base without any flags; only the overrides below are CLI-shaped.
|
|
2975
|
+
*/
|
|
2976
|
+
function resolveRunnerConfig(baseRunner, options) {
|
|
2977
|
+
const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
|
|
2978
|
+
const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
|
|
2979
|
+
const runner = {
|
|
2980
|
+
...baseRunner ?? {},
|
|
2981
|
+
cache: resolveCacheOption(baseRunner?.cache, options),
|
|
2982
|
+
ruleConcurrency: ruleConcurrency ?? baseRunner?.ruleConcurrency,
|
|
2983
|
+
stats: options.stats === false ? false : baseRunner?.stats,
|
|
2984
|
+
timeoutMs: timeoutMs ?? baseRunner?.timeoutMs
|
|
2985
|
+
};
|
|
2986
|
+
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
2585
2987
|
}
|
|
2586
|
-
function
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2988
|
+
function parsePositiveIntegerOption(value, label) {
|
|
2989
|
+
if (value === void 0) return;
|
|
2990
|
+
const parsed = Number(value);
|
|
2991
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
|
|
2992
|
+
return parsed;
|
|
2993
|
+
}
|
|
2994
|
+
function resolveCacheOption(baseCache, options) {
|
|
2995
|
+
if (options.cache === false) return false;
|
|
2996
|
+
if (options.cacheLocation !== void 0) return typeof baseCache === "object" ? {
|
|
2997
|
+
...baseCache,
|
|
2998
|
+
location: options.cacheLocation
|
|
2999
|
+
} : { location: options.cacheLocation };
|
|
3000
|
+
return baseCache;
|
|
3001
|
+
}
|
|
3002
|
+
//#endregion
|
|
3003
|
+
//#region src/cli/stats/ci.ts
|
|
3004
|
+
function isCi(env = process.env) {
|
|
3005
|
+
const ci = env.CI;
|
|
3006
|
+
if (ci !== void 0 && ci !== "" && ci !== "0" && ci !== "false") return true;
|
|
3007
|
+
return Boolean(env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.BUILDKITE || env.TF_BUILD);
|
|
3008
|
+
}
|
|
3009
|
+
//#endregion
|
|
3010
|
+
//#region src/cli/stats/since.ts
|
|
3011
|
+
/**
|
|
3012
|
+
* Parse a `--since` time into an inclusive lower-bound timestamp.
|
|
3013
|
+
*
|
|
3014
|
+
* Supported formats:
|
|
3015
|
+
* - `Nh`/`Nd`/`Nw`/`Nm`/`Ny` — now minus N hours/days/weeks/months/years.
|
|
3016
|
+
* Weeks, months, and years are fixed approximations (7/30/365 days). `m` is
|
|
3017
|
+
* months here, not minutes — a day is the finest window this tool tracks.
|
|
3018
|
+
* - `YYYY-MM`/`YYYY-MM-DD` — start of that UTC month/day.
|
|
3019
|
+
*
|
|
3020
|
+
* Edge cases:
|
|
3021
|
+
* - An empty time means "no lower bound".
|
|
3022
|
+
* - An unrecognized time throws.
|
|
3023
|
+
*/
|
|
3024
|
+
const HOUR_MS = 36e5;
|
|
3025
|
+
const UNIT_MS = {
|
|
3026
|
+
d: 24 * HOUR_MS,
|
|
3027
|
+
h: HOUR_MS,
|
|
3028
|
+
m: 720 * HOUR_MS,
|
|
3029
|
+
w: 168 * HOUR_MS,
|
|
3030
|
+
y: 8760 * HOUR_MS
|
|
3031
|
+
};
|
|
3032
|
+
function parseSince(time, now) {
|
|
3033
|
+
if (time === void 0 || time === "") return;
|
|
3034
|
+
const relative = /^(\d+)([dhmwy])$/u.exec(time);
|
|
3035
|
+
if (relative) return now - Number(relative[1]) * UNIT_MS[relative[2]];
|
|
3036
|
+
const calendar = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/u.exec(time);
|
|
3037
|
+
if (calendar) {
|
|
3038
|
+
const year = Number(calendar[1]);
|
|
3039
|
+
const month = Number(calendar[2]);
|
|
3040
|
+
const day = calendar[3] === void 0 ? 1 : Number(calendar[3]);
|
|
3041
|
+
return Date.UTC(year, month - 1, day);
|
|
2592
3042
|
}
|
|
2593
|
-
|
|
3043
|
+
throw new Error(`Invalid --since "${time}": use e.g. 24h, 7d, 2w, 3m, 1y, 2025-01, or 2025-01-15.`);
|
|
2594
3044
|
}
|
|
2595
|
-
|
|
3045
|
+
//#endregion
|
|
3046
|
+
//#region src/cli/stats/jsonl.ts
|
|
3047
|
+
const DEFAULT_RETENTION_MONTHS = 12;
|
|
3048
|
+
const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
|
|
3049
|
+
const DAY_MS = 864e5;
|
|
3050
|
+
const MONTH_LABELS$1 = [
|
|
3051
|
+
"Jan",
|
|
3052
|
+
"Feb",
|
|
3053
|
+
"Mar",
|
|
3054
|
+
"Apr",
|
|
3055
|
+
"May",
|
|
3056
|
+
"Jun",
|
|
3057
|
+
"Jul",
|
|
3058
|
+
"Aug",
|
|
3059
|
+
"Sep",
|
|
3060
|
+
"Oct",
|
|
3061
|
+
"Nov",
|
|
3062
|
+
"Dec"
|
|
3063
|
+
];
|
|
3064
|
+
function createJsonlStatsStore(options) {
|
|
3065
|
+
const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
|
|
2596
3066
|
return {
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
failed: 0,
|
|
2601
|
-
planned,
|
|
2602
|
-
queued: 0,
|
|
2603
|
-
running: 0,
|
|
2604
|
-
skipped: 0
|
|
3067
|
+
query: (filter) => query(options.dir, filter ?? {}, Date.now),
|
|
3068
|
+
querySeries: (filter) => querySeries(options.dir, filter ?? {}, Date.now),
|
|
3069
|
+
record: (input) => record(options.dir, input, Date.now(), retentionMonths)
|
|
2605
3070
|
};
|
|
2606
3071
|
}
|
|
2607
|
-
function
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
jobTokens: /* @__PURE__ */ new Map(),
|
|
2615
|
-
progress: createProgress(),
|
|
2616
|
-
ruleGroups: /* @__PURE__ */ new Map(),
|
|
2617
|
-
totalTokens: 0,
|
|
2618
|
-
warnCount: 0
|
|
3072
|
+
function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
|
|
3073
|
+
const row = rows.get(key) ?? {
|
|
3074
|
+
inTok: 0,
|
|
3075
|
+
key,
|
|
3076
|
+
outTok: 0,
|
|
3077
|
+
runs: 0,
|
|
3078
|
+
totalTok: 0
|
|
2619
3079
|
};
|
|
3080
|
+
row.inTok += inTok;
|
|
3081
|
+
row.outTok += outTok;
|
|
3082
|
+
row.totalTok += totalTok;
|
|
3083
|
+
if (countRun) row.runs += 1;
|
|
3084
|
+
rows.set(key, row);
|
|
3085
|
+
return row;
|
|
2620
3086
|
}
|
|
2621
|
-
function
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
filesTotal: 0,
|
|
2626
|
-
jobsCompleted: 0,
|
|
2627
|
-
jobsStarted: 0,
|
|
2628
|
-
jobsTotal: 0,
|
|
2629
|
-
planningComplete: false
|
|
2630
|
-
};
|
|
3087
|
+
function bucketLabel(startMs, interval) {
|
|
3088
|
+
const date = new Date(startMs);
|
|
3089
|
+
if (interval === "month") return MONTH_LABELS$1[date.getUTCMonth()];
|
|
3090
|
+
return `${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
2631
3091
|
}
|
|
2632
|
-
function
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
const
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
const footer = formatFooters(state, options, now);
|
|
2639
|
-
const actualSeparatorRows = visible.length > 0 ? separatorRows : 0;
|
|
2640
|
-
const visibleFooters = options.rows === void 0 ? footer : footer.slice(0, Math.max(options.rows - visible.length - actualSeparatorRows, 0));
|
|
2641
|
-
if (actualSeparatorRows === 1) visible.push("");
|
|
2642
|
-
visible.push(...visibleFooters);
|
|
2643
|
-
return options.color ? visible.map((row) => styleRow(row, state, state.warnCount, state.errorCount, options)) : visible;
|
|
3092
|
+
function bucketStart(ts, interval) {
|
|
3093
|
+
const date = new Date(ts);
|
|
3094
|
+
if (interval === "month") return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);
|
|
3095
|
+
const dayStart = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
3096
|
+
if (interval === "week") return dayStart - (new Date(dayStart).getUTCDay() + 6) % 7 * DAY_MS;
|
|
3097
|
+
return dayStart;
|
|
2644
3098
|
}
|
|
2645
|
-
function
|
|
2646
|
-
return
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
completed: 0,
|
|
2650
|
-
failed: 0,
|
|
2651
|
-
firstJobIndex: job.index,
|
|
2652
|
-
planned: 0,
|
|
2653
|
-
ruleId: job.ruleId,
|
|
2654
|
-
skipped: 0,
|
|
2655
|
-
terminalDurationMs: 0
|
|
2656
|
-
};
|
|
3099
|
+
function chooseInterval(since, nowMs, timestamps) {
|
|
3100
|
+
if (since !== void 0) return intervalForDuration(nowMs - since);
|
|
3101
|
+
if (timestamps.length < 2) return "day";
|
|
3102
|
+
return intervalForDuration(Math.max(...timestamps) - Math.min(...timestamps));
|
|
2657
3103
|
}
|
|
2658
|
-
function
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
for (
|
|
2664
|
-
|
|
3104
|
+
function fillBuckets(buckets, interval) {
|
|
3105
|
+
if (buckets.size === 0) return [];
|
|
3106
|
+
const starts = [...buckets.keys()].sort((left, right) => left - right);
|
|
3107
|
+
const last = starts.at(-1);
|
|
3108
|
+
const filled = [];
|
|
3109
|
+
for (let startMs = starts[0]; startMs <= last; startMs = nextBucketStart(startMs, interval)) filled.push(buckets.get(startMs) ?? {
|
|
3110
|
+
inTok: 0,
|
|
3111
|
+
key: bucketLabel(startMs, interval),
|
|
3112
|
+
outTok: 0,
|
|
3113
|
+
runs: 0,
|
|
3114
|
+
startMs,
|
|
3115
|
+
totalTok: 0
|
|
3116
|
+
});
|
|
3117
|
+
return filled;
|
|
2665
3118
|
}
|
|
2666
|
-
function
|
|
2667
|
-
|
|
2668
|
-
|
|
3119
|
+
function intervalForDuration(ms) {
|
|
3120
|
+
const days = ms / DAY_MS;
|
|
3121
|
+
if (days <= 21) return "day";
|
|
3122
|
+
if (days <= 120) return "week";
|
|
3123
|
+
return "month";
|
|
2669
3124
|
}
|
|
2670
|
-
function
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3125
|
+
function isEnoent(error) {
|
|
3126
|
+
return error instanceof Error && error.code === "ENOENT";
|
|
3127
|
+
}
|
|
3128
|
+
function monthIndexOf(ts) {
|
|
3129
|
+
const date = new Date(ts);
|
|
3130
|
+
return date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
3131
|
+
}
|
|
3132
|
+
function nextBucketStart(startMs, interval) {
|
|
3133
|
+
if (interval === "month") {
|
|
3134
|
+
const date = new Date(startMs);
|
|
3135
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1);
|
|
3136
|
+
}
|
|
3137
|
+
return startMs + (interval === "week" ? 7 * DAY_MS : DAY_MS);
|
|
3138
|
+
}
|
|
3139
|
+
function parseRunLine(line) {
|
|
3140
|
+
try {
|
|
3141
|
+
const value = JSON.parse(line);
|
|
3142
|
+
if (typeof value.ts === "number" && typeof value.cwd === "string" && value.usage != null) return value;
|
|
3143
|
+
return;
|
|
3144
|
+
} catch {
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
async function prune(dir, ts, retentionMonths) {
|
|
3149
|
+
if (retentionMonths <= 0) return;
|
|
3150
|
+
const cutoff = monthIndexOf(ts) - (retentionMonths - 1);
|
|
3151
|
+
const files = await statsFiles(dir);
|
|
3152
|
+
for (const { month, name } of files) if (month < cutoff) await rm(join(dir, name));
|
|
3153
|
+
}
|
|
3154
|
+
async function query(dir, filter, now) {
|
|
3155
|
+
const dimension = filter.by ?? "model";
|
|
3156
|
+
const since = parseSince(filter.since, now());
|
|
3157
|
+
const rules = ruleSet(filter.rules);
|
|
3158
|
+
const runs = await readRuns(dir);
|
|
3159
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3160
|
+
let totalRuns = 0;
|
|
3161
|
+
let totalIn = 0;
|
|
3162
|
+
let totalOut = 0;
|
|
3163
|
+
let totalTok = 0;
|
|
3164
|
+
for (const run of runs) {
|
|
3165
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
3166
|
+
const slice = sliceRun(run, rules);
|
|
3167
|
+
if (!slice.touched) continue;
|
|
3168
|
+
totalRuns += 1;
|
|
3169
|
+
totalIn += slice.inTok;
|
|
3170
|
+
totalOut += slice.outTok;
|
|
3171
|
+
totalTok += slice.totalTok;
|
|
3172
|
+
if (dimension === "dir") {
|
|
3173
|
+
accumulate(rows, run.cwd, slice.inTok, slice.outTok, slice.totalTok, true);
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
3177
|
+
for (const usageRecord of slice.usageRecords) {
|
|
3178
|
+
const key = recordKey(usageRecord, dimension);
|
|
3179
|
+
accumulate(rows, key, usageRecord.inTok, usageRecord.outTok, usageRecord.totalTok, !seenKeys.has(key));
|
|
3180
|
+
seenKeys.add(key);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return {
|
|
3184
|
+
dimension,
|
|
3185
|
+
rows: [...rows.values()].sort((left, right) => right.totalTok - left.totalTok),
|
|
3186
|
+
totalIn,
|
|
3187
|
+
totalOut,
|
|
3188
|
+
totalRuns,
|
|
3189
|
+
totalTok
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
async function querySeries(dir, filter, now) {
|
|
3193
|
+
const nowMs = now();
|
|
3194
|
+
const since = parseSince(filter.since, nowMs);
|
|
3195
|
+
const rules = ruleSet(filter.rules);
|
|
3196
|
+
const runs = await readRuns(dir);
|
|
3197
|
+
const sliced = [];
|
|
3198
|
+
for (const run of runs) {
|
|
3199
|
+
if (!runInWindow(run, since, filter.cwd)) continue;
|
|
3200
|
+
const slice = sliceRun(run, rules);
|
|
3201
|
+
if (slice.touched) sliced.push({
|
|
3202
|
+
slice,
|
|
3203
|
+
ts: run.ts
|
|
2678
3204
|
});
|
|
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;
|
|
2685
3205
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
3206
|
+
const interval = filter.interval ?? chooseInterval(since, nowMs, sliced.map((entry) => entry.ts));
|
|
3207
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3208
|
+
let totalRuns = 0;
|
|
3209
|
+
let totalIn = 0;
|
|
3210
|
+
let totalOut = 0;
|
|
3211
|
+
let totalTok = 0;
|
|
3212
|
+
for (const { slice, ts } of sliced) {
|
|
3213
|
+
const startMs = bucketStart(ts, interval);
|
|
3214
|
+
const bucket = buckets.get(startMs) ?? {
|
|
3215
|
+
inTok: 0,
|
|
3216
|
+
key: bucketLabel(startMs, interval),
|
|
3217
|
+
outTok: 0,
|
|
3218
|
+
runs: 0,
|
|
3219
|
+
startMs,
|
|
3220
|
+
totalTok: 0
|
|
3221
|
+
};
|
|
3222
|
+
bucket.runs += 1;
|
|
3223
|
+
bucket.inTok += slice.inTok;
|
|
3224
|
+
bucket.outTok += slice.outTok;
|
|
3225
|
+
bucket.totalTok += slice.totalTok;
|
|
3226
|
+
buckets.set(startMs, bucket);
|
|
3227
|
+
totalRuns += 1;
|
|
3228
|
+
totalIn += slice.inTok;
|
|
3229
|
+
totalOut += slice.outTok;
|
|
3230
|
+
totalTok += slice.totalTok;
|
|
3231
|
+
}
|
|
3232
|
+
return {
|
|
3233
|
+
buckets: fillBuckets(buckets, interval),
|
|
3234
|
+
interval,
|
|
3235
|
+
totalIn,
|
|
3236
|
+
totalOut,
|
|
3237
|
+
totalRuns,
|
|
3238
|
+
totalTok
|
|
3239
|
+
};
|
|
2690
3240
|
}
|
|
2691
|
-
function
|
|
2692
|
-
const
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
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));
|
|
3241
|
+
async function readRuns(dir) {
|
|
3242
|
+
const files = await statsFiles(dir);
|
|
3243
|
+
const runs = [];
|
|
3244
|
+
for (const { name } of files) {
|
|
3245
|
+
const content = await readFile(join(dir, name), "utf8");
|
|
3246
|
+
for (const line of content.split("\n")) {
|
|
3247
|
+
if (line.trim() === "") continue;
|
|
3248
|
+
const run = parseRunLine(line);
|
|
3249
|
+
if (run) runs.push(run);
|
|
2718
3250
|
}
|
|
2719
3251
|
}
|
|
2720
|
-
return
|
|
2721
|
-
}
|
|
2722
|
-
function formatDuration$1(ms) {
|
|
2723
|
-
const durationMs = Math.max(ms, 0);
|
|
2724
|
-
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
2725
|
-
return formatDuration(intervalToDuration({
|
|
2726
|
-
end: durationMs,
|
|
2727
|
-
start: 0
|
|
2728
|
-
}));
|
|
3252
|
+
return runs;
|
|
2729
3253
|
}
|
|
2730
|
-
function
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
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
|
-
];
|
|
3254
|
+
async function record(dir, input, ts, retentionMonths) {
|
|
3255
|
+
const stat = {
|
|
3256
|
+
ts,
|
|
3257
|
+
...input
|
|
3258
|
+
};
|
|
3259
|
+
await mkdir(dir, { recursive: true });
|
|
3260
|
+
await appendFile(join(dir, statsFileName(ts)), `${JSON.stringify(stat)}\n`, "utf8");
|
|
3261
|
+
await prune(dir, ts, retentionMonths);
|
|
2742
3262
|
}
|
|
2743
|
-
function
|
|
2744
|
-
if (
|
|
2745
|
-
return
|
|
2746
|
-
|
|
2747
|
-
planned,
|
|
2748
|
-
tick,
|
|
2749
|
-
width: 10
|
|
2750
|
-
})}`;
|
|
3263
|
+
function recordKey(record, dimension) {
|
|
3264
|
+
if (dimension === "rule") return record.ruleId;
|
|
3265
|
+
if (dimension === "operation") return record.operation ?? record.ruleId;
|
|
3266
|
+
return `${record.providerId}/${record.modelId}`;
|
|
2751
3267
|
}
|
|
2752
|
-
function
|
|
2753
|
-
|
|
2754
|
-
const terminal = group.completed + group.cached + group.failed + group.cancelled + group.skipped;
|
|
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);
|
|
3268
|
+
function ruleSet(rules) {
|
|
3269
|
+
return rules && rules.length > 0 ? new Set(rules) : void 0;
|
|
2761
3270
|
}
|
|
2762
|
-
function
|
|
2763
|
-
return
|
|
3271
|
+
function runInWindow(run, since, cwd) {
|
|
3272
|
+
return (since === void 0 || run.ts >= since) && (cwd === void 0 || run.cwd === cwd);
|
|
2764
3273
|
}
|
|
2765
|
-
function
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3274
|
+
function sliceRun(run, rules) {
|
|
3275
|
+
if (rules === void 0) return {
|
|
3276
|
+
inTok: run.usage.inTok,
|
|
3277
|
+
outTok: run.usage.outTok,
|
|
3278
|
+
totalTok: run.usage.totalTok,
|
|
3279
|
+
touched: true,
|
|
3280
|
+
usageRecords: run.usage.records
|
|
3281
|
+
};
|
|
3282
|
+
const usageRecords = run.usage.records.filter((record) => rules.has(record.ruleId));
|
|
3283
|
+
return {
|
|
3284
|
+
inTok: sumBy(usageRecords, (record) => record.inTok),
|
|
3285
|
+
outTok: sumBy(usageRecords, (record) => record.outTok),
|
|
3286
|
+
totalTok: sumBy(usageRecords, (record) => record.totalTok),
|
|
3287
|
+
touched: usageRecords.length > 0,
|
|
3288
|
+
usageRecords
|
|
3289
|
+
};
|
|
2770
3290
|
}
|
|
2771
|
-
function
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
if (spinner) styledRow = styledRow.replace(spinner, colors$2.cyan(spinner));
|
|
2775
|
-
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("/"));
|
|
3291
|
+
function statsFileName(ts) {
|
|
3292
|
+
const date = new Date(ts);
|
|
3293
|
+
return `stats-${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}.jsonl`;
|
|
2776
3294
|
}
|
|
2777
|
-
function
|
|
2778
|
-
|
|
3295
|
+
async function statsFiles(dir) {
|
|
3296
|
+
let entries;
|
|
3297
|
+
try {
|
|
3298
|
+
entries = await readdir(dir);
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
if (isEnoent(error)) return [];
|
|
3301
|
+
throw error;
|
|
3302
|
+
}
|
|
3303
|
+
return entries.map((name) => {
|
|
3304
|
+
const match = STATS_FILE_PATTERN.exec(name);
|
|
3305
|
+
return match ? {
|
|
3306
|
+
month: Number(match[1]) * 12 + (Number(match[2]) - 1),
|
|
3307
|
+
name
|
|
3308
|
+
} : void 0;
|
|
3309
|
+
}).filter((entry) => entry !== void 0).sort((left, right) => left.month - right.month);
|
|
2779
3310
|
}
|
|
2780
|
-
function
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
execution: { ...progress.execution }
|
|
2785
|
-
};
|
|
3311
|
+
function sumBy(items, select) {
|
|
3312
|
+
let total = 0;
|
|
3313
|
+
for (const item of items) total += select(item);
|
|
3314
|
+
return total;
|
|
2786
3315
|
}
|
|
2787
3316
|
//#endregion
|
|
2788
|
-
//#region src/cli/
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3317
|
+
//#region src/cli/stats/record.ts
|
|
3318
|
+
function createRunStat(input) {
|
|
3319
|
+
return {
|
|
3320
|
+
cwd: input.cwd,
|
|
3321
|
+
durationMs: input.durationMs,
|
|
3322
|
+
ruleCounts: input.ruleCounts,
|
|
3323
|
+
usage: {
|
|
3324
|
+
inTok: input.usage.inputTokens,
|
|
3325
|
+
outTok: input.usage.outputTokens,
|
|
3326
|
+
records: input.usage.records.map(toStatsUsageRecord),
|
|
3327
|
+
totalTok: input.usage.totalTokens
|
|
3328
|
+
}
|
|
2800
3329
|
};
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
3330
|
+
}
|
|
3331
|
+
function readOperation(metadata) {
|
|
3332
|
+
if (typeof metadata !== "object" || metadata === null || !("operation" in metadata)) return;
|
|
3333
|
+
const operation = metadata.operation;
|
|
3334
|
+
return typeof operation === "string" ? operation : void 0;
|
|
3335
|
+
}
|
|
3336
|
+
function toStatsUsageRecord(record) {
|
|
3337
|
+
return {
|
|
3338
|
+
filePath: record.filePath,
|
|
3339
|
+
inTok: record.inputTokens ?? 0,
|
|
3340
|
+
modelId: record.modelId,
|
|
3341
|
+
operation: readOperation(record.metadata),
|
|
3342
|
+
outTok: record.outputTokens ?? 0,
|
|
3343
|
+
providerId: record.providerId,
|
|
3344
|
+
ruleId: record.ruleId,
|
|
3345
|
+
totalTok: record.totalTokens ?? 0
|
|
2807
3346
|
};
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3347
|
+
}
|
|
3348
|
+
//#endregion
|
|
3349
|
+
//#region src/cli/commands/lint/stats.ts
|
|
3350
|
+
function createStatsCollector() {
|
|
3351
|
+
const counts = {
|
|
3352
|
+
cached: 0,
|
|
3353
|
+
cancelled: 0,
|
|
3354
|
+
completed: 0,
|
|
3355
|
+
failed: 0,
|
|
3356
|
+
planned: 0
|
|
2813
3357
|
};
|
|
3358
|
+
let startedAt;
|
|
3359
|
+
let endedAt;
|
|
2814
3360
|
return {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
3361
|
+
counts,
|
|
3362
|
+
durationMs: () => startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0,
|
|
3363
|
+
reporter: {
|
|
3364
|
+
onPrepareStart: (payload) => {
|
|
3365
|
+
startedAt = payload.startedAt;
|
|
3366
|
+
},
|
|
3367
|
+
onRunEnd: (payload) => {
|
|
3368
|
+
counts.cached = payload.execution.cached;
|
|
3369
|
+
counts.cancelled = payload.execution.cancelled;
|
|
3370
|
+
counts.completed = payload.execution.completed;
|
|
3371
|
+
counts.failed = payload.execution.failed;
|
|
3372
|
+
counts.planned = payload.execution.planned;
|
|
3373
|
+
endedAt = payload.endedAt;
|
|
2827
3374
|
}
|
|
2828
|
-
|
|
2829
|
-
},
|
|
2830
|
-
write
|
|
3375
|
+
}
|
|
2831
3376
|
};
|
|
2832
3377
|
}
|
|
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
|
|
3378
|
+
function mergeProgressReporters(base, extra) {
|
|
3379
|
+
if (!base) return extra;
|
|
3380
|
+
if (!extra) return base;
|
|
3381
|
+
const baseState = {
|
|
3382
|
+
disabled: false,
|
|
3383
|
+
reporter: base
|
|
2844
3384
|
};
|
|
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
|
|
3385
|
+
const extraState = {
|
|
3386
|
+
disabled: false,
|
|
3387
|
+
reporter: extra
|
|
3388
|
+
};
|
|
3389
|
+
let firstFailure;
|
|
3390
|
+
const deliver = (state, callback) => {
|
|
3391
|
+
if (state.disabled) return;
|
|
3392
|
+
try {
|
|
3393
|
+
callback(state.reporter);
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
state.disabled = true;
|
|
3396
|
+
firstFailure ??= normalizeReporterError(error);
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
const deliverBoth = (callback) => {
|
|
3400
|
+
deliver(baseState, callback);
|
|
3401
|
+
deliver(extraState, callback);
|
|
2867
3402
|
};
|
|
2868
|
-
}
|
|
2869
|
-
function createRenderingProgressReporter(summary, renderer) {
|
|
2870
3403
|
return {
|
|
2871
3404
|
onDiagnostic: (payload) => {
|
|
2872
|
-
|
|
2873
|
-
renderer.render();
|
|
3405
|
+
deliverBoth((reporter) => reporter.onDiagnostic?.(payload));
|
|
2874
3406
|
},
|
|
2875
3407
|
onExecuteEnd: (payload) => {
|
|
2876
|
-
|
|
2877
|
-
renderer.render();
|
|
3408
|
+
deliverBoth((reporter) => reporter.onExecuteEnd?.(payload));
|
|
2878
3409
|
},
|
|
2879
3410
|
onExecuteStart: (payload) => {
|
|
2880
|
-
|
|
2881
|
-
renderer.render();
|
|
3411
|
+
deliverBoth((reporter) => reporter.onExecuteStart?.(payload));
|
|
2882
3412
|
},
|
|
2883
3413
|
onFileReady: (payload) => {
|
|
2884
|
-
|
|
2885
|
-
renderer.render();
|
|
3414
|
+
deliverBoth((reporter) => reporter.onFileReady?.(payload));
|
|
2886
3415
|
},
|
|
2887
3416
|
onJobEnd: (payload) => {
|
|
2888
|
-
|
|
2889
|
-
renderer.render();
|
|
3417
|
+
deliverBoth((reporter) => reporter.onJobEnd?.(payload));
|
|
2890
3418
|
},
|
|
2891
3419
|
onJobQueued: (payload) => {
|
|
2892
|
-
|
|
2893
|
-
renderer.render();
|
|
3420
|
+
deliverBoth((reporter) => reporter.onJobQueued?.(payload));
|
|
2894
3421
|
},
|
|
2895
3422
|
onJobRetry: (payload) => {
|
|
2896
|
-
|
|
2897
|
-
renderer.render();
|
|
3423
|
+
deliverBoth((reporter) => reporter.onJobRetry?.(payload));
|
|
2898
3424
|
},
|
|
2899
3425
|
onJobStart: (payload) => {
|
|
2900
|
-
|
|
2901
|
-
|
|
3426
|
+
deliverBoth((reporter) => reporter.onJobStart?.(payload));
|
|
3427
|
+
},
|
|
3428
|
+
onPlanningEnd: (payload) => {
|
|
3429
|
+
deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
|
|
2902
3430
|
},
|
|
2903
3431
|
onPrepareEnd: (payload) => {
|
|
2904
|
-
|
|
2905
|
-
renderer.render();
|
|
3432
|
+
deliverBoth((reporter) => reporter.onPrepareEnd?.(payload));
|
|
2906
3433
|
},
|
|
2907
3434
|
onPrepareStart: (payload) => {
|
|
2908
|
-
|
|
2909
|
-
renderer.start();
|
|
3435
|
+
deliverBoth((reporter) => reporter.onPrepareStart?.(payload));
|
|
2910
3436
|
},
|
|
2911
3437
|
onRunEnd: (payload) => {
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
},
|
|
2915
|
-
onUsage: (payload) => {
|
|
2916
|
-
|
|
2917
|
-
|
|
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;
|
|
3036
|
-
}
|
|
3037
|
-
if (options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
|
|
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();
|
|
3438
|
+
deliverBoth((reporter) => reporter.onRunEnd?.(payload));
|
|
3439
|
+
if (firstFailure) throw firstFailure;
|
|
3440
|
+
},
|
|
3441
|
+
onUsage: (payload) => {
|
|
3442
|
+
deliverBoth((reporter) => reporter.onUsage?.(payload));
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3062
3445
|
}
|
|
3063
|
-
function
|
|
3064
|
-
|
|
3446
|
+
function resolveStatsWrite(stats, env) {
|
|
3447
|
+
if (isCi(env)) return;
|
|
3448
|
+
if (!(stats === false ? false : stats === void 0 || stats === true ? true : stats.enabled !== false)) return;
|
|
3449
|
+
const config = typeof stats === "object" ? stats : void 0;
|
|
3450
|
+
return {
|
|
3451
|
+
dir: config?.location ?? getStatsDir(env),
|
|
3452
|
+
retentionMonths: config?.retentionMonths
|
|
3453
|
+
};
|
|
3065
3454
|
}
|
|
3066
|
-
function
|
|
3067
|
-
|
|
3068
|
-
|
|
3455
|
+
async function writeRunStats(target, collector, result, cwd) {
|
|
3456
|
+
const store = createJsonlStatsStore({
|
|
3457
|
+
dir: target.dir,
|
|
3458
|
+
retentionMonths: target.retentionMonths
|
|
3459
|
+
});
|
|
3460
|
+
try {
|
|
3461
|
+
await store.record(createRunStat({
|
|
3462
|
+
cwd,
|
|
3463
|
+
durationMs: collector.durationMs(),
|
|
3464
|
+
ruleCounts: {
|
|
3465
|
+
cached: result.execution.cached,
|
|
3466
|
+
cancelled: result.execution.cancelled,
|
|
3467
|
+
completed: result.execution.completed,
|
|
3468
|
+
failed: result.execution.failed,
|
|
3469
|
+
planned: result.execution.planned
|
|
3470
|
+
},
|
|
3471
|
+
usage: result.usage
|
|
3472
|
+
}));
|
|
3473
|
+
} catch {}
|
|
3069
3474
|
}
|
|
3070
|
-
|
|
3071
|
-
if (
|
|
3072
|
-
return
|
|
3475
|
+
function normalizeReporterError(error) {
|
|
3476
|
+
if (error instanceof Error) return error;
|
|
3477
|
+
return new Error(error != null ? String(error) : "Unknown progress reporter error.");
|
|
3073
3478
|
}
|
|
3074
|
-
|
|
3479
|
+
//#endregion
|
|
3480
|
+
//#region src/cli/commands/lint/execution.ts
|
|
3481
|
+
/** Runs one session while keeping CLI flags and stats behavior identical for every caller. */
|
|
3482
|
+
async function executeLint(options) {
|
|
3483
|
+
const runner = resolveRunnerConfig(options.session.runner, options.runnerOptions);
|
|
3484
|
+
const statsTarget = resolveStatsWrite(runner?.stats, options.io.env);
|
|
3485
|
+
const statsCollector = statsTarget ? createStatsCollector() : void 0;
|
|
3486
|
+
const persistStats = async (result) => {
|
|
3487
|
+
if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, result, options.session.cwd);
|
|
3488
|
+
};
|
|
3489
|
+
const targetSelection = options.targets === void 0 ? { inputs: options.inputs } : { targets: options.targets };
|
|
3490
|
+
let result;
|
|
3075
3491
|
try {
|
|
3076
|
-
|
|
3492
|
+
result = await options.session.run({
|
|
3493
|
+
...targetSelection,
|
|
3494
|
+
cacheOnly: options.cacheOnly,
|
|
3495
|
+
modelOverride: options.modelOverride,
|
|
3496
|
+
outputLanguage: options.outputLanguage,
|
|
3497
|
+
progress: mergeProgressReporters(options.progress, statsCollector?.reporter),
|
|
3498
|
+
runner,
|
|
3499
|
+
signal: options.createSignal?.()
|
|
3500
|
+
});
|
|
3077
3501
|
} catch (error) {
|
|
3078
|
-
if (
|
|
3502
|
+
if (error instanceof AlintRunError || error instanceof AlintRunCancelledError || error instanceof AlintCachePersistenceError) await persistStats(error.result);
|
|
3079
3503
|
throw error;
|
|
3080
3504
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
* Streams directory entries so unmatched trees never accumulate into recursive child arrays.
|
|
3084
|
-
* The consumer owns result ordering because filesystem iteration order is not stable.
|
|
3085
|
-
*/
|
|
3086
|
-
async function* walkFiles(root, options) {
|
|
3087
|
-
const pendingDirectories = [root];
|
|
3088
|
-
while (pendingDirectories.length > 0) {
|
|
3089
|
-
const currentDirectory = pendingDirectories.pop();
|
|
3090
|
-
if (currentDirectory === void 0) continue;
|
|
3091
|
-
const childDirectories = [];
|
|
3092
|
-
const directory = await opendir(currentDirectory, { bufferSize: directoryBufferSize });
|
|
3093
|
-
for await (const entry of directory) {
|
|
3094
|
-
const path = resolve(currentDirectory, entry.name);
|
|
3095
|
-
if (entry.isDirectory()) {
|
|
3096
|
-
if (!await shouldPruneDirectory(path, options)) childDirectories.push(path);
|
|
3097
|
-
continue;
|
|
3098
|
-
}
|
|
3099
|
-
if (entry.isFile()) yield path;
|
|
3100
|
-
}
|
|
3101
|
-
for (const path of childDirectories.reverse()) pendingDirectories.push(path);
|
|
3102
|
-
}
|
|
3505
|
+
await persistStats(result);
|
|
3506
|
+
return result;
|
|
3103
3507
|
}
|
|
3104
3508
|
//#endregion
|
|
3105
|
-
//#region src/cli/
|
|
3509
|
+
//#region src/cli/commands/integrations/stop-gate/timeout.ts
|
|
3510
|
+
const maximumTimerChunkMs = 2147483647;
|
|
3106
3511
|
/**
|
|
3107
|
-
*
|
|
3108
|
-
*
|
|
3109
|
-
*
|
|
3110
|
-
* the same way, or the editor reads a different file and reports nothing.
|
|
3512
|
+
* Node shortens an overflowing timer to one millisecond. The public Stop Gate limit is currently
|
|
3513
|
+
* below that boundary, while chunking here keeps this process-level safety helper correct if the
|
|
3514
|
+
* host budget changes independently later.
|
|
3111
3515
|
*/
|
|
3112
|
-
function
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3516
|
+
function createTimeoutSignal(timeoutMs) {
|
|
3517
|
+
const controller = new AbortController();
|
|
3518
|
+
let remainingMs = timeoutMs;
|
|
3519
|
+
let timer;
|
|
3520
|
+
const schedule = () => {
|
|
3521
|
+
const delay = Math.min(remainingMs, maximumTimerChunkMs);
|
|
3522
|
+
timer = setTimeout(() => {
|
|
3523
|
+
remainingMs -= delay;
|
|
3524
|
+
if (remainingMs > 0) {
|
|
3525
|
+
schedule();
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
controller.abort(new DOMException("Stop Gate timed out.", "TimeoutError"));
|
|
3529
|
+
}, delay);
|
|
3120
3530
|
};
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
return normalizeConfig(config).reduce((merged, item) => item.runner ? {
|
|
3126
|
-
...merged,
|
|
3127
|
-
...item.runner
|
|
3128
|
-
} : merged, void 0);
|
|
3129
|
-
}
|
|
3130
|
-
function mergeCacheConfig(setupCache, configCache) {
|
|
3131
|
-
if (configCache === void 0) return setupCache;
|
|
3132
|
-
if (typeof configCache === "boolean") return configCache;
|
|
3133
|
-
if (typeof setupCache === "object") return {
|
|
3134
|
-
...setupCache,
|
|
3135
|
-
...configCache
|
|
3531
|
+
schedule();
|
|
3532
|
+
return {
|
|
3533
|
+
dispose: () => clearTimeout(timer),
|
|
3534
|
+
signal: controller.signal
|
|
3136
3535
|
};
|
|
3137
|
-
return configCache;
|
|
3138
3536
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3537
|
+
//#endregion
|
|
3538
|
+
//#region src/cli/commands/integrations/stop-gate/run.ts
|
|
3539
|
+
async function runStopGateLint(options) {
|
|
3540
|
+
const dirtyTargets = options.stopGate.target === "dirty-files" ? await findDirtyLintTargets(options.config, options.cwd) : void 0;
|
|
3541
|
+
const targets = dirtyTargets ?? await findLintTargets({
|
|
3542
|
+
config: options.config,
|
|
3543
|
+
cwd: options.cwd,
|
|
3544
|
+
errorOnUnmatchedPattern: true,
|
|
3545
|
+
globInputPaths: true,
|
|
3546
|
+
inputs: ["."]
|
|
3547
|
+
});
|
|
3548
|
+
if (options.stopGate.target === "dirty-files" && targets.files.length === 0) return;
|
|
3549
|
+
const session = await createRunSession({
|
|
3550
|
+
...options.io,
|
|
3551
|
+
cwd: options.cwd
|
|
3552
|
+
}, { config: options.config });
|
|
3553
|
+
let timeout;
|
|
3554
|
+
try {
|
|
3555
|
+
let result;
|
|
3556
|
+
let runError;
|
|
3557
|
+
try {
|
|
3558
|
+
result = await executeLint({
|
|
3559
|
+
createSignal: () => {
|
|
3560
|
+
timeout = createTimeoutSignal(options.stopGate.timeoutMs);
|
|
3561
|
+
return timeout.signal;
|
|
3562
|
+
},
|
|
3563
|
+
io: options.io,
|
|
3564
|
+
runnerOptions: { format: "json" },
|
|
3565
|
+
session,
|
|
3566
|
+
targets
|
|
3567
|
+
});
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
if (!(error instanceof AlintRunError)) throw error;
|
|
3570
|
+
runError = error;
|
|
3571
|
+
result = error.result;
|
|
3572
|
+
}
|
|
3573
|
+
if (dirtyTargets !== void 0) result = filterResultToChangedLines(result, {
|
|
3574
|
+
changedLines: dirtyTargets.changedLines,
|
|
3575
|
+
cwd: options.cwd
|
|
3576
|
+
});
|
|
3577
|
+
return runError === void 0 ? {
|
|
3578
|
+
files: targets.files,
|
|
3579
|
+
result
|
|
3580
|
+
} : {
|
|
3581
|
+
error: runError,
|
|
3582
|
+
files: targets.files,
|
|
3583
|
+
result
|
|
3584
|
+
};
|
|
3585
|
+
} finally {
|
|
3586
|
+
timeout?.dispose();
|
|
3587
|
+
await session.shutdown();
|
|
3588
|
+
}
|
|
3147
3589
|
}
|
|
3148
3590
|
//#endregion
|
|
3149
|
-
//#region src/cli/
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3591
|
+
//#region src/cli/commands/integrations/stop-gate/stop-gate.ts
|
|
3592
|
+
const stopGate = defineCommand({
|
|
3593
|
+
action: (context, options) => runStopGateCommand(options, context.io),
|
|
3594
|
+
description: "Run the repository Stop Gate integration",
|
|
3595
|
+
exactArguments: true,
|
|
3596
|
+
name: "stop-gate",
|
|
3597
|
+
options: [{
|
|
3598
|
+
description: "Codex session id",
|
|
3599
|
+
flags: "--session-id <id>"
|
|
3600
|
+
}]
|
|
3601
|
+
});
|
|
3602
|
+
async function runStopGateCommand(options, io) {
|
|
3603
|
+
if (!options.sessionId) {
|
|
3604
|
+
io.stderr.write("Stop Gate requires --session-id.\n");
|
|
3605
|
+
return 2;
|
|
3606
|
+
}
|
|
3607
|
+
try {
|
|
3608
|
+
const cwd = await findGitRoot(io.cwd);
|
|
3609
|
+
const loaded = await loadAlintConfigWithMetadata(cwd);
|
|
3610
|
+
if (loaded.configFile === void 0) {
|
|
3611
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3612
|
+
errorCount: 0,
|
|
3613
|
+
schemaVersion: 2,
|
|
3614
|
+
status: "inactive",
|
|
3615
|
+
warningCount: 0
|
|
3172
3616
|
});
|
|
3173
|
-
return
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
projectTargets: runOptions.projectTargets,
|
|
3184
|
-
runner: runOptions.runner,
|
|
3185
|
-
setupConfig,
|
|
3186
|
-
signal: runOptions.signal
|
|
3617
|
+
return 0;
|
|
3618
|
+
}
|
|
3619
|
+
const stopGate = resolveStopGateConfig(loaded.config, cwd);
|
|
3620
|
+
if (!stopGate.enabled) {
|
|
3621
|
+
await removeStopGateReport(options.sessionId);
|
|
3622
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3623
|
+
errorCount: 0,
|
|
3624
|
+
schemaVersion: 2,
|
|
3625
|
+
status: "inactive",
|
|
3626
|
+
warningCount: 0
|
|
3187
3627
|
});
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3628
|
+
return 0;
|
|
3629
|
+
}
|
|
3630
|
+
const lint = await runStopGateLint({
|
|
3631
|
+
config: loaded.config,
|
|
3632
|
+
cwd,
|
|
3633
|
+
io,
|
|
3634
|
+
stopGate
|
|
3635
|
+
});
|
|
3636
|
+
if (lint === void 0) {
|
|
3637
|
+
await removeStopGateReport(options.sessionId);
|
|
3638
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3639
|
+
errorCount: 0,
|
|
3640
|
+
schemaVersion: 2,
|
|
3641
|
+
status: "no-dirty-files",
|
|
3642
|
+
warningCount: 0
|
|
3643
|
+
});
|
|
3644
|
+
return 0;
|
|
3645
|
+
}
|
|
3646
|
+
if (lint.error !== void 0) {
|
|
3647
|
+
const reportPath = await writeStopGateReport(options.sessionId, {
|
|
3648
|
+
cwd,
|
|
3649
|
+
diagnostics: lint.result.diagnostics,
|
|
3650
|
+
execution: lint.result.execution,
|
|
3651
|
+
failure: {
|
|
3652
|
+
details: lint.error.failures,
|
|
3653
|
+
message: lint.error.message
|
|
3654
|
+
},
|
|
3655
|
+
files: lint.files,
|
|
3656
|
+
schemaVersion: 1,
|
|
3657
|
+
target: stopGate.target,
|
|
3658
|
+
usage: lint.result.usage
|
|
3659
|
+
});
|
|
3660
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3661
|
+
errorCount: 0,
|
|
3662
|
+
message: lint.error.message,
|
|
3663
|
+
reportPath,
|
|
3664
|
+
schemaVersion: 2,
|
|
3665
|
+
status: "runtime-error",
|
|
3666
|
+
warningCount: 0
|
|
3667
|
+
});
|
|
3668
|
+
return 1;
|
|
3669
|
+
}
|
|
3670
|
+
const errorCount = lint.result.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
3671
|
+
const warningCount = lint.result.diagnostics.length - errorCount;
|
|
3672
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
3673
|
+
await removeStopGateReport(options.sessionId);
|
|
3674
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3675
|
+
errorCount,
|
|
3676
|
+
schemaVersion: 2,
|
|
3677
|
+
status: "clean",
|
|
3678
|
+
warningCount
|
|
3679
|
+
});
|
|
3680
|
+
return 0;
|
|
3681
|
+
}
|
|
3682
|
+
const reportPath = await writeStopGateReport(options.sessionId, {
|
|
3683
|
+
cwd,
|
|
3684
|
+
diagnostics: lint.result.diagnostics,
|
|
3685
|
+
execution: lint.result.execution,
|
|
3686
|
+
files: lint.files,
|
|
3687
|
+
schemaVersion: 1,
|
|
3688
|
+
target: stopGate.target,
|
|
3689
|
+
usage: lint.result.usage
|
|
3690
|
+
});
|
|
3691
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3692
|
+
errorCount,
|
|
3693
|
+
findingsHash: fingerprintDiagnostics(lint.result.diagnostics),
|
|
3694
|
+
reportPath,
|
|
3695
|
+
schemaVersion: 2,
|
|
3696
|
+
status: errorCount > 0 ? "errors" : "warnings",
|
|
3697
|
+
warningCount
|
|
3698
|
+
});
|
|
3699
|
+
return 0;
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
writeEnvelope((chunk) => io.stdout.write(chunk), {
|
|
3702
|
+
errorCount: 0,
|
|
3703
|
+
message: error instanceof StopGateReportTooLargeError ? error.message : error instanceof AlintRunCancelledError ? "Stop Gate timed out before lint completed." : errorMessageFrom(error) ?? "unknown error",
|
|
3704
|
+
schemaVersion: 2,
|
|
3705
|
+
status: "runtime-error",
|
|
3706
|
+
warningCount: 0
|
|
3707
|
+
});
|
|
3708
|
+
return 1;
|
|
3709
|
+
}
|
|
3193
3710
|
}
|
|
3194
3711
|
//#endregion
|
|
3195
|
-
//#region src/cli/commands/
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
return
|
|
3205
|
-
`${label} ${error.message}`,
|
|
3206
|
-
"",
|
|
3207
|
-
...fileFailures.length > 0 ? [
|
|
3208
|
-
`Failed Files ${fileFailures.length}`,
|
|
3209
|
-
"",
|
|
3210
|
-
...formatFileFailures(fileFailures, color)
|
|
3211
|
-
] : [],
|
|
3212
|
-
...groups.length > 0 ? [
|
|
3213
|
-
`Failed Rules ${groups.length}`,
|
|
3214
|
-
"",
|
|
3215
|
-
...formatFailureGroups(groups, color)
|
|
3216
|
-
] : []
|
|
3217
|
-
].join("\n");
|
|
3712
|
+
//#region src/cli/commands/integrations/integrations.ts
|
|
3713
|
+
const integrations = defineCommand({
|
|
3714
|
+
children: [stopGate],
|
|
3715
|
+
description: "Run external integrations",
|
|
3716
|
+
name: "integrations"
|
|
3717
|
+
});
|
|
3718
|
+
//#endregion
|
|
3719
|
+
//#region src/cli/reporters/json.ts
|
|
3720
|
+
function formatJson(result) {
|
|
3721
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
3218
3722
|
}
|
|
3219
|
-
|
|
3723
|
+
//#endregion
|
|
3724
|
+
//#region src/cli/reporters/stylish.ts
|
|
3725
|
+
const colors$3 = createColors({ force: true });
|
|
3726
|
+
function formatStylish(input, options = {}) {
|
|
3727
|
+
const diagnostics = Array.isArray(input) ? input : input.diagnostics;
|
|
3728
|
+
const result = Array.isArray(input) ? void 0 : input;
|
|
3729
|
+
const style = createStyle(options.color === true);
|
|
3730
|
+
if (diagnostics.length === 0) return result?.execution?.cached ? `${formatSummary(diagnostics, result, style)}\n` : "";
|
|
3731
|
+
const diagnosticsByFile = /* @__PURE__ */ new Map();
|
|
3732
|
+
for (const diagnostic of diagnostics) {
|
|
3733
|
+
const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
|
|
3734
|
+
if (fileDiagnostics) {
|
|
3735
|
+
fileDiagnostics.push(diagnostic);
|
|
3736
|
+
continue;
|
|
3737
|
+
}
|
|
3738
|
+
diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
|
|
3739
|
+
}
|
|
3220
3740
|
const lines = [];
|
|
3221
|
-
for (const [
|
|
3222
|
-
|
|
3223
|
-
const
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
lines.push(`
|
|
3228
|
-
for (const cause of failure.causes) {
|
|
3229
|
-
const code = cause.code === void 0 ? "" : `[${cause.code}] `;
|
|
3230
|
-
lines.push(` Caused by: ${code}${cause.message}`);
|
|
3231
|
-
}
|
|
3741
|
+
for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
|
|
3742
|
+
lines.push(style.file(filePath));
|
|
3743
|
+
for (const diagnostic of fileDiagnostics) {
|
|
3744
|
+
const line = diagnostic.loc?.start.line ?? 0;
|
|
3745
|
+
const column = diagnostic.loc?.start.column ?? 0;
|
|
3746
|
+
const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
|
|
3747
|
+
lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
|
|
3232
3748
|
}
|
|
3233
3749
|
lines.push("");
|
|
3234
3750
|
}
|
|
3235
|
-
|
|
3751
|
+
lines.push("", formatSummary(diagnostics, result, style));
|
|
3752
|
+
return `${lines.join("\n")}\n`;
|
|
3236
3753
|
}
|
|
3237
|
-
function
|
|
3238
|
-
|
|
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;
|
|
3754
|
+
function countCachedDiagnostics(diagnostics, severity) {
|
|
3755
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity && diagnostic.cached === true).length;
|
|
3246
3756
|
}
|
|
3247
|
-
function
|
|
3248
|
-
|
|
3249
|
-
return `${failure.job.inputPath} > ${target}`;
|
|
3757
|
+
function countDiagnostics$1(diagnostics, severity) {
|
|
3758
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
3250
3759
|
}
|
|
3251
|
-
function
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3760
|
+
function createStyle(color) {
|
|
3761
|
+
if (!color) return {
|
|
3762
|
+
error: identity,
|
|
3763
|
+
file: identity,
|
|
3764
|
+
location: identity,
|
|
3765
|
+
ruleId: identity,
|
|
3766
|
+
summaryToken: identity,
|
|
3767
|
+
warning: identity
|
|
3768
|
+
};
|
|
3769
|
+
return {
|
|
3770
|
+
error: colors$3.red,
|
|
3771
|
+
file: colors$3.underline,
|
|
3772
|
+
location: colors$3.dim,
|
|
3773
|
+
ruleId: colors$3.dim,
|
|
3774
|
+
summaryToken: colors$3.cyan,
|
|
3775
|
+
warning: colors$3.yellow
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
function formatDiagnosticCount(total, cached, label) {
|
|
3779
|
+
return `${total} ${label}${cached > 0 ? ` (${cached} cached)` : ""}`;
|
|
3780
|
+
}
|
|
3781
|
+
function formatExecutionSummary(execution) {
|
|
3782
|
+
if (!execution || execution.cached === 0) return;
|
|
3783
|
+
return `${execution.cached}/${execution.planned} cached`;
|
|
3784
|
+
}
|
|
3785
|
+
function formatSummary(diagnostics, result, style) {
|
|
3786
|
+
const warnCount = countDiagnostics$1(diagnostics, "warn");
|
|
3787
|
+
const errorCount = countDiagnostics$1(diagnostics, "error");
|
|
3788
|
+
const cachedWarnCount = countCachedDiagnostics(diagnostics, "warn");
|
|
3789
|
+
const cachedErrorCount = countCachedDiagnostics(diagnostics, "error");
|
|
3790
|
+
const tokens = result === void 0 ? void 0 : formatTokenSummary(result);
|
|
3791
|
+
const problemSummary = [style.warning(formatDiagnosticCount(warnCount, cachedWarnCount, "warn")), style.error(formatDiagnosticCount(errorCount, cachedErrorCount, "error"))].join(" / ");
|
|
3792
|
+
if (tokens === void 0) return problemSummary;
|
|
3793
|
+
const execution = formatExecutionSummary(result?.execution);
|
|
3794
|
+
return [
|
|
3795
|
+
problemSummary,
|
|
3796
|
+
style.summaryToken(tokens),
|
|
3797
|
+
execution
|
|
3798
|
+
].filter((segment) => segment !== void 0).join(" | ");
|
|
3799
|
+
}
|
|
3800
|
+
function formatTokenSummary(result) {
|
|
3801
|
+
const liveTokens = result.usage.totalTokens.toLocaleString("en-US");
|
|
3802
|
+
const cachedTokens = result.usage.cached?.totalTokens ?? 0;
|
|
3803
|
+
return `${liveTokens} tokens${cachedTokens > 0 ? ` (${cachedTokens.toLocaleString("en-US")} cached)` : ""}`;
|
|
3804
|
+
}
|
|
3805
|
+
function identity(value) {
|
|
3806
|
+
return value;
|
|
3259
3807
|
}
|
|
3260
3808
|
//#endregion
|
|
3261
|
-
//#region src/cli/
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3809
|
+
//#region src/cli/reporters/index.ts
|
|
3810
|
+
function formatDiagnostics(format, result, options = {}) {
|
|
3811
|
+
if (format === "json") return formatJson(result);
|
|
3812
|
+
if (format === "stylish") return formatStylish(result, { color: options.color });
|
|
3813
|
+
throw new Error(`Unknown reporter "${format}".`);
|
|
3814
|
+
}
|
|
3815
|
+
//#endregion
|
|
3816
|
+
//#region src/cli/reporters/progress/plain.ts
|
|
3817
|
+
function createPlainProgressReporter(options) {
|
|
3818
|
+
const writeLine = (line) => options.write(`${line}\n`);
|
|
3819
|
+
return {
|
|
3820
|
+
onJobRetry: () => {},
|
|
3821
|
+
onJobStart: (payload) => {
|
|
3822
|
+
const target = payload.job.target.name ? `${payload.job.target.kind} ${payload.job.target.name}` : payload.job.target.kind;
|
|
3823
|
+
writeLine(`scan ${payload.job.inputPath} > ${target} > ${payload.job.ruleId}`);
|
|
3824
|
+
},
|
|
3825
|
+
onPrepareEnd: ({ filesTotal }) => writeLine(`alint prepared: ${filesTotal} files`),
|
|
3826
|
+
onPrepareStart: () => writeLine("alint preparing"),
|
|
3827
|
+
onRunEnd: (payload) => {
|
|
3828
|
+
const warnCount = countDiagnostics(payload.diagnostics, "warn");
|
|
3829
|
+
const errorCount = countDiagnostics(payload.diagnostics, "error");
|
|
3830
|
+
const state = payload.execution.failed > 0 ? "failed" : "finished";
|
|
3831
|
+
const { cached, cancelled, completed, failed, skipped } = payload.execution;
|
|
3832
|
+
writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens, ${completed} completed, ${cached} cached, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped`);
|
|
3833
|
+
}
|
|
3277
3834
|
};
|
|
3278
|
-
return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
|
|
3279
3835
|
}
|
|
3280
|
-
function
|
|
3281
|
-
|
|
3282
|
-
const parsed = Number(value);
|
|
3283
|
-
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
|
|
3284
|
-
return parsed;
|
|
3836
|
+
function countDiagnostics(diagnostics, severity) {
|
|
3837
|
+
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
3285
3838
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3839
|
+
//#endregion
|
|
3840
|
+
//#region src/cli/reporters/progress/summary.ts
|
|
3841
|
+
const colors$2 = createColors({ force: true });
|
|
3842
|
+
const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3843
|
+
function createSummaryProgressReporter(options) {
|
|
3844
|
+
const now = Date.now;
|
|
3845
|
+
const state = createInitialState();
|
|
3846
|
+
return {
|
|
3847
|
+
getRows: () => createRows(state, options, now()),
|
|
3848
|
+
onDiagnostic: ({ diagnostic, progress }) => {
|
|
3849
|
+
updateProgress(state, progress);
|
|
3850
|
+
if (diagnostic.severity === "warn") state.warnCount += 1;
|
|
3851
|
+
else state.errorCount += 1;
|
|
3852
|
+
},
|
|
3853
|
+
onExecuteEnd: ({ progress }) => {
|
|
3854
|
+
updateProgress(state, progress);
|
|
3855
|
+
},
|
|
3856
|
+
onExecuteStart: ({ progress, startedAt }) => {
|
|
3857
|
+
updateProgress(state, progress);
|
|
3858
|
+
state.runStartedAt = startedAt ?? state.runStartedAt ?? now();
|
|
3859
|
+
},
|
|
3860
|
+
onFileReady: ({ progress }) => updateProgress(state, progress),
|
|
3861
|
+
onJobEnd: (payload) => {
|
|
3862
|
+
updateProgress(state, payload.progress);
|
|
3863
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
3864
|
+
const group = state.ruleGroups.get(payload.job.ruleId);
|
|
3865
|
+
if (!group) throw new Error(`Cannot finish unqueued progress job "${payload.job.id}".`);
|
|
3866
|
+
group[payload.state] += 1;
|
|
3867
|
+
if (payload.endedAt !== void 0 && current?.startedAt !== void 0 && payload.state !== "cached") group.terminalDurationMs += Math.max(payload.endedAt - current.startedAt, 0);
|
|
3868
|
+
if (payload.state === "cached") {
|
|
3869
|
+
const cachedTokens = state.jobTokens.get(payload.job.id) ?? 0;
|
|
3870
|
+
state.totalTokens = Math.max(state.totalTokens - cachedTokens, 0);
|
|
3871
|
+
state.cachedTokens += cachedTokens;
|
|
3872
|
+
}
|
|
3873
|
+
state.activeJobs.delete(payload.job.id);
|
|
3874
|
+
state.jobTokens.delete(payload.job.id);
|
|
3875
|
+
},
|
|
3876
|
+
onJobQueued: ({ job, progress }) => {
|
|
3877
|
+
updateProgress(state, progress);
|
|
3878
|
+
const group = state.ruleGroups.get(job.ruleId) ?? createRuleGroup(job);
|
|
3879
|
+
group.planned += 1;
|
|
3880
|
+
state.ruleGroups.set(job.ruleId, group);
|
|
3881
|
+
state.activeJobs.set(job.id, {
|
|
3882
|
+
job,
|
|
3883
|
+
state: "queued"
|
|
3884
|
+
});
|
|
3885
|
+
},
|
|
3886
|
+
onJobRetry: (payload) => {
|
|
3887
|
+
updateProgress(state, payload.progress);
|
|
3888
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
3889
|
+
state.activeJobs.set(payload.job.id, {
|
|
3890
|
+
job: payload.job,
|
|
3891
|
+
retry: {
|
|
3892
|
+
attempt: payload.attempt,
|
|
3893
|
+
maxAttempts: payload.maxAttempts,
|
|
3894
|
+
startedAt: payload.startedAt
|
|
3895
|
+
},
|
|
3896
|
+
startedAt: current?.startedAt ?? payload.startedAt ?? now(),
|
|
3897
|
+
state: current?.state ?? "running"
|
|
3898
|
+
});
|
|
3899
|
+
},
|
|
3900
|
+
onJobStart: (payload) => {
|
|
3901
|
+
updateProgress(state, payload.progress);
|
|
3902
|
+
state.activeJobs.set(payload.job.id, {
|
|
3903
|
+
job: payload.job,
|
|
3904
|
+
startedAt: payload.startedAt ?? now(),
|
|
3905
|
+
state: "running"
|
|
3906
|
+
});
|
|
3907
|
+
},
|
|
3908
|
+
onPlanningEnd: ({ progress }) => updateProgress(state, progress),
|
|
3909
|
+
onPrepareStart: ({ startedAt }) => {
|
|
3910
|
+
state.activeJobs.clear();
|
|
3911
|
+
state.errorCount = 0;
|
|
3912
|
+
state.execution = createCounts();
|
|
3913
|
+
state.progress = createProgress();
|
|
3914
|
+
state.ruleGroups.clear();
|
|
3915
|
+
state.runStartedAt = startedAt ?? now();
|
|
3916
|
+
state.animationTick = 0;
|
|
3917
|
+
state.cachedTokens = 0;
|
|
3918
|
+
state.jobTokens.clear();
|
|
3919
|
+
state.totalTokens = 0;
|
|
3920
|
+
state.warnCount = 0;
|
|
3921
|
+
},
|
|
3922
|
+
onRunEnd: (payload) => {
|
|
3923
|
+
state.warnCount = payload.diagnostics.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
3924
|
+
state.errorCount = payload.diagnostics.length - state.warnCount;
|
|
3925
|
+
updateProgress(state, payload.progress);
|
|
3926
|
+
state.totalTokens = payload.usage.totalTokens;
|
|
3927
|
+
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
3928
|
+
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
3929
|
+
},
|
|
3930
|
+
onUsage: ({ job, progress, record }) => {
|
|
3931
|
+
updateProgress(state, progress);
|
|
3932
|
+
if (record.totalTokens != null && Number.isFinite(record.totalTokens)) {
|
|
3933
|
+
state.jobTokens.set(job.id, (state.jobTokens.get(job.id) ?? 0) + record.totalTokens);
|
|
3934
|
+
state.totalTokens += record.totalTokens;
|
|
3935
|
+
}
|
|
3936
|
+
},
|
|
3937
|
+
/**
|
|
3938
|
+
* Advances the summary animation on each renderer interval.
|
|
3939
|
+
*
|
|
3940
|
+
* Triggering workflow:
|
|
3941
|
+
*
|
|
3942
|
+
* `createTtyProgressRenderer`
|
|
3943
|
+
* -> `createCliProgressReporter` `createInterval` callback
|
|
3944
|
+
* -> `SummaryProgressReporter.tick` (this handler)
|
|
3945
|
+
*
|
|
3946
|
+
* Upstream:
|
|
3947
|
+
* - `createCliProgressReporter` `createInterval` callback registered by `createTtyProgressRenderer`
|
|
3948
|
+
*
|
|
3949
|
+
* Downstream:
|
|
3950
|
+
* - Increments `state.animationTick`, consumed by {@link createRows}, {@link formatSpinnerFrame}, and {@link formatMiniBarSegment}.
|
|
3951
|
+
*/
|
|
3952
|
+
tick: () => {
|
|
3953
|
+
state.animationTick += 1;
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
function compareRuleGroups(left, right) {
|
|
3958
|
+
const leftActive = left.jobs.some((job) => job.state === "running") ? 1 : 0;
|
|
3959
|
+
const rightActive = right.jobs.some((job) => job.state === "running") ? 1 : 0;
|
|
3960
|
+
if (leftActive !== rightActive) return rightActive - leftActive;
|
|
3961
|
+
if (left.failed !== right.failed) return right.failed - left.failed;
|
|
3962
|
+
return left.firstJobIndex - right.firstJobIndex;
|
|
3293
3963
|
}
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
return Boolean(env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.BUILDKITE || env.TF_BUILD);
|
|
3964
|
+
function compareRunningJobs(left, right) {
|
|
3965
|
+
const leftStartedAt = left.startedAt ?? Number.POSITIVE_INFINITY;
|
|
3966
|
+
const rightStartedAt = right.startedAt ?? Number.POSITIVE_INFINITY;
|
|
3967
|
+
if (leftStartedAt !== rightStartedAt) return leftStartedAt - rightStartedAt;
|
|
3968
|
+
return left.job.index - right.job.index;
|
|
3300
3969
|
}
|
|
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);
|
|
3970
|
+
function completeGraphemeBoundary(input, maximumIndex) {
|
|
3971
|
+
let boundary = 0;
|
|
3972
|
+
for (const segment of graphemeSegmenter.segment(input)) {
|
|
3973
|
+
const end = segment.index + segment.segment.length;
|
|
3974
|
+
if (end > maximumIndex) break;
|
|
3975
|
+
boundary = end;
|
|
3334
3976
|
}
|
|
3335
|
-
|
|
3977
|
+
return boundary;
|
|
3336
3978
|
}
|
|
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;
|
|
3979
|
+
function createCounts(planned = 0) {
|
|
3358
3980
|
return {
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3981
|
+
cached: 0,
|
|
3982
|
+
cancelled: 0,
|
|
3983
|
+
completed: 0,
|
|
3984
|
+
failed: 0,
|
|
3985
|
+
planned,
|
|
3986
|
+
queued: 0,
|
|
3987
|
+
running: 0,
|
|
3988
|
+
skipped: 0
|
|
3362
3989
|
};
|
|
3363
3990
|
}
|
|
3364
|
-
function
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3991
|
+
function createInitialState() {
|
|
3992
|
+
return {
|
|
3993
|
+
activeJobs: /* @__PURE__ */ new Map(),
|
|
3994
|
+
animationTick: 0,
|
|
3995
|
+
cachedTokens: 0,
|
|
3996
|
+
errorCount: 0,
|
|
3997
|
+
execution: createCounts(),
|
|
3998
|
+
jobTokens: /* @__PURE__ */ new Map(),
|
|
3999
|
+
progress: createProgress(),
|
|
4000
|
+
ruleGroups: /* @__PURE__ */ new Map(),
|
|
4001
|
+
totalTokens: 0,
|
|
4002
|
+
warnCount: 0
|
|
3371
4003
|
};
|
|
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
4004
|
}
|
|
3431
|
-
function
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
4005
|
+
function createProgress() {
|
|
4006
|
+
return {
|
|
4007
|
+
execution: createCounts(),
|
|
4008
|
+
filesPlanned: 0,
|
|
4009
|
+
filesTotal: 0,
|
|
4010
|
+
jobsCompleted: 0,
|
|
4011
|
+
jobsStarted: 0,
|
|
4012
|
+
jobsTotal: 0,
|
|
4013
|
+
planningComplete: false
|
|
4014
|
+
};
|
|
3439
4015
|
}
|
|
3440
|
-
|
|
3441
|
-
if (
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
3444
|
-
|
|
4016
|
+
function createRows(state, options, now) {
|
|
4017
|
+
if (options.rows !== void 0 && options.rows <= 0) return [];
|
|
4018
|
+
const groups = createRuleGroups(state);
|
|
4019
|
+
const footerRows = options.rows === void 0 ? 3 : Math.min(options.rows, 3);
|
|
4020
|
+
const separatorRows = options.rows === void 0 || options.rows - footerRows > 1 ? 1 : 0;
|
|
4021
|
+
const visible = formatContentRows(groups, state, options, now, options.rows === void 0 ? Number.POSITIVE_INFINITY : Math.max(options.rows - footerRows - separatorRows, 0));
|
|
4022
|
+
const footer = formatFooters(state, options, now);
|
|
4023
|
+
const actualSeparatorRows = visible.length > 0 ? separatorRows : 0;
|
|
4024
|
+
const visibleFooters = options.rows === void 0 ? footer : footer.slice(0, Math.max(options.rows - visible.length - actualSeparatorRows, 0));
|
|
4025
|
+
if (actualSeparatorRows === 1) visible.push("");
|
|
4026
|
+
visible.push(...visibleFooters);
|
|
4027
|
+
return options.color ? visible.map((row) => styleRow(row, state, state.warnCount, state.errorCount, options)) : visible;
|
|
3445
4028
|
}
|
|
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
|
-
}
|
|
4029
|
+
function createRuleGroup(job) {
|
|
3475
4030
|
return {
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
4031
|
+
cached: 0,
|
|
4032
|
+
cancelled: 0,
|
|
4033
|
+
completed: 0,
|
|
4034
|
+
failed: 0,
|
|
4035
|
+
firstJobIndex: job.index,
|
|
4036
|
+
planned: 0,
|
|
4037
|
+
ruleId: job.ruleId,
|
|
4038
|
+
skipped: 0,
|
|
4039
|
+
terminalDurationMs: 0
|
|
3482
4040
|
};
|
|
3483
4041
|
}
|
|
3484
|
-
|
|
3485
|
-
const
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
const
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
4042
|
+
function createRuleGroups(state) {
|
|
4043
|
+
const groups = new Map([...state.ruleGroups].map(([ruleId, group]) => [ruleId, {
|
|
4044
|
+
...group,
|
|
4045
|
+
jobs: []
|
|
4046
|
+
}]));
|
|
4047
|
+
for (const job of state.activeJobs.values()) groups.get(job.job.ruleId)?.jobs.push(job);
|
|
4048
|
+
return [...groups.values()].sort(compareRuleGroups);
|
|
4049
|
+
}
|
|
4050
|
+
function estimateEta(durationMs, terminal, planned) {
|
|
4051
|
+
if (terminal <= 0 || planned <= 0 || durationMs <= 0) return "eta ?";
|
|
4052
|
+
return `eta ~${formatDuration$1(durationMs * (planned - terminal) / terminal)}`;
|
|
4053
|
+
}
|
|
4054
|
+
function fitRow(row, columns) {
|
|
4055
|
+
if (columns <= 0) return "";
|
|
4056
|
+
if (fastStringWidth(row) <= columns) return row;
|
|
4057
|
+
let limit = columns;
|
|
4058
|
+
while (limit > 0) {
|
|
4059
|
+
const result = fastStringTruncatedWidth(row, {
|
|
4060
|
+
ellipsis: "…",
|
|
4061
|
+
limit
|
|
3496
4062
|
});
|
|
4063
|
+
const index = completeGraphemeBoundary(row, result.index);
|
|
4064
|
+
const visible = row.slice(0, index);
|
|
4065
|
+
const fitted = `${visible}${visible.includes("\x1B") ? "\x1B[0m" : ""}${result.ellipsed ? "…" : ""}`;
|
|
4066
|
+
const overflow = fastStringWidth(fitted) - columns;
|
|
4067
|
+
if (overflow <= 0) return fitted;
|
|
4068
|
+
limit -= overflow;
|
|
3497
4069
|
}
|
|
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
|
-
};
|
|
4070
|
+
return "";
|
|
3532
4071
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
4072
|
+
function formatCollapsedRunningRow(hidden, options) {
|
|
4073
|
+
return fitRow(` └─ ${hidden} more running`, options.columns);
|
|
4074
|
+
}
|
|
4075
|
+
function formatContentRows(groups, state, options, now, contentBudget) {
|
|
4076
|
+
const rows = [];
|
|
4077
|
+
const renderableGroups = groups.filter((group) => group.jobs.some((job) => job.state === "running") || group.failed > 0);
|
|
4078
|
+
const runningCount = renderableGroups.reduce((sum, group) => sum + group.jobs.filter((job) => job.state === "running").length, 0);
|
|
4079
|
+
if (contentBudget === 1) {
|
|
4080
|
+
const failedGroup = renderableGroups.find((group) => group.failed > 0);
|
|
4081
|
+
if (failedGroup) return [formatRuleRow(failedGroup, state, options)];
|
|
4082
|
+
if (runningCount > 0) return [formatCollapsedRunningRow(runningCount, options)];
|
|
4083
|
+
}
|
|
4084
|
+
for (const [groupIndex, group] of renderableGroups.entries()) {
|
|
4085
|
+
if (rows.length >= contentBudget) break;
|
|
4086
|
+
const groupStart = rows.length;
|
|
4087
|
+
rows.push(formatRuleRow(group, state, options));
|
|
4088
|
+
const runningJobs = group.jobs.filter((job) => job.state === "running").sort(compareRunningJobs);
|
|
4089
|
+
const remainingGroups = renderableGroups.length - groupIndex - 1;
|
|
4090
|
+
for (const [index, job] of runningJobs.entries()) {
|
|
4091
|
+
const rowsReservedForLaterGroups = Math.min(remainingGroups, Math.max(contentBudget - rows.length, 0));
|
|
4092
|
+
if (rows.length >= contentBudget) {
|
|
4093
|
+
const hidden = runningJobs.length - index;
|
|
4094
|
+
if (rows.length > groupStart + 1 && hidden > 0) rows[rows.length - 1] = formatCollapsedRunningRow(hidden, options);
|
|
4095
|
+
break;
|
|
4096
|
+
}
|
|
4097
|
+
if (rows.length + 1 + rowsReservedForLaterGroups >= contentBudget) {
|
|
4098
|
+
if (runningJobs.length > 1) rows.push(formatCollapsedRunningRow(runningJobs.length - index, options));
|
|
4099
|
+
break;
|
|
4100
|
+
}
|
|
4101
|
+
rows.push(formatTargetRow(job, options, now, index === runningJobs.length - 1));
|
|
3542
4102
|
}
|
|
3543
4103
|
}
|
|
3544
|
-
return
|
|
4104
|
+
return rows;
|
|
3545
4105
|
}
|
|
3546
|
-
|
|
3547
|
-
const
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
await prune(dir, ts, retentionMonths);
|
|
4106
|
+
function formatDuration$1(ms) {
|
|
4107
|
+
const durationMs = Math.max(ms, 0);
|
|
4108
|
+
if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)}s`;
|
|
4109
|
+
return formatDuration(intervalToDuration({
|
|
4110
|
+
end: durationMs,
|
|
4111
|
+
start: 0
|
|
4112
|
+
}));
|
|
3554
4113
|
}
|
|
3555
|
-
function
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
4114
|
+
function formatFooters(state, options, now) {
|
|
4115
|
+
const terminal = terminalCount(state.execution);
|
|
4116
|
+
const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
|
|
4117
|
+
const progressBar = state.progress.planningComplete ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
|
|
4118
|
+
const eta = state.progress.planningComplete ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
|
|
4119
|
+
const projectedTokens = state.progress.planningComplete && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
4120
|
+
const discovering = state.progress.planningComplete ? "" : " jobs (discovering)";
|
|
4121
|
+
return [
|
|
4122
|
+
fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration$1(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
|
|
4123
|
+
fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
|
|
4124
|
+
fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
|
|
4125
|
+
];
|
|
3559
4126
|
}
|
|
3560
|
-
function
|
|
3561
|
-
|
|
4127
|
+
function formatMiniBarSegment(completed, planned, tick, options) {
|
|
4128
|
+
if (options.columns < 60) return "";
|
|
4129
|
+
return ` ${formatMiniBar({
|
|
4130
|
+
completed,
|
|
4131
|
+
planned,
|
|
4132
|
+
tick,
|
|
4133
|
+
width: 10
|
|
4134
|
+
})}`;
|
|
3562
4135
|
}
|
|
3563
|
-
function
|
|
3564
|
-
|
|
4136
|
+
function formatRuleRow(group, state, options) {
|
|
4137
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
4138
|
+
const terminal = group.completed + group.cached + group.failed + group.cancelled + group.skipped;
|
|
4139
|
+
const percent = group.planned > 0 ? Math.floor(terminal / group.planned * 100) : 0;
|
|
4140
|
+
const bar = formatMiniBarSegment(terminal, group.planned, state.animationTick, options);
|
|
4141
|
+
const running = group.jobs.filter((job) => job.state === "running").length;
|
|
4142
|
+
const failed = group.failed > 0 ? ` ${group.failed} failed` : "";
|
|
4143
|
+
const eta = estimateEta(group.terminalDurationMs, terminal, group.planned);
|
|
4144
|
+
return fitRow(`${spinner} ${group.ruleId} ${terminal}/${group.planned} ${percent}%${bar} ${eta} ${running} running${failed}`, options.columns);
|
|
3565
4145
|
}
|
|
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
|
-
};
|
|
4146
|
+
function formatSpinnerFrame(animationTick, spinnerFrames) {
|
|
4147
|
+
return spinnerFrames[animationTick % Math.max(spinnerFrames.length, 1)] ?? "";
|
|
3582
4148
|
}
|
|
3583
|
-
function
|
|
3584
|
-
const
|
|
3585
|
-
|
|
4149
|
+
function formatTargetRow(jobState, options, now, last) {
|
|
4150
|
+
const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
|
|
4151
|
+
const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
|
|
4152
|
+
const startedAt = jobState.retry?.startedAt ?? jobState.startedAt ?? now;
|
|
4153
|
+
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
4154
|
}
|
|
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);
|
|
4155
|
+
function styleRow(row, state, warnCount, errorCount, options) {
|
|
4156
|
+
let styledRow = row;
|
|
4157
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
4158
|
+
if (spinner) styledRow = styledRow.replace(spinner, colors$2.cyan(spinner));
|
|
4159
|
+
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
4160
|
}
|
|
3603
|
-
function
|
|
3604
|
-
|
|
3605
|
-
for (const item of items) total += select(item);
|
|
3606
|
-
return total;
|
|
4161
|
+
function terminalCount(counts) {
|
|
4162
|
+
return counts.completed + counts.failed + counts.cached + counts.skipped + counts.cancelled;
|
|
3607
4163
|
}
|
|
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
|
-
}
|
|
4164
|
+
function updateProgress(state, progress) {
|
|
4165
|
+
state.execution = { ...progress.execution };
|
|
4166
|
+
state.progress = {
|
|
4167
|
+
...progress,
|
|
4168
|
+
execution: { ...progress.execution }
|
|
3621
4169
|
};
|
|
3622
4170
|
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
4171
|
+
//#endregion
|
|
4172
|
+
//#region src/cli/reporters/progress/tty.ts
|
|
4173
|
+
const clearCurrentLine = "\r\x1B[K";
|
|
4174
|
+
const clearPreviousLine = "\r\x1B[1A\x1B[K";
|
|
4175
|
+
function createTtyProgressRenderer(options) {
|
|
4176
|
+
let interval;
|
|
4177
|
+
let previousRows = 0;
|
|
4178
|
+
const clearPreviousFrame = () => {
|
|
4179
|
+
if (previousRows === 0) return;
|
|
4180
|
+
let sequence = clearCurrentLine;
|
|
4181
|
+
for (let row = 1; row < previousRows; row += 1) sequence += clearPreviousLine;
|
|
4182
|
+
options.write(sequence);
|
|
4183
|
+
previousRows = 0;
|
|
4184
|
+
};
|
|
4185
|
+
const render = () => {
|
|
4186
|
+
clearPreviousFrame();
|
|
4187
|
+
const rows = options.getRows();
|
|
4188
|
+
if (rows.length === 0) return;
|
|
4189
|
+
options.write(rows.join("\n"));
|
|
4190
|
+
previousRows = rows.length;
|
|
3638
4191
|
};
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
cached: 0,
|
|
3645
|
-
cancelled: 0,
|
|
3646
|
-
completed: 0,
|
|
3647
|
-
failed: 0,
|
|
3648
|
-
planned: 0
|
|
4192
|
+
const write = (chunk) => {
|
|
4193
|
+
const wasRendering = interval !== void 0;
|
|
4194
|
+
clearPreviousFrame();
|
|
4195
|
+
options.write(chunk);
|
|
4196
|
+
if (wasRendering) render();
|
|
3649
4197
|
};
|
|
3650
|
-
let startedAt;
|
|
3651
|
-
let endedAt;
|
|
3652
4198
|
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;
|
|
4199
|
+
finish: () => {
|
|
4200
|
+
if (interval) {
|
|
4201
|
+
options.clearInterval(interval);
|
|
4202
|
+
interval = void 0;
|
|
3666
4203
|
}
|
|
3667
|
-
|
|
4204
|
+
clearPreviousFrame();
|
|
4205
|
+
},
|
|
4206
|
+
render,
|
|
4207
|
+
start: () => {
|
|
4208
|
+
if (!interval) {
|
|
4209
|
+
interval = options.createInterval(render, options.intervalMs);
|
|
4210
|
+
if (isUnrefableInterval(interval)) interval.unref();
|
|
4211
|
+
}
|
|
4212
|
+
render();
|
|
4213
|
+
},
|
|
4214
|
+
write
|
|
3668
4215
|
};
|
|
3669
4216
|
}
|
|
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
|
-
}
|
|
4217
|
+
function isUnrefableInterval(interval) {
|
|
4218
|
+
if (typeof interval !== "object" || interval === null || !("unref" in interval)) return false;
|
|
4219
|
+
return typeof interval.unref === "function";
|
|
4220
|
+
}
|
|
4221
|
+
//#endregion
|
|
4222
|
+
//#region src/cli/reporters/progress/index.ts
|
|
4223
|
+
function createCliProgressReporter(options) {
|
|
4224
|
+
if (!options.isTty) return {
|
|
4225
|
+
dispose: () => {},
|
|
4226
|
+
reporter: createPlainProgressReporter({ write: options.write }),
|
|
4227
|
+
write: options.write
|
|
3690
4228
|
};
|
|
3691
|
-
const
|
|
3692
|
-
|
|
3693
|
-
|
|
4229
|
+
const summary = createSummaryProgressReporter({
|
|
4230
|
+
color: options.color,
|
|
4231
|
+
columns: options.columns,
|
|
4232
|
+
cwd: options.cwd,
|
|
4233
|
+
rows: options.rows,
|
|
4234
|
+
spinnerFrames: cliSpinners.dots.frames
|
|
4235
|
+
});
|
|
4236
|
+
const renderer = createTtyProgressRenderer({
|
|
4237
|
+
clearInterval: (handle) => globalThis.clearInterval(handle),
|
|
4238
|
+
createInterval: (callback, intervalMs) => globalThis.setInterval(() => {
|
|
4239
|
+
summary.tick();
|
|
4240
|
+
callback();
|
|
4241
|
+
}, intervalMs),
|
|
4242
|
+
getRows: summary.getRows,
|
|
4243
|
+
intervalMs: 120,
|
|
4244
|
+
write: options.write
|
|
4245
|
+
});
|
|
4246
|
+
const reporter = createRenderingProgressReporter(summary, renderer);
|
|
4247
|
+
return {
|
|
4248
|
+
dispose: renderer.finish,
|
|
4249
|
+
reporter,
|
|
4250
|
+
write: renderer.write
|
|
3694
4251
|
};
|
|
4252
|
+
}
|
|
4253
|
+
function createRenderingProgressReporter(summary, renderer) {
|
|
3695
4254
|
return {
|
|
3696
4255
|
onDiagnostic: (payload) => {
|
|
3697
|
-
|
|
4256
|
+
summary.onDiagnostic?.(payload);
|
|
4257
|
+
renderer.render();
|
|
3698
4258
|
},
|
|
3699
4259
|
onExecuteEnd: (payload) => {
|
|
3700
|
-
|
|
4260
|
+
summary.onExecuteEnd?.(payload);
|
|
4261
|
+
renderer.render();
|
|
3701
4262
|
},
|
|
3702
4263
|
onExecuteStart: (payload) => {
|
|
3703
|
-
|
|
4264
|
+
summary.onExecuteStart?.(payload);
|
|
4265
|
+
renderer.render();
|
|
3704
4266
|
},
|
|
3705
4267
|
onFileReady: (payload) => {
|
|
3706
|
-
|
|
4268
|
+
summary.onFileReady?.(payload);
|
|
4269
|
+
renderer.render();
|
|
3707
4270
|
},
|
|
3708
4271
|
onJobEnd: (payload) => {
|
|
3709
|
-
|
|
4272
|
+
summary.onJobEnd?.(payload);
|
|
4273
|
+
renderer.render();
|
|
3710
4274
|
},
|
|
3711
4275
|
onJobQueued: (payload) => {
|
|
3712
|
-
|
|
4276
|
+
summary.onJobQueued?.(payload);
|
|
4277
|
+
renderer.render();
|
|
3713
4278
|
},
|
|
3714
4279
|
onJobRetry: (payload) => {
|
|
3715
|
-
|
|
4280
|
+
summary.onJobRetry?.(payload);
|
|
4281
|
+
renderer.render();
|
|
3716
4282
|
},
|
|
3717
4283
|
onJobStart: (payload) => {
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
onPlanningEnd: (payload) => {
|
|
3721
|
-
deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
|
|
4284
|
+
summary.onJobStart?.(payload);
|
|
4285
|
+
renderer.render();
|
|
3722
4286
|
},
|
|
3723
4287
|
onPrepareEnd: (payload) => {
|
|
3724
|
-
|
|
4288
|
+
summary.onPrepareEnd?.(payload);
|
|
4289
|
+
renderer.render();
|
|
3725
4290
|
},
|
|
3726
4291
|
onPrepareStart: (payload) => {
|
|
3727
|
-
|
|
4292
|
+
summary.onPrepareStart?.(payload);
|
|
4293
|
+
renderer.start();
|
|
3728
4294
|
},
|
|
3729
4295
|
onRunEnd: (payload) => {
|
|
3730
|
-
|
|
3731
|
-
|
|
4296
|
+
summary.onRunEnd?.(payload);
|
|
4297
|
+
renderer.render();
|
|
3732
4298
|
},
|
|
3733
4299
|
onUsage: (payload) => {
|
|
3734
|
-
|
|
4300
|
+
summary.onUsage?.(payload);
|
|
4301
|
+
renderer.render();
|
|
3735
4302
|
}
|
|
3736
4303
|
};
|
|
3737
4304
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
return {
|
|
3743
|
-
dir: config?.location ?? getStatsDir(env),
|
|
3744
|
-
retentionMonths: config?.retentionMonths
|
|
3745
|
-
};
|
|
4305
|
+
//#endregion
|
|
4306
|
+
//#region src/cli/commands/lint/errors.ts
|
|
4307
|
+
const colors$1 = createColors({ force: true });
|
|
4308
|
+
function formatCancelledError(error, color) {
|
|
4309
|
+
return `${color ? colors$1.red("error") : "error"} ${error.message}\n`;
|
|
3746
4310
|
}
|
|
3747
|
-
|
|
3748
|
-
const
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
} catch {}
|
|
4311
|
+
function formatRunError(error, color) {
|
|
4312
|
+
const label = color ? colors$1.red("error") : "error";
|
|
4313
|
+
const fileFailures = error.failures.filter((failure) => "file" in failure);
|
|
4314
|
+
const groups = groupFailuresByRule(error.failures.filter((failure) => "job" in failure));
|
|
4315
|
+
return [
|
|
4316
|
+
`${label} ${error.message}`,
|
|
4317
|
+
"",
|
|
4318
|
+
...fileFailures.length > 0 ? [
|
|
4319
|
+
`Failed Files ${fileFailures.length}`,
|
|
4320
|
+
"",
|
|
4321
|
+
...formatFileFailures(fileFailures, color)
|
|
4322
|
+
] : [],
|
|
4323
|
+
...groups.length > 0 ? [
|
|
4324
|
+
`Failed Rules ${groups.length}`,
|
|
4325
|
+
"",
|
|
4326
|
+
...formatFailureGroups(groups, color)
|
|
4327
|
+
] : []
|
|
4328
|
+
].join("\n");
|
|
3766
4329
|
}
|
|
3767
|
-
function
|
|
3768
|
-
|
|
3769
|
-
|
|
4330
|
+
function formatFailureGroups(groups, color) {
|
|
4331
|
+
const lines = [];
|
|
4332
|
+
for (const [ruleId, group] of groups) {
|
|
4333
|
+
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
4334
|
+
const targetLabel = group.length === 1 ? "target" : "targets";
|
|
4335
|
+
lines.push(`${failLabel} ${ruleId} ${group.length} ${targetLabel}`);
|
|
4336
|
+
for (const failure of group) {
|
|
4337
|
+
lines.push(` ${formatTarget(failure)}`);
|
|
4338
|
+
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
4339
|
+
for (const cause of failure.causes) {
|
|
4340
|
+
const code = cause.code === void 0 ? "" : `[${cause.code}] `;
|
|
4341
|
+
lines.push(` Caused by: ${code}${cause.message}`);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
lines.push("");
|
|
4345
|
+
}
|
|
4346
|
+
return lines;
|
|
4347
|
+
}
|
|
4348
|
+
function formatFileFailures(failures, color) {
|
|
4349
|
+
const lines = [];
|
|
4350
|
+
for (const failure of failures) {
|
|
4351
|
+
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
4352
|
+
lines.push(`${failLabel} ${failure.file.path}`);
|
|
4353
|
+
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
4354
|
+
lines.push("");
|
|
4355
|
+
}
|
|
4356
|
+
return lines;
|
|
4357
|
+
}
|
|
4358
|
+
function formatTarget(failure) {
|
|
4359
|
+
const target = failure.job.target.name ? `${failure.job.target.kind} ${failure.job.target.name}` : failure.job.target.kind;
|
|
4360
|
+
return `${failure.job.inputPath} > ${target}`;
|
|
4361
|
+
}
|
|
4362
|
+
function groupFailuresByRule(failures) {
|
|
4363
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4364
|
+
for (const failure of failures) {
|
|
4365
|
+
const group = groups.get(failure.job.ruleId) ?? [];
|
|
4366
|
+
group.push(failure);
|
|
4367
|
+
groups.set(failure.job.ruleId, group);
|
|
4368
|
+
}
|
|
4369
|
+
return [...groups];
|
|
3770
4370
|
}
|
|
3771
4371
|
//#endregion
|
|
3772
4372
|
//#region src/cli/commands/lint/index.ts
|
|
@@ -3779,78 +4379,101 @@ const lint = defineCommand({
|
|
|
3779
4379
|
arguments: "[...files]",
|
|
3780
4380
|
default: true,
|
|
3781
4381
|
description: "Run alint",
|
|
3782
|
-
name: "lint"
|
|
4382
|
+
name: "lint",
|
|
4383
|
+
options: [{
|
|
4384
|
+
description: "Lint only staged, unstaged, and untracked files",
|
|
4385
|
+
flags: "--dirty"
|
|
4386
|
+
}]
|
|
3783
4387
|
});
|
|
3784
4388
|
async function assertConfigExists(cwd, configPath) {
|
|
3785
4389
|
const resolvedConfigPath = resolve(cwd, configPath);
|
|
3786
4390
|
try {
|
|
3787
4391
|
if (!(await stat(resolvedConfigPath)).isFile()) throw new Error(`Config file "${configPath}" is not a file.`);
|
|
3788
4392
|
} catch (error) {
|
|
3789
|
-
if (
|
|
4393
|
+
if (isNodeErrorCode(error, "ENOENT")) throw new Error(`Config file "${configPath}" does not exist.`);
|
|
3790
4394
|
throw error;
|
|
3791
4395
|
}
|
|
3792
4396
|
}
|
|
3793
|
-
function isNodeError(error) {
|
|
3794
|
-
return error instanceof Error && "code" in error;
|
|
3795
|
-
}
|
|
3796
4397
|
async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
3797
|
-
if (options.
|
|
3798
|
-
|
|
4398
|
+
if (options.dirty && files.length > 0) {
|
|
4399
|
+
io.stderr.write("The --dirty option does not accept file arguments.\n");
|
|
4400
|
+
return 2;
|
|
4401
|
+
}
|
|
4402
|
+
const cwd = options.dirty ? await findGitRoot(io.cwd) : io.cwd;
|
|
4403
|
+
const runIo = cwd === io.cwd ? io : {
|
|
4404
|
+
...io,
|
|
4405
|
+
cwd
|
|
4406
|
+
};
|
|
4407
|
+
if (options.config) await assertConfigExists(cwd, options.config);
|
|
4408
|
+
let changedLines;
|
|
4409
|
+
let config;
|
|
4410
|
+
let targets;
|
|
4411
|
+
if (options.dirty) {
|
|
4412
|
+
config = await loadAlintConfig(cwd, options.config);
|
|
4413
|
+
const dirtyTargets = await findDirtyLintTargets(config, cwd);
|
|
4414
|
+
changedLines = dirtyTargets.changedLines;
|
|
4415
|
+
targets = dirtyTargets;
|
|
4416
|
+
if (targets.files.length === 0) return 0;
|
|
4417
|
+
}
|
|
4418
|
+
const session = await createRunSession(runIo, config === void 0 ? { configPath: options.config } : { config });
|
|
3799
4419
|
try {
|
|
3800
|
-
const
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
write: (chunk) => io.stderr.write(chunk)
|
|
4420
|
+
const progress = shouldEnableProgress(options, runIo) ? createCliProgressReporter({
|
|
4421
|
+
color: runIo.stderr.isTTY === true,
|
|
4422
|
+
columns: runIo.stderr.columns ?? 80,
|
|
4423
|
+
cwd,
|
|
4424
|
+
isTty: runIo.stderr.isTTY === true,
|
|
4425
|
+
rows: runIo.stderr.rows,
|
|
4426
|
+
write: (chunk) => runIo.stderr.write(chunk)
|
|
3808
4427
|
}) : void 0;
|
|
3809
4428
|
const restoreProgressConsole = progress ? interceptConsoleOutput({ write: progress.write }) : void 0;
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
};
|
|
4429
|
+
const visibleResult = (runResult) => changedLines === void 0 ? runResult : filterResultToChangedLines(runResult, {
|
|
4430
|
+
changedLines,
|
|
4431
|
+
cwd
|
|
4432
|
+
});
|
|
3815
4433
|
const writeResult = (runResult) => {
|
|
3816
|
-
|
|
4434
|
+
runIo.stdout.write(formatDiagnostics(options.format, visibleResult(runResult), { color: runIo.stdout.isTTY === true }));
|
|
3817
4435
|
};
|
|
4436
|
+
const targetSelection = targets === void 0 ? { inputs: files } : { targets };
|
|
3818
4437
|
let result;
|
|
3819
4438
|
try {
|
|
3820
|
-
result = await
|
|
4439
|
+
result = await executeLint({
|
|
4440
|
+
...targetSelection,
|
|
3821
4441
|
cacheOnly: options.cacheOnly,
|
|
3822
|
-
|
|
4442
|
+
io: runIo,
|
|
3823
4443
|
modelOverride: options.model,
|
|
3824
4444
|
outputLanguage: options.outputLanguage,
|
|
3825
|
-
progress:
|
|
3826
|
-
|
|
4445
|
+
progress: progress?.reporter,
|
|
4446
|
+
runnerOptions: options,
|
|
4447
|
+
session
|
|
3827
4448
|
});
|
|
3828
4449
|
} catch (error) {
|
|
3829
|
-
restoreProgressConsole?.();
|
|
3830
|
-
progress?.dispose();
|
|
3831
4450
|
if (error instanceof NoFilesFoundError) {
|
|
3832
|
-
|
|
4451
|
+
runIo.stderr.write(`${error.message}\n`);
|
|
3833
4452
|
return 2;
|
|
3834
4453
|
}
|
|
4454
|
+
if (error instanceof AlintCachePersistenceError) {
|
|
4455
|
+
writeResult(error.result);
|
|
4456
|
+
const message = errorMessageFrom(error.cause) ?? error.message;
|
|
4457
|
+
runIo.stderr.write(`Cache persistence failed: ${message}\n`);
|
|
4458
|
+
return 1;
|
|
4459
|
+
}
|
|
3835
4460
|
if (error instanceof AlintRunError) {
|
|
3836
|
-
await persistStats(error.result);
|
|
3837
4461
|
writeResult(error.result);
|
|
3838
|
-
|
|
4462
|
+
runIo.stderr.write(formatRunError(error, runIo.stderr.isTTY === true));
|
|
3839
4463
|
return 2;
|
|
3840
4464
|
}
|
|
3841
4465
|
if (error instanceof AlintRunCancelledError) {
|
|
3842
|
-
await persistStats(error.result);
|
|
3843
4466
|
writeResult(error.result);
|
|
3844
|
-
|
|
4467
|
+
runIo.stderr.write(formatCancelledError(error, runIo.stderr.isTTY === true));
|
|
3845
4468
|
return 2;
|
|
3846
4469
|
}
|
|
3847
4470
|
throw error;
|
|
4471
|
+
} finally {
|
|
4472
|
+
restoreProgressConsole?.();
|
|
4473
|
+
progress?.dispose();
|
|
3848
4474
|
}
|
|
3849
|
-
restoreProgressConsole?.();
|
|
3850
|
-
progress?.dispose();
|
|
3851
|
-
await persistStats(result);
|
|
3852
4475
|
writeResult(result);
|
|
3853
|
-
return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
4476
|
+
return visibleResult(result).diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
3854
4477
|
} finally {
|
|
3855
4478
|
await session.shutdown();
|
|
3856
4479
|
}
|
|
@@ -3862,7 +4485,7 @@ function shouldEnableProgress(options, io) {
|
|
|
3862
4485
|
//#endregion
|
|
3863
4486
|
//#region src/cli/commands/lsp/index.ts
|
|
3864
4487
|
const lsp = defineCommand({
|
|
3865
|
-
action: async (context) => (await import("./server-
|
|
4488
|
+
action: async (context) => (await import("./server-xx0w7UQL.mjs")).startLspServer(context.io),
|
|
3866
4489
|
description: "Run alint as a language server over stdio",
|
|
3867
4490
|
examples: [["# Point an editor at the project's own alint", "alint lsp"].join("\n")],
|
|
3868
4491
|
help: ["Serve alint diagnostics over the Language Server Protocol on stdin/stdout.", "The server is cache-first: it publishes findings that are already cached and never calls a model on its own. Runs that spend tokens happen only through the `alint.runFile` and `alint.runWorkspace` commands."].join("\n\n"),
|
|
@@ -3901,14 +4524,14 @@ async function inspectOutputFile(io, file, options) {
|
|
|
3901
4524
|
try {
|
|
3902
4525
|
text = await readFile(filePath, "utf8");
|
|
3903
4526
|
} catch (error) {
|
|
3904
|
-
io.stderr.write(`Could not read output file "${file}": ${errorMessageFrom
|
|
4527
|
+
io.stderr.write(`Could not read output file "${file}": ${errorMessageFrom(error)}\n`);
|
|
3905
4528
|
return 2;
|
|
3906
4529
|
}
|
|
3907
4530
|
let parsed;
|
|
3908
4531
|
try {
|
|
3909
4532
|
parsed = JSON.parse(text);
|
|
3910
4533
|
} catch (error) {
|
|
3911
|
-
io.stderr.write(`Could not parse output file "${file}": ${errorMessageFrom
|
|
4534
|
+
io.stderr.write(`Could not parse output file "${file}": ${errorMessageFrom(error)}\n`);
|
|
3912
4535
|
return 2;
|
|
3913
4536
|
}
|
|
3914
4537
|
if (!isRunResult(parsed)) {
|
|
@@ -3918,7 +4541,7 @@ async function inspectOutputFile(io, file, options) {
|
|
|
3918
4541
|
try {
|
|
3919
4542
|
io.stdout.write(formatDiagnostics(options.format, parsed, { color: io.stdout.isTTY === true }));
|
|
3920
4543
|
} catch (error) {
|
|
3921
|
-
io.stderr.write(`${errorMessageFrom
|
|
4544
|
+
io.stderr.write(`${errorMessageFrom(error)}\n`);
|
|
3922
4545
|
return 2;
|
|
3923
4546
|
}
|
|
3924
4547
|
return parsed.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
@@ -4443,7 +5066,7 @@ async function runStatsCommand(options, io) {
|
|
|
4443
5066
|
}));
|
|
4444
5067
|
return 0;
|
|
4445
5068
|
} catch (error) {
|
|
4446
|
-
io.stderr.write(`${errorMessageFrom
|
|
5069
|
+
io.stderr.write(`${errorMessageFrom(error) ?? "Failed to read stats."}\n`);
|
|
4447
5070
|
return 2;
|
|
4448
5071
|
}
|
|
4449
5072
|
}
|
|
@@ -4479,6 +5102,7 @@ function statsLocation(stats) {
|
|
|
4479
5102
|
const commandTree = [
|
|
4480
5103
|
setup,
|
|
4481
5104
|
config,
|
|
5105
|
+
integrations,
|
|
4482
5106
|
plugin,
|
|
4483
5107
|
output,
|
|
4484
5108
|
defineCommand({
|
|
@@ -4619,4 +5243,4 @@ function shouldCaptureHelp(argv) {
|
|
|
4619
5243
|
return argv.includes("--help") || argv.includes("-h");
|
|
4620
5244
|
}
|
|
4621
5245
|
//#endregion
|
|
4622
|
-
export {
|
|
5246
|
+
export { createRunSession as a, formatJson as i, formatDiagnostics as n, formatStylish as r, executeCli as t };
|