@alint-js/cli 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,6 +184,8 @@ alint config inspect src/index.ts
184
184
  alint config providers list
185
185
  alint config providers show openrouter
186
186
  alint config models list
187
+ alint config models list --with-speed
188
+ alint config models list --with-speed --with-speed-concurrency openrouter=10 --with-speed-concurrency ollama=1
187
189
  alint config models show ollama/qwen
188
190
  alint config models probe --endpoint http://localhost:11434/v1
189
191
  alint config models rm qwen --provider ollama
@@ -194,6 +196,8 @@ When a model ID exists under multiple providers, qualify it as `<provider>/<mode
194
196
 
195
197
  `models rm` removes one exact configured model. `models prune` probes provider model endpoints and destructively removes configured IDs that are no longer reported. Interactive prune asks for confirmation; scripts must pass `-N --yes`.
196
198
 
199
+ `models list --with-speed` sends live streamed requests to every configured model. It reports median repeat and non-repeat latency, non-repeat output throughput, and successful attempts. Each model receives one repeat warm-up, three measured repeat requests, and three non-repeat requests. Model jobs run concurrently within independent provider limits: OpenRouter defaults to 20, while CLIProxyAPI and other providers default to 2. Repeat `--with-speed-concurrency <provider-id>=<limit>` to override one or more configured providers. Requests within one model remain serial so its repeat and non-repeat cache measurements do not overlap. An unavailable model is shown as `errored` without preventing the remaining models from running. On a TTY, spinners and a refreshable table show active models, phases, and samples. Each request uses the configured runner timeout, or 60 seconds when none is configured. This command consumes provider tokens and may incur charges.
200
+
197
201
  Save machine-readable output and inspect it later without rerunning model calls:
198
202
 
199
203
  ```bash
@@ -339,7 +343,7 @@ Reference the directory through the same static `plugins` table:
339
343
  ```toml
340
344
  [[config.group]]
341
345
  files = [ "src/**/*.py" ]
342
- language = "text/plain"
346
+ language = "plaintext"
343
347
 
344
348
  [config.group.plugins]
345
349
  arch = "./rules/architecture"
@@ -364,7 +368,7 @@ Rule severities follow the familiar lint convention:
364
368
  - `"warn"` or `1` reports a warning.
365
369
  - `"error"` or `2` reports an error.
366
370
 
367
- Flat configs can analyze non-JavaScript files by selecting `text/plain`:
371
+ Flat configs can analyze non-JavaScript files by selecting `plaintext`. Language ids follow [VS Code's language identifiers](https://code.visualstudio.com/docs/languages/identifiers), so a plugin that registers a language should use the same id:
368
372
 
369
373
  ```ts
370
374
  import docsPlugin from '@your-alint-config/docs-rules'
@@ -374,7 +378,7 @@ import { defineConfig } from '@alint-js/cli'
374
378
  export default defineConfig([
375
379
  {
376
380
  files: ['docs/**/*.md', '**/*.txt'],
377
- language: 'text/plain',
381
+ language: 'plaintext',
378
382
  plugins: {
379
383
  docs: docsPlugin,
380
384
  },
@@ -522,7 +526,7 @@ defineRule({
522
526
  | [`@alint-js/agent-pi`](https://github.com/moeru-ai/alint/tree/main/packages/agent-pi) | Pi-backed `AgentAdapter`. |
523
527
  | [`@alint-js/plugin-example`](https://github.com/moeru-ai/alint/tree/main/packages/plugin-example) | Example TypeScript/JavaScript model-backed rules. |
524
528
  | [`@alint-js/plugin-example-agent`](https://github.com/moeru-ai/alint/tree/main/packages/plugin-example-agent) | Example plugin for framework-agnostic agentic rules. |
525
- | [`@alint-js/plugin-example-go`](https://github.com/moeru-ai/alint/tree/main/packages/plugin-example-go) | Example semantic Go review plugin using `text/plain`. |
529
+ | [`@alint-js/plugin-example-go`](https://github.com/moeru-ai/alint/tree/main/packages/plugin-example-go) | Example semantic Go review plugin using `plaintext`. |
526
530
 
527
531
  ## Documentation Automation
528
532
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-DUyZBCTT.mjs";
2
+ import { t as executeCli } from "../cli-B0jEUKYS.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -1,22 +1,22 @@
1
1
  import process from "node:process";
2
2
  import { inspect } from "node:util";
3
3
  import { cac } from "cac";
4
- import { appendFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
4
+ import { appendFile, mkdir, opendir, readFile, readdir, rm, stat } from "node:fs/promises";
5
5
  import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
6
- import { AlintRunCancelledError, AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
6
+ import { AlintRunCancelledError, AlintRunError, benchmarkModels, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
7
7
  import { isAbsolute, join, relative, resolve } from "pathe";
8
- import { getBorderCharacters, table } from "table";
9
8
  import { errorMessageFrom } from "@moeru/std/error";
9
+ import { getBorderCharacters, table } from "table";
10
10
  import { createColors } from "tinyrainbow";
11
11
  import cliSpinners from "cli-spinners";
12
+ import fastStringWidth from "fast-string-width";
12
13
  import { relative as relative$1 } from "node:path";
13
14
  import fastStringTruncatedWidth from "fast-string-truncated-width";
14
- import fastStringWidth from "fast-string-width";
15
15
  import Gitignore from "gitignore-fs";
16
16
  import { Minimatch, minimatch } from "minimatch";
17
17
  import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
18
18
  //#region package.json
19
- var version = "0.1.4";
19
+ var version = "0.1.5";
20
20
  //#endregion
21
21
  //#region src/cli/output.ts
22
22
  function escapeLineValue(value) {
@@ -218,6 +218,7 @@ function shouldSkipOptionValue(arg) {
218
218
  "--rule-concurrency",
219
219
  "--since",
220
220
  "--timeout-ms",
221
+ "--with-speed-concurrency",
221
222
  "-c",
222
223
  "-f",
223
224
  "-l"
@@ -246,7 +247,43 @@ async function runConfigInspectCommand(path, configPath, io) {
246
247
  return 0;
247
248
  }
248
249
  //#endregion
250
+ //#region src/cli/reporters/progress/bar.ts
251
+ const minBarWidth = 4;
252
+ const maxBarWidth = 16;
253
+ const animationThreshold = .4;
254
+ function formatMiniBar(options) {
255
+ if (options.planned <= 0 || options.width < minBarWidth) return "";
256
+ const width = Math.min(Math.max(Math.floor(options.width), minBarWidth), maxBarWidth);
257
+ const ratio = Math.min(Math.max(options.completed / options.planned, 0), 1);
258
+ const completeCells = Math.min(Math.floor(ratio * width), width);
259
+ if (width - completeCells === 0) return `[${"█".repeat(width)}]`;
260
+ if (ratio < animationThreshold) return formatStaticBar(completeCells, width);
261
+ const frontierIndex = completeCells;
262
+ const phase = positiveModulo(options.tick, frontierIndex + 2);
263
+ if (phase === frontierIndex + 1) return formatStaticBar(completeCells, width);
264
+ return `[${Array.from({ length: width }, (_, index) => {
265
+ if (phase === 0) {
266
+ if (index === 0) return "▓";
267
+ if (index <= frontierIndex) return "█";
268
+ return "░";
269
+ }
270
+ if (index < phase) return "█";
271
+ if (index === phase) return "▓";
272
+ if (index === phase + 1 && index <= frontierIndex) return "░";
273
+ if (index <= frontierIndex) return "█";
274
+ return "░";
275
+ }).join("")}]`;
276
+ }
277
+ function formatStaticBar(completeCells, width) {
278
+ return `[${"█".repeat(completeCells)}${"░".repeat(width - completeCells)}]`;
279
+ }
280
+ function positiveModulo(value, divisor) {
281
+ return (Math.floor(value) % divisor + divisor) % divisor;
282
+ }
283
+ //#endregion
249
284
  //#region src/cli/provider-registry.ts
285
+ const colors$4 = createColors({ force: true });
286
+ const defaultBenchmarkConcurrency = 2;
250
287
  function formatDuplicateModelIdentity(identity) {
251
288
  return [
252
289
  `model "${escapeLineValue(identity)}" is configured more than once.`,
@@ -256,6 +293,7 @@ function formatDuplicateModelIdentity(identity) {
256
293
  }
257
294
  const providerSetupSources = [
258
295
  {
296
+ benchmarkConcurrency: 2,
259
297
  defaultEndpoint: "http://127.0.0.1:8317/v1",
260
298
  defaultProviderId: "CLIProxyAPI",
261
299
  label: "CLIProxyAPI",
@@ -263,11 +301,21 @@ const providerSetupSources = [
263
301
  value: "cliProxyApi"
264
302
  },
265
303
  {
304
+ benchmarkConcurrency: 2,
266
305
  label: "Custom OpenAI-compatible provider",
267
306
  probeModels: true,
268
307
  value: "custom"
269
308
  },
270
309
  {
310
+ benchmarkConcurrency: 20,
311
+ defaultEndpoint: "https://openrouter.ai/api/v1",
312
+ defaultProviderId: "openrouter",
313
+ label: "OpenRouter",
314
+ probeModels: true,
315
+ value: "openrouter"
316
+ },
317
+ {
318
+ benchmarkConcurrency: 2,
271
319
  defaultEndpoint: "https://api.cerebras.ai/v1",
272
320
  defaultProviderId: "cerebras",
273
321
  label: "Cerebras",
@@ -275,6 +323,7 @@ const providerSetupSources = [
275
323
  value: "cerebras"
276
324
  },
277
325
  {
326
+ benchmarkConcurrency: 2,
278
327
  defaultEndpoint: "https://api.groq.com/openai/v1",
279
328
  defaultProviderId: "groq",
280
329
  label: "Groq",
@@ -282,12 +331,14 @@ const providerSetupSources = [
282
331
  value: "groq"
283
332
  },
284
333
  {
334
+ benchmarkConcurrency: 2,
285
335
  defaultEndpoint: "http://localhost:11434/v1",
286
336
  label: "Ollama",
287
337
  probeModels: true,
288
338
  value: "ollama"
289
339
  },
290
340
  {
341
+ benchmarkConcurrency: 2,
291
342
  label: "Manual model entry",
292
343
  probeModels: false,
293
344
  value: "manual"
@@ -353,6 +404,88 @@ function formatAmbiguousModels(request, candidates) {
353
404
  ...choices.map((choice) => ` ${escapeLineValue(choice)}`)
354
405
  ].join("\n")}\n`;
355
406
  }
407
+ function formatModelBenchmarkList(config, benchmarks, options = {}) {
408
+ const benchmarksByIdentity = /* @__PURE__ */ new Map();
409
+ for (const benchmark of benchmarks) {
410
+ const identity = `${benchmark.providerId}/${benchmark.modelId}`;
411
+ const matches = benchmarksByIdentity.get(identity) ?? [];
412
+ matches.push(benchmark);
413
+ benchmarksByIdentity.set(identity, matches);
414
+ }
415
+ return formatBenchmarkTable(flattenModels(config).map(({ model, provider }) => {
416
+ const benchmark = benchmarksByIdentity.get(`${provider.id}/${model.id}`)?.shift();
417
+ return benchmark === void 0 ? [
418
+ model.id,
419
+ provider.id,
420
+ model.name ?? model.id,
421
+ "n/a",
422
+ "n/a",
423
+ "n/a",
424
+ "0/0"
425
+ ] : benchmarkRow(model.id, model.name ?? model.id, provider.id, benchmark, options.color === true);
426
+ }));
427
+ }
428
+ function formatModelBenchmarkProgressFrame(config, progress, options) {
429
+ const columns = {
430
+ 3: {
431
+ truncate: 10,
432
+ width: 10
433
+ },
434
+ 4: {
435
+ truncate: 12,
436
+ width: 12
437
+ },
438
+ 5: {
439
+ truncate: 16,
440
+ width: 16
441
+ },
442
+ 6: {
443
+ truncate: 8,
444
+ width: 8
445
+ }
446
+ };
447
+ const models = flattenModels(config);
448
+ const modelCapacity = options.maxRows === void 0 ? models.length : Math.max(options.maxRows - 2, 1);
449
+ const activeByModelIndex = new Map(progress.active.map((current) => [current.modelIndex, current]));
450
+ const completed = progress.results.filter((result) => result !== void 0).length;
451
+ const activeIndex = progress.active[0]?.modelIndex ?? completed;
452
+ const start = Math.min(Math.max(activeIndex - Math.floor(modelCapacity / 2), 0), Math.max(models.length - modelCapacity, 0));
453
+ const end = Math.min(start + modelCapacity, models.length);
454
+ const visibleModels = models.slice(start, end);
455
+ const bodyRows = formatBenchmarkTable(visibleModels.map(({ model, provider }, visibleIndex) => {
456
+ const modelIndex = start + visibleIndex;
457
+ const benchmark = progress.results[modelIndex];
458
+ if (benchmark !== void 0) return benchmarkRow(model.id, model.name ?? model.id, provider.id, benchmark, options.color === true);
459
+ const current = activeByModelIndex.get(modelIndex);
460
+ if (current !== void 0) return activeBenchmarkRow(model.id, model.name ?? model.id, provider.id, current, options);
461
+ return [
462
+ model.id,
463
+ provider.id,
464
+ model.name ?? model.id,
465
+ "pending",
466
+ "pending",
467
+ "pending",
468
+ "0/0"
469
+ ];
470
+ }), columns).trimEnd().split("\n");
471
+ const range = models.length > visibleModels.length ? ` · showing models ${start + 1}-${end} of ${models.length}` : "";
472
+ const bar = formatMiniBar({
473
+ completed,
474
+ planned: progress.modelsTotal,
475
+ tick: options.tick,
476
+ width: 12
477
+ });
478
+ const running = progress.active.length === 0 ? "" : ` · ${progress.active.length} running`;
479
+ const speedColumn = bodyRows[0]?.indexOf("repeat") ?? -1;
480
+ if (speedColumn < 0) throw new Error("Benchmark progress table is missing its speed columns.");
481
+ return { rows: [...bodyRows.map((row) => ({
482
+ prefix: row.slice(0, speedColumn),
483
+ suffix: row.slice(speedColumn)
484
+ })), {
485
+ prefix: "",
486
+ suffix: `${completed}/${progress.modelsTotal} models complete ${bar}${running}${range}`
487
+ }] };
488
+ }
356
489
  function formatModelList(config) {
357
490
  return formatTable([[
358
491
  "id",
@@ -426,10 +559,65 @@ async function probeModels(endpoint, headers = {}) {
426
559
  if (!isRecord$1(body) || !Array.isArray(body.data) || !body.data.every(isModelRecord)) throw new TypeError("Expected OpenAI-compatible models response with data array.");
427
560
  return body.data.map((model) => model.id);
428
561
  }
562
+ function resolveProviderBenchmarkConcurrency(config, overrides = []) {
563
+ const concurrency = Object.fromEntries(config.providers.map((provider) => [provider.id, findProviderSetupSourceByEndpoint(provider.endpoint)?.benchmarkConcurrency ?? defaultBenchmarkConcurrency]));
564
+ for (const override of overrides) {
565
+ const separatorIndex = override.indexOf("=");
566
+ if (separatorIndex <= 0 || separatorIndex === override.length - 1) throw new Error("Invalid benchmark concurrency override. Expected <provider-id>=<limit>.");
567
+ const providerId = override.slice(0, separatorIndex);
568
+ const value = override.slice(separatorIndex + 1);
569
+ const limit = Number(value);
570
+ if (!(providerId in concurrency)) throw new Error(`Unknown provider "${escapeLineValue(providerId)}" in benchmark concurrency override.`);
571
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(limit) || limit < 1) throw new Error(`Benchmark concurrency for provider "${escapeLineValue(providerId)}" must be a positive integer.`);
572
+ concurrency[providerId] = limit;
573
+ }
574
+ return concurrency;
575
+ }
576
+ function activeBenchmarkRow(modelId, modelName, providerId, current, options) {
577
+ const frame = options.color === true ? colors$4.cyan(options.frame) : options.frame;
578
+ const active = current.warmup ? "warm-up" : `${frame} ${current.sample}/${current.samples}`;
579
+ return [
580
+ modelId,
581
+ providerId,
582
+ modelName,
583
+ current.phase === "repeat" ? active : "done",
584
+ current.phase === "non-repeat" ? active : "pending",
585
+ current.phase === "non-repeat" ? "measuring" : "pending",
586
+ `${current.success.completed}/${current.success.attempted}`
587
+ ];
588
+ }
589
+ function benchmarkRow(modelId, modelName, providerId, benchmark, color) {
590
+ const errored = color ? colors$4.red("errored") : "errored";
591
+ return [
592
+ modelId,
593
+ providerId,
594
+ modelName,
595
+ benchmark.error === void 0 ? formatDuration$1(benchmark.repeatMs) : errored,
596
+ benchmark.error === void 0 ? formatDuration$1(benchmark.nonRepeatMs) : errored,
597
+ benchmark.error === void 0 ? formatThroughput(benchmark.throughput) : errored,
598
+ `${benchmark.success.completed}/${benchmark.success.attempted}`
599
+ ];
600
+ }
429
601
  function canonicalIdentity(candidate) {
430
602
  return `${candidate.provider.id}/${candidate.model.id}`;
431
603
  }
432
- function formatTable(rows) {
604
+ function formatBenchmarkTable(rows, columns) {
605
+ return formatTable([[
606
+ "id",
607
+ "provider",
608
+ "name",
609
+ "repeat",
610
+ "non-repeat",
611
+ "throughput",
612
+ "success"
613
+ ], ...rows], columns);
614
+ }
615
+ function formatDuration$1(durationMs) {
616
+ if (durationMs === void 0) return "n/a";
617
+ if (durationMs < 1e3) return `${Math.round(durationMs)}ms`;
618
+ return `${stripTrailingZeros((durationMs / 1e3).toFixed(2))}s`;
619
+ }
620
+ function formatTable(rows, columns) {
433
621
  if (rows.length <= 1) return "";
434
622
  return table(rows, {
435
623
  border: getBorderCharacters("void"),
@@ -437,9 +625,13 @@ function formatTable(rows) {
437
625
  paddingLeft: 0,
438
626
  paddingRight: 2
439
627
  },
628
+ columns,
440
629
  drawHorizontalLine: () => false
441
630
  });
442
631
  }
632
+ function formatThroughput(throughput) {
633
+ return throughput === void 0 ? "n/a" : `${throughput.toFixed(1)} tok/s`;
634
+ }
443
635
  function isModelRecord(value) {
444
636
  return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0;
445
637
  }
@@ -449,6 +641,9 @@ function isRecord$1(value) {
449
641
  function normalizeProviderIdBase(value) {
450
642
  return value.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider";
451
643
  }
644
+ function stripTrailingZeros(value) {
645
+ return String(Number(value));
646
+ }
452
647
  //#endregion
453
648
  //#region src/cli/commands/config/setup-config.ts
454
649
  function formatUnknownProvider(providerId, scope) {
@@ -471,16 +666,163 @@ async function loadScopedSetupConfig(io, local) {
471
666
  };
472
667
  }
473
668
  //#endregion
669
+ //#region src/cli/commands/config/models/benchmark-progress.ts
670
+ /** Keeps the benchmark preview live without mixing transient frames into the final stdout table. */
671
+ function createModelBenchmarkProgressDisplay(options) {
672
+ const frames = cliSpinners.dots.frames;
673
+ let frameIndex = 0;
674
+ let latest;
675
+ let previous;
676
+ let interval;
677
+ const render = () => {
678
+ if (latest === void 0) return;
679
+ const next = formatModelBenchmarkProgressFrame(options.config, latest, {
680
+ color: options.color,
681
+ frame: frames[frameIndex % frames.length] ?? "",
682
+ maxRows: options.output.rows,
683
+ tick: 0
684
+ });
685
+ if (previous === void 0 || !sameLayout(previous, next)) {
686
+ clearFrame(options.output, previous?.rows.length ?? 0);
687
+ options.output.write(`${next.rows.map((row) => `${row.prefix}${row.suffix}`).join("\n")}\r`);
688
+ previous = next;
689
+ return;
690
+ }
691
+ for (const [rowIndex, row] of next.rows.entries()) {
692
+ const previousRow = previous.rows[rowIndex];
693
+ if (previousRow !== void 0 && previousRow.suffix !== row.suffix) patchSuffix(options.output, rowIndex, next.rows.length, fastStringWidth(row.prefix), row.suffix);
694
+ }
695
+ previous = next;
696
+ };
697
+ const start = () => {
698
+ if (interval === void 0) {
699
+ interval = globalThis.setInterval(() => {
700
+ frameIndex += 1;
701
+ render();
702
+ }, cliSpinners.dots.interval);
703
+ interval.unref();
704
+ }
705
+ render();
706
+ };
707
+ /**
708
+ * Applies one core benchmark progress snapshot to the transient TTY table.
709
+ *
710
+ * Triggering workflow:
711
+ *
712
+ * `benchmarkModels` `onProgress`
713
+ * -> `config models list --with-speed`
714
+ * -> `ModelBenchmarkProgressDisplay.update`
715
+ *
716
+ * Upstream:
717
+ * - `benchmarkModels` `onProgress` callback registered by `runListModelsCommand`
718
+ *
719
+ * Downstream:
720
+ * - incremental benchmark table renderer `start` or `render`
721
+ */
722
+ const update = (progress) => {
723
+ latest = progress;
724
+ if (interval === void 0) {
725
+ start();
726
+ return;
727
+ }
728
+ render();
729
+ };
730
+ return {
731
+ finish: () => {
732
+ if (interval !== void 0) {
733
+ globalThis.clearInterval(interval);
734
+ interval = void 0;
735
+ }
736
+ clearFrame(options.output, previous?.rows.length ?? 0);
737
+ previous = void 0;
738
+ },
739
+ update
740
+ };
741
+ }
742
+ function clearFrame(output, rowCount) {
743
+ if (rowCount === 0) return;
744
+ let sequence = "\r\x1B[K";
745
+ for (let row = 1; row < rowCount; row += 1) sequence += "\r\x1B[1A\x1B[K";
746
+ output.write(sequence);
747
+ }
748
+ function patchSuffix(output, rowIndex, rowCount, column, suffix) {
749
+ const rowsUp = rowCount - rowIndex - 1;
750
+ const moveUp = rowsUp === 0 ? "" : `\x1B[${rowsUp}A`;
751
+ const moveDown = rowsUp === 0 ? "" : `\x1B[${rowsUp}B`;
752
+ const moveRight = column === 0 ? "" : `\x1B[${column}C`;
753
+ output.write(`\r${moveUp}${moveRight}\x1B[K${suffix}\r${moveDown}`);
754
+ }
755
+ function sameLayout(previous, next) {
756
+ return previous.rows.length === next.rows.length && previous.rows.every((row, index) => row.prefix === next.rows[index]?.prefix);
757
+ }
758
+ //#endregion
474
759
  //#region src/cli/commands/config/models/ls.ts
475
760
  const ls$1 = defineCommand({
476
- async action(context) {
477
- context.io.stdout.write(formatModelList(await loadMergedSetupConfig(context.io)));
478
- return 0;
479
- },
761
+ action: runListModelsCommand,
480
762
  alias: ["ls"],
481
763
  description: "List configured models",
482
- name: "list"
764
+ name: "list",
765
+ options: [{
766
+ description: "Benchmark configured models with live requests",
767
+ flags: "--with-speed"
768
+ }, {
769
+ description: "Override one provider benchmark concurrency (repeatable)",
770
+ flags: "--with-speed-concurrency <provider-id=limit>"
771
+ }]
483
772
  });
773
+ /**
774
+ * Lists configured models and optionally benchmarks each model with live streamed requests.
775
+ *
776
+ * Triggering workflow:
777
+ *
778
+ * {@link defineCommand}
779
+ * -> `config models list`
780
+ * -> {@link runListModelsCommand}
781
+ *
782
+ * Upstream:
783
+ * - `config models list` command dispatch
784
+ *
785
+ * Downstream:
786
+ * - {@link loadMergedSetupConfig}
787
+ * - {@link benchmarkModels}
788
+ * - {@link createModelBenchmarkProgressDisplay}
789
+ * - `context.io.stdout.write`
790
+ */
791
+ async function runListModelsCommand(context, options) {
792
+ const config = await loadMergedSetupConfig(context.io);
793
+ const concurrencyOverrides = options.withSpeedConcurrency === void 0 ? [] : [options.withSpeedConcurrency].flat();
794
+ if (options.withSpeed !== true) {
795
+ if (concurrencyOverrides.length > 0) {
796
+ context.io.stderr.write("--with-speed-concurrency requires --with-speed.\n");
797
+ return 2;
798
+ }
799
+ context.io.stdout.write(formatModelList(config));
800
+ return 0;
801
+ }
802
+ let providerConcurrency;
803
+ try {
804
+ providerConcurrency = resolveProviderBenchmarkConcurrency(config, concurrencyOverrides);
805
+ } catch (error) {
806
+ context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid benchmark concurrency override."}\n`);
807
+ return 2;
808
+ }
809
+ const progress = context.io.stderr.isTTY === true ? createModelBenchmarkProgressDisplay({
810
+ color: true,
811
+ config,
812
+ output: context.io.stderr
813
+ }) : void 0;
814
+ let benchmarks;
815
+ try {
816
+ benchmarks = await benchmarkModels(config, {
817
+ onProgress: (snapshot) => progress?.update(snapshot),
818
+ providerConcurrency
819
+ });
820
+ } finally {
821
+ progress?.finish();
822
+ }
823
+ context.io.stdout.write(formatModelBenchmarkList(config, benchmarks, { color: context.io.stdout.isTTY === true }));
824
+ return 0;
825
+ }
484
826
  //#endregion
485
827
  //#region src/cli/commands/config/probe.ts
486
828
  function providerHeadersFromOptions(options) {
@@ -1794,40 +2136,6 @@ function countDiagnostics(diagnostics, severity) {
1794
2136
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
1795
2137
  }
1796
2138
  //#endregion
1797
- //#region src/cli/reporters/progress/bar.ts
1798
- const minBarWidth = 4;
1799
- const maxBarWidth = 16;
1800
- const animationThreshold = .4;
1801
- function formatMiniBar(options) {
1802
- if (options.planned <= 0 || options.width < minBarWidth) return "";
1803
- const width = Math.min(Math.max(Math.floor(options.width), minBarWidth), maxBarWidth);
1804
- const ratio = Math.min(Math.max(options.completed / options.planned, 0), 1);
1805
- const completeCells = Math.min(Math.floor(ratio * width), width);
1806
- if (width - completeCells === 0) return `[${"█".repeat(width)}]`;
1807
- if (ratio < animationThreshold) return formatStaticBar(completeCells, width);
1808
- const frontierIndex = completeCells;
1809
- const phase = positiveModulo(options.tick, frontierIndex + 2);
1810
- if (phase === frontierIndex + 1) return formatStaticBar(completeCells, width);
1811
- return `[${Array.from({ length: width }, (_, index) => {
1812
- if (phase === 0) {
1813
- if (index === 0) return "▓";
1814
- if (index <= frontierIndex) return "█";
1815
- return "░";
1816
- }
1817
- if (index < phase) return "█";
1818
- if (index === phase) return "▓";
1819
- if (index === phase + 1 && index <= frontierIndex) return "░";
1820
- if (index <= frontierIndex) return "█";
1821
- return "░";
1822
- }).join("")}]`;
1823
- }
1824
- function formatStaticBar(completeCells, width) {
1825
- return `[${"█".repeat(completeCells)}${"░".repeat(width - completeCells)}]`;
1826
- }
1827
- function positiveModulo(value, divisor) {
1828
- return (Math.floor(value) % divisor + divisor) % divisor;
1829
- }
1830
- //#endregion
1831
2139
  //#region src/cli/reporters/progress/summary.ts
1832
2140
  const colors$2 = createColors({ force: true });
1833
2141
  const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
@@ -1896,6 +2204,7 @@ function createSummaryProgressReporter(options) {
1896
2204
  state: "running"
1897
2205
  });
1898
2206
  },
2207
+ onPlanningEnd: ({ progress }) => updateProgress(state, progress),
1899
2208
  onPrepareStart: ({ startedAt }) => {
1900
2209
  state.activeJobs.clear();
1901
2210
  state.errorCount = 0;
@@ -1995,11 +2304,12 @@ function createInitialState() {
1995
2304
  function createProgress() {
1996
2305
  return {
1997
2306
  execution: createCounts(),
2307
+ filesPlanned: 0,
1998
2308
  filesTotal: 0,
1999
- final: false,
2000
2309
  jobsCompleted: 0,
2001
2310
  jobsStarted: 0,
2002
- jobsTotal: 0
2311
+ jobsTotal: 0,
2312
+ planningComplete: false
2003
2313
  };
2004
2314
  }
2005
2315
  function createRows(state, options, now) {
@@ -2098,10 +2408,10 @@ function formatDuration(ms) {
2098
2408
  function formatFooters(state, options, now) {
2099
2409
  const terminal = terminalCount(state.execution);
2100
2410
  const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
2101
- const progressBar = state.progress.final ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
2102
- const eta = state.progress.final ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
2103
- const projectedTokens = state.progress.final && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
2104
- const discovering = state.progress.final ? "" : " jobs (discovering)";
2411
+ const progressBar = state.progress.planningComplete ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
2412
+ const eta = state.progress.planningComplete ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
2413
+ const projectedTokens = state.progress.planningComplete && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
2414
+ const discovering = state.progress.planningComplete ? "" : " jobs (discovering)";
2105
2415
  return [
2106
2416
  fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
2107
2417
  fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
@@ -2289,6 +2599,7 @@ function createRenderingProgressReporter(summary, renderer) {
2289
2599
  //#endregion
2290
2600
  //#region src/cli/commands/lint/discovery.ts
2291
2601
  const minimatchOptions = { dot: true };
2602
+ const directoryBufferSize = 256;
2292
2603
  var NoFilesFoundError = class extends Error {
2293
2604
  globInputPaths;
2294
2605
  pattern;
@@ -2376,15 +2687,16 @@ async function resolveInputFiles(options) {
2376
2687
  })) continue;
2377
2688
  directories.push(input);
2378
2689
  if (!hasFilePatterns) continue;
2379
- const directoryFiles = (await walkFiles(path, {
2690
+ const directoryFiles = [];
2691
+ for await (const file of walkFiles(path, {
2380
2692
  cwd: options.cwd,
2381
2693
  gitignore: options.gitignore,
2382
2694
  ignoredPatterns
2383
- })).sort();
2384
- for (const directoryFile of directoryFiles) {
2385
- const relativePath = normalizeRelativePath(options.cwd, directoryFile);
2386
- if (hasFilePatterns && matchesDiscoveryFile(relativePath, options.config, { cwd: options.cwd })) candidates.push(relativePath);
2695
+ })) {
2696
+ const relativePath = normalizeRelativePath(options.cwd, file);
2697
+ if (hasFilePatterns && matchesDiscoveryFile(relativePath, options.config, { cwd: options.cwd })) directoryFiles.push(relativePath);
2387
2698
  }
2699
+ for (const file of directoryFiles.sort()) candidates.push(file);
2388
2700
  continue;
2389
2701
  }
2390
2702
  if (options.globInputPaths && isGlobPattern(input)) {
@@ -2397,7 +2709,7 @@ async function resolveInputFiles(options) {
2397
2709
  pattern: input
2398
2710
  });
2399
2711
  if (matches.length === 0 && options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
2400
- candidates.push(...matches);
2712
+ for (const match of matches) candidates.push(match);
2401
2713
  continue;
2402
2714
  }
2403
2715
  if (options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
@@ -2412,20 +2724,19 @@ async function searchGlob(options) {
2412
2724
  const rootStats = await statPath(root);
2413
2725
  const pattern = normalizeGlobPattern(options.cwd, options.pattern);
2414
2726
  if (!rootStats?.isDirectory()) return [];
2415
- const files = (await walkFiles(root, {
2727
+ const candidates = [];
2728
+ for await (const file of walkFiles(root, {
2416
2729
  cwd: options.cwd,
2417
2730
  gitignore: options.gitignore,
2418
2731
  ignoredPatterns: options.ignoredPatterns
2419
- })).sort();
2420
- const candidates = [];
2421
- for (const file of files) {
2732
+ })) {
2422
2733
  const relativePath = normalizeRelativePath(options.cwd, file);
2423
2734
  if (matchesGlob(relativePath, pattern) && shouldIncludeGlobCandidate(relativePath, options.config, {
2424
2735
  cwd: options.cwd,
2425
2736
  hasFilePatterns: options.hasFilePatterns
2426
2737
  })) candidates.push(relativePath);
2427
2738
  }
2428
- return candidates;
2739
+ return candidates.sort();
2429
2740
  }
2430
2741
  function shouldFilterGitignoredFiles(config) {
2431
2742
  return normalizeConfig(config).some((item) => item.ignore?.gitignore === true);
@@ -2446,18 +2757,27 @@ async function statPath(path) {
2446
2757
  throw error;
2447
2758
  }
2448
2759
  }
2449
- async function walkFiles(root, options) {
2450
- const entries = await readdir(root, { withFileTypes: true });
2451
- const files = [];
2452
- for (const entry of entries) {
2453
- const path = resolve(root, entry.name);
2454
- if (entry.isDirectory()) {
2455
- if (!await shouldPruneDirectory(path, options)) files.push(...await walkFiles(path, options));
2456
- continue;
2760
+ /**
2761
+ * Streams directory entries so unmatched trees never accumulate into recursive child arrays.
2762
+ * The consumer owns result ordering because filesystem iteration order is not stable.
2763
+ */
2764
+ async function* walkFiles(root, options) {
2765
+ const pendingDirectories = [root];
2766
+ while (pendingDirectories.length > 0) {
2767
+ const currentDirectory = pendingDirectories.pop();
2768
+ if (currentDirectory === void 0) continue;
2769
+ const childDirectories = [];
2770
+ const directory = await opendir(currentDirectory, { bufferSize: directoryBufferSize });
2771
+ for await (const entry of directory) {
2772
+ const path = resolve(currentDirectory, entry.name);
2773
+ if (entry.isDirectory()) {
2774
+ if (!await shouldPruneDirectory(path, options)) childDirectories.push(path);
2775
+ continue;
2776
+ }
2777
+ if (entry.isFile()) yield path;
2457
2778
  }
2458
- if (entry.isFile()) files.push(path);
2779
+ for (const path of childDirectories.reverse()) pendingDirectories.push(path);
2459
2780
  }
2460
- return files;
2461
2781
  }
2462
2782
  //#endregion
2463
2783
  //#region src/cli/commands/lint/errors.ts
@@ -3007,6 +3327,9 @@ function mergeProgressReporters(base, extra) {
3007
3327
  onJobStart: (payload) => {
3008
3328
  deliverBoth((reporter) => reporter.onJobStart?.(payload));
3009
3329
  },
3330
+ onPlanningEnd: (payload) => {
3331
+ deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
3332
+ },
3010
3333
  onPrepareEnd: (payload) => {
3011
3334
  deliverBoth((reporter) => reporter.onPrepareEnd?.(payload));
3012
3335
  },
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-DUyZBCTT.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-B0jEUKYS.mjs";
2
2
  import { ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated } from "@alint-js/config";
3
3
  import { defineConfig } from "@alint-js/core";
4
4
  export { defineConfig, executeCli, formatDiagnostics, formatJson, formatStylish, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -27,8 +27,8 @@
27
27
  "pathe": "^2.0.3",
28
28
  "table": "^6.9.0",
29
29
  "tinyrainbow": "^3.1.0",
30
- "@alint-js/config": "0.1.4",
31
- "@alint-js/core": "0.1.4"
30
+ "@alint-js/config": "0.1.5",
31
+ "@alint-js/core": "0.1.5"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@pnpm/find-workspace-dir": "^1000.1.5",
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "alint": "node ./dist/bin/index.mjs",
41
+ "bench": "vitest bench --config vitest.config.ts",
41
42
  "build": "tsdown",
42
43
  "build:binary": "tsdown --config tsdown.binary.config.ts --config-loader unrun",
43
44
  "typecheck": "tsc -p tsconfig.json --noEmit",