@alint-js/cli 0.4.0 → 0.6.0

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