@alint-js/cli 0.5.0 → 0.7.0

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