@alint-js/cli 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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-3Ov9-ZVf.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -1,22 +1,23 @@
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";
14
+ import { formatDuration, intervalToDuration } from "date-fns";
13
15
  import fastStringTruncatedWidth from "fast-string-truncated-width";
14
- import fastStringWidth from "fast-string-width";
15
16
  import Gitignore from "gitignore-fs";
16
17
  import { Minimatch, minimatch } from "minimatch";
17
18
  import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
18
19
  //#region package.json
19
- var version = "0.1.4";
20
+ var version = "0.2.0";
20
21
  //#endregion
21
22
  //#region src/cli/output.ts
22
23
  function escapeLineValue(value) {
@@ -218,6 +219,7 @@ function shouldSkipOptionValue(arg) {
218
219
  "--rule-concurrency",
219
220
  "--since",
220
221
  "--timeout-ms",
222
+ "--with-speed-concurrency",
221
223
  "-c",
222
224
  "-f",
223
225
  "-l"
@@ -246,7 +248,43 @@ async function runConfigInspectCommand(path, configPath, io) {
246
248
  return 0;
247
249
  }
248
250
  //#endregion
251
+ //#region src/cli/reporters/progress/bar.ts
252
+ const minBarWidth = 4;
253
+ const maxBarWidth = 16;
254
+ const animationThreshold = .4;
255
+ function formatMiniBar(options) {
256
+ if (options.planned <= 0 || options.width < minBarWidth) return "";
257
+ const width = Math.min(Math.max(Math.floor(options.width), minBarWidth), maxBarWidth);
258
+ const ratio = Math.min(Math.max(options.completed / options.planned, 0), 1);
259
+ const completeCells = Math.min(Math.floor(ratio * width), width);
260
+ if (width - completeCells === 0) return `[${"█".repeat(width)}]`;
261
+ if (ratio < animationThreshold) return formatStaticBar(completeCells, width);
262
+ const frontierIndex = completeCells;
263
+ const phase = positiveModulo(options.tick, frontierIndex + 2);
264
+ if (phase === frontierIndex + 1) return formatStaticBar(completeCells, width);
265
+ return `[${Array.from({ length: width }, (_, index) => {
266
+ if (phase === 0) {
267
+ if (index === 0) return "▓";
268
+ if (index <= frontierIndex) return "█";
269
+ return "░";
270
+ }
271
+ if (index < phase) return "█";
272
+ if (index === phase) return "▓";
273
+ if (index === phase + 1 && index <= frontierIndex) return "░";
274
+ if (index <= frontierIndex) return "█";
275
+ return "░";
276
+ }).join("")}]`;
277
+ }
278
+ function formatStaticBar(completeCells, width) {
279
+ return `[${"█".repeat(completeCells)}${"░".repeat(width - completeCells)}]`;
280
+ }
281
+ function positiveModulo(value, divisor) {
282
+ return (Math.floor(value) % divisor + divisor) % divisor;
283
+ }
284
+ //#endregion
249
285
  //#region src/cli/provider-registry.ts
286
+ const colors$4 = createColors({ force: true });
287
+ const defaultBenchmarkConcurrency = 2;
250
288
  function formatDuplicateModelIdentity(identity) {
251
289
  return [
252
290
  `model "${escapeLineValue(identity)}" is configured more than once.`,
@@ -256,6 +294,7 @@ function formatDuplicateModelIdentity(identity) {
256
294
  }
257
295
  const providerSetupSources = [
258
296
  {
297
+ benchmarkConcurrency: 2,
259
298
  defaultEndpoint: "http://127.0.0.1:8317/v1",
260
299
  defaultProviderId: "CLIProxyAPI",
261
300
  label: "CLIProxyAPI",
@@ -263,11 +302,21 @@ const providerSetupSources = [
263
302
  value: "cliProxyApi"
264
303
  },
265
304
  {
305
+ benchmarkConcurrency: 2,
266
306
  label: "Custom OpenAI-compatible provider",
267
307
  probeModels: true,
268
308
  value: "custom"
269
309
  },
270
310
  {
311
+ benchmarkConcurrency: 20,
312
+ defaultEndpoint: "https://openrouter.ai/api/v1",
313
+ defaultProviderId: "openrouter",
314
+ label: "OpenRouter",
315
+ probeModels: true,
316
+ value: "openrouter"
317
+ },
318
+ {
319
+ benchmarkConcurrency: 2,
271
320
  defaultEndpoint: "https://api.cerebras.ai/v1",
272
321
  defaultProviderId: "cerebras",
273
322
  label: "Cerebras",
@@ -275,6 +324,7 @@ const providerSetupSources = [
275
324
  value: "cerebras"
276
325
  },
277
326
  {
327
+ benchmarkConcurrency: 2,
278
328
  defaultEndpoint: "https://api.groq.com/openai/v1",
279
329
  defaultProviderId: "groq",
280
330
  label: "Groq",
@@ -282,12 +332,14 @@ const providerSetupSources = [
282
332
  value: "groq"
283
333
  },
284
334
  {
335
+ benchmarkConcurrency: 2,
285
336
  defaultEndpoint: "http://localhost:11434/v1",
286
337
  label: "Ollama",
287
338
  probeModels: true,
288
339
  value: "ollama"
289
340
  },
290
341
  {
342
+ benchmarkConcurrency: 2,
291
343
  label: "Manual model entry",
292
344
  probeModels: false,
293
345
  value: "manual"
@@ -353,6 +405,88 @@ function formatAmbiguousModels(request, candidates) {
353
405
  ...choices.map((choice) => ` ${escapeLineValue(choice)}`)
354
406
  ].join("\n")}\n`;
355
407
  }
408
+ function formatModelBenchmarkList(config, benchmarks, options = {}) {
409
+ const benchmarksByIdentity = /* @__PURE__ */ new Map();
410
+ for (const benchmark of benchmarks) {
411
+ const identity = `${benchmark.providerId}/${benchmark.modelId}`;
412
+ const matches = benchmarksByIdentity.get(identity) ?? [];
413
+ matches.push(benchmark);
414
+ benchmarksByIdentity.set(identity, matches);
415
+ }
416
+ return formatBenchmarkTable(flattenModels(config).map(({ model, provider }) => {
417
+ const benchmark = benchmarksByIdentity.get(`${provider.id}/${model.id}`)?.shift();
418
+ return benchmark === void 0 ? [
419
+ model.id,
420
+ provider.id,
421
+ model.name ?? model.id,
422
+ "n/a",
423
+ "n/a",
424
+ "n/a",
425
+ "0/0"
426
+ ] : benchmarkRow(model.id, model.name ?? model.id, provider.id, benchmark, options.color === true);
427
+ }));
428
+ }
429
+ function formatModelBenchmarkProgressFrame(config, progress, options) {
430
+ const columns = {
431
+ 3: {
432
+ truncate: 10,
433
+ width: 10
434
+ },
435
+ 4: {
436
+ truncate: 12,
437
+ width: 12
438
+ },
439
+ 5: {
440
+ truncate: 16,
441
+ width: 16
442
+ },
443
+ 6: {
444
+ truncate: 8,
445
+ width: 8
446
+ }
447
+ };
448
+ const models = flattenModels(config);
449
+ const modelCapacity = options.maxRows === void 0 ? models.length : Math.max(options.maxRows - 2, 1);
450
+ const activeByModelIndex = new Map(progress.active.map((current) => [current.modelIndex, current]));
451
+ const completed = progress.results.filter((result) => result !== void 0).length;
452
+ const activeIndex = progress.active[0]?.modelIndex ?? completed;
453
+ const start = Math.min(Math.max(activeIndex - Math.floor(modelCapacity / 2), 0), Math.max(models.length - modelCapacity, 0));
454
+ const end = Math.min(start + modelCapacity, models.length);
455
+ const visibleModels = models.slice(start, end);
456
+ const bodyRows = formatBenchmarkTable(visibleModels.map(({ model, provider }, visibleIndex) => {
457
+ const modelIndex = start + visibleIndex;
458
+ const benchmark = progress.results[modelIndex];
459
+ if (benchmark !== void 0) return benchmarkRow(model.id, model.name ?? model.id, provider.id, benchmark, options.color === true);
460
+ const current = activeByModelIndex.get(modelIndex);
461
+ if (current !== void 0) return activeBenchmarkRow(model.id, model.name ?? model.id, provider.id, current, options);
462
+ return [
463
+ model.id,
464
+ provider.id,
465
+ model.name ?? model.id,
466
+ "pending",
467
+ "pending",
468
+ "pending",
469
+ "0/0"
470
+ ];
471
+ }), columns).trimEnd().split("\n");
472
+ const range = models.length > visibleModels.length ? ` · showing models ${start + 1}-${end} of ${models.length}` : "";
473
+ const bar = formatMiniBar({
474
+ completed,
475
+ planned: progress.modelsTotal,
476
+ tick: options.tick,
477
+ width: 12
478
+ });
479
+ const running = progress.active.length === 0 ? "" : ` · ${progress.active.length} running`;
480
+ const speedColumn = bodyRows[0]?.indexOf("repeat") ?? -1;
481
+ if (speedColumn < 0) throw new Error("Benchmark progress table is missing its speed columns.");
482
+ return { rows: [...bodyRows.map((row) => ({
483
+ prefix: row.slice(0, speedColumn),
484
+ suffix: row.slice(speedColumn)
485
+ })), {
486
+ prefix: "",
487
+ suffix: `${completed}/${progress.modelsTotal} models complete ${bar}${running}${range}`
488
+ }] };
489
+ }
356
490
  function formatModelList(config) {
357
491
  return formatTable([[
358
492
  "id",
@@ -426,10 +560,65 @@ async function probeModels(endpoint, headers = {}) {
426
560
  if (!isRecord$1(body) || !Array.isArray(body.data) || !body.data.every(isModelRecord)) throw new TypeError("Expected OpenAI-compatible models response with data array.");
427
561
  return body.data.map((model) => model.id);
428
562
  }
563
+ function resolveProviderBenchmarkConcurrency(config, overrides = []) {
564
+ const concurrency = Object.fromEntries(config.providers.map((provider) => [provider.id, findProviderSetupSourceByEndpoint(provider.endpoint)?.benchmarkConcurrency ?? defaultBenchmarkConcurrency]));
565
+ for (const override of overrides) {
566
+ const separatorIndex = override.indexOf("=");
567
+ if (separatorIndex <= 0 || separatorIndex === override.length - 1) throw new Error("Invalid benchmark concurrency override. Expected <provider-id>=<limit>.");
568
+ const providerId = override.slice(0, separatorIndex);
569
+ const value = override.slice(separatorIndex + 1);
570
+ const limit = Number(value);
571
+ if (!(providerId in concurrency)) throw new Error(`Unknown provider "${escapeLineValue(providerId)}" in benchmark concurrency override.`);
572
+ if (!/^\d+$/u.test(value) || !Number.isSafeInteger(limit) || limit < 1) throw new Error(`Benchmark concurrency for provider "${escapeLineValue(providerId)}" must be a positive integer.`);
573
+ concurrency[providerId] = limit;
574
+ }
575
+ return concurrency;
576
+ }
577
+ function activeBenchmarkRow(modelId, modelName, providerId, current, options) {
578
+ const frame = options.color === true ? colors$4.cyan(options.frame) : options.frame;
579
+ const active = current.warmup ? "warm-up" : `${frame} ${current.sample}/${current.samples}`;
580
+ return [
581
+ modelId,
582
+ providerId,
583
+ modelName,
584
+ current.phase === "repeat" ? active : "done",
585
+ current.phase === "non-repeat" ? active : "pending",
586
+ current.phase === "non-repeat" ? "measuring" : "pending",
587
+ `${current.success.completed}/${current.success.attempted}`
588
+ ];
589
+ }
590
+ function benchmarkRow(modelId, modelName, providerId, benchmark, color) {
591
+ const errored = color ? colors$4.red("errored") : "errored";
592
+ return [
593
+ modelId,
594
+ providerId,
595
+ modelName,
596
+ benchmark.error === void 0 ? formatDuration$2(benchmark.repeatMs) : errored,
597
+ benchmark.error === void 0 ? formatDuration$2(benchmark.nonRepeatMs) : errored,
598
+ benchmark.error === void 0 ? formatThroughput(benchmark.throughput) : errored,
599
+ `${benchmark.success.completed}/${benchmark.success.attempted}`
600
+ ];
601
+ }
429
602
  function canonicalIdentity(candidate) {
430
603
  return `${candidate.provider.id}/${candidate.model.id}`;
431
604
  }
432
- function formatTable(rows) {
605
+ function formatBenchmarkTable(rows, columns) {
606
+ return formatTable([[
607
+ "id",
608
+ "provider",
609
+ "name",
610
+ "repeat",
611
+ "non-repeat",
612
+ "throughput",
613
+ "success"
614
+ ], ...rows], columns);
615
+ }
616
+ function formatDuration$2(durationMs) {
617
+ if (durationMs === void 0) return "n/a";
618
+ if (durationMs < 1e3) return `${Math.round(durationMs)}ms`;
619
+ return `${stripTrailingZeros((durationMs / 1e3).toFixed(2))}s`;
620
+ }
621
+ function formatTable(rows, columns) {
433
622
  if (rows.length <= 1) return "";
434
623
  return table(rows, {
435
624
  border: getBorderCharacters("void"),
@@ -437,9 +626,13 @@ function formatTable(rows) {
437
626
  paddingLeft: 0,
438
627
  paddingRight: 2
439
628
  },
629
+ columns,
440
630
  drawHorizontalLine: () => false
441
631
  });
442
632
  }
633
+ function formatThroughput(throughput) {
634
+ return throughput === void 0 ? "n/a" : `${throughput.toFixed(1)} tok/s`;
635
+ }
443
636
  function isModelRecord(value) {
444
637
  return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0;
445
638
  }
@@ -449,6 +642,9 @@ function isRecord$1(value) {
449
642
  function normalizeProviderIdBase(value) {
450
643
  return value.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider";
451
644
  }
645
+ function stripTrailingZeros(value) {
646
+ return String(Number(value));
647
+ }
452
648
  //#endregion
453
649
  //#region src/cli/commands/config/setup-config.ts
454
650
  function formatUnknownProvider(providerId, scope) {
@@ -471,16 +667,163 @@ async function loadScopedSetupConfig(io, local) {
471
667
  };
472
668
  }
473
669
  //#endregion
670
+ //#region src/cli/commands/config/models/benchmark-progress.ts
671
+ /** Keeps the benchmark preview live without mixing transient frames into the final stdout table. */
672
+ function createModelBenchmarkProgressDisplay(options) {
673
+ const frames = cliSpinners.dots.frames;
674
+ let frameIndex = 0;
675
+ let latest;
676
+ let previous;
677
+ let interval;
678
+ const render = () => {
679
+ if (latest === void 0) return;
680
+ const next = formatModelBenchmarkProgressFrame(options.config, latest, {
681
+ color: options.color,
682
+ frame: frames[frameIndex % frames.length] ?? "",
683
+ maxRows: options.output.rows,
684
+ tick: 0
685
+ });
686
+ if (previous === void 0 || !sameLayout(previous, next)) {
687
+ clearFrame(options.output, previous?.rows.length ?? 0);
688
+ options.output.write(`${next.rows.map((row) => `${row.prefix}${row.suffix}`).join("\n")}\r`);
689
+ previous = next;
690
+ return;
691
+ }
692
+ for (const [rowIndex, row] of next.rows.entries()) {
693
+ const previousRow = previous.rows[rowIndex];
694
+ if (previousRow !== void 0 && previousRow.suffix !== row.suffix) patchSuffix(options.output, rowIndex, next.rows.length, fastStringWidth(row.prefix), row.suffix);
695
+ }
696
+ previous = next;
697
+ };
698
+ const start = () => {
699
+ if (interval === void 0) {
700
+ interval = globalThis.setInterval(() => {
701
+ frameIndex += 1;
702
+ render();
703
+ }, cliSpinners.dots.interval);
704
+ interval.unref();
705
+ }
706
+ render();
707
+ };
708
+ /**
709
+ * Applies one core benchmark progress snapshot to the transient TTY table.
710
+ *
711
+ * Triggering workflow:
712
+ *
713
+ * `benchmarkModels` `onProgress`
714
+ * -> `config models list --with-speed`
715
+ * -> `ModelBenchmarkProgressDisplay.update`
716
+ *
717
+ * Upstream:
718
+ * - `benchmarkModels` `onProgress` callback registered by `runListModelsCommand`
719
+ *
720
+ * Downstream:
721
+ * - incremental benchmark table renderer `start` or `render`
722
+ */
723
+ const update = (progress) => {
724
+ latest = progress;
725
+ if (interval === void 0) {
726
+ start();
727
+ return;
728
+ }
729
+ render();
730
+ };
731
+ return {
732
+ finish: () => {
733
+ if (interval !== void 0) {
734
+ globalThis.clearInterval(interval);
735
+ interval = void 0;
736
+ }
737
+ clearFrame(options.output, previous?.rows.length ?? 0);
738
+ previous = void 0;
739
+ },
740
+ update
741
+ };
742
+ }
743
+ function clearFrame(output, rowCount) {
744
+ if (rowCount === 0) return;
745
+ let sequence = "\r\x1B[K";
746
+ for (let row = 1; row < rowCount; row += 1) sequence += "\r\x1B[1A\x1B[K";
747
+ output.write(sequence);
748
+ }
749
+ function patchSuffix(output, rowIndex, rowCount, column, suffix) {
750
+ const rowsUp = rowCount - rowIndex - 1;
751
+ const moveUp = rowsUp === 0 ? "" : `\x1B[${rowsUp}A`;
752
+ const moveDown = rowsUp === 0 ? "" : `\x1B[${rowsUp}B`;
753
+ const moveRight = column === 0 ? "" : `\x1B[${column}C`;
754
+ output.write(`\r${moveUp}${moveRight}\x1B[K${suffix}\r${moveDown}`);
755
+ }
756
+ function sameLayout(previous, next) {
757
+ return previous.rows.length === next.rows.length && previous.rows.every((row, index) => row.prefix === next.rows[index]?.prefix);
758
+ }
759
+ //#endregion
474
760
  //#region src/cli/commands/config/models/ls.ts
475
761
  const ls$1 = defineCommand({
476
- async action(context) {
477
- context.io.stdout.write(formatModelList(await loadMergedSetupConfig(context.io)));
478
- return 0;
479
- },
762
+ action: runListModelsCommand,
480
763
  alias: ["ls"],
481
764
  description: "List configured models",
482
- name: "list"
765
+ name: "list",
766
+ options: [{
767
+ description: "Benchmark configured models with live requests",
768
+ flags: "--with-speed"
769
+ }, {
770
+ description: "Override one provider benchmark concurrency (repeatable)",
771
+ flags: "--with-speed-concurrency <provider-id=limit>"
772
+ }]
483
773
  });
774
+ /**
775
+ * Lists configured models and optionally benchmarks each model with live streamed requests.
776
+ *
777
+ * Triggering workflow:
778
+ *
779
+ * {@link defineCommand}
780
+ * -> `config models list`
781
+ * -> {@link runListModelsCommand}
782
+ *
783
+ * Upstream:
784
+ * - `config models list` command dispatch
785
+ *
786
+ * Downstream:
787
+ * - {@link loadMergedSetupConfig}
788
+ * - {@link benchmarkModels}
789
+ * - {@link createModelBenchmarkProgressDisplay}
790
+ * - `context.io.stdout.write`
791
+ */
792
+ async function runListModelsCommand(context, options) {
793
+ const config = await loadMergedSetupConfig(context.io);
794
+ const concurrencyOverrides = options.withSpeedConcurrency === void 0 ? [] : [options.withSpeedConcurrency].flat();
795
+ if (options.withSpeed !== true) {
796
+ if (concurrencyOverrides.length > 0) {
797
+ context.io.stderr.write("--with-speed-concurrency requires --with-speed.\n");
798
+ return 2;
799
+ }
800
+ context.io.stdout.write(formatModelList(config));
801
+ return 0;
802
+ }
803
+ let providerConcurrency;
804
+ try {
805
+ providerConcurrency = resolveProviderBenchmarkConcurrency(config, concurrencyOverrides);
806
+ } catch (error) {
807
+ context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid benchmark concurrency override."}\n`);
808
+ return 2;
809
+ }
810
+ const progress = context.io.stderr.isTTY === true ? createModelBenchmarkProgressDisplay({
811
+ color: true,
812
+ config,
813
+ output: context.io.stderr
814
+ }) : void 0;
815
+ let benchmarks;
816
+ try {
817
+ benchmarks = await benchmarkModels(config, {
818
+ onProgress: (snapshot) => progress?.update(snapshot),
819
+ providerConcurrency
820
+ });
821
+ } finally {
822
+ progress?.finish();
823
+ }
824
+ context.io.stdout.write(formatModelBenchmarkList(config, benchmarks, { color: context.io.stdout.isTTY === true }));
825
+ return 0;
826
+ }
484
827
  //#endregion
485
828
  //#region src/cli/commands/config/probe.ts
486
829
  function providerHeadersFromOptions(options) {
@@ -1794,40 +2137,6 @@ function countDiagnostics(diagnostics, severity) {
1794
2137
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
1795
2138
  }
1796
2139
  //#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
2140
  //#region src/cli/reporters/progress/summary.ts
1832
2141
  const colors$2 = createColors({ force: true });
1833
2142
  const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
@@ -1896,6 +2205,7 @@ function createSummaryProgressReporter(options) {
1896
2205
  state: "running"
1897
2206
  });
1898
2207
  },
2208
+ onPlanningEnd: ({ progress }) => updateProgress(state, progress),
1899
2209
  onPrepareStart: ({ startedAt }) => {
1900
2210
  state.activeJobs.clear();
1901
2211
  state.errorCount = 0;
@@ -1995,11 +2305,12 @@ function createInitialState() {
1995
2305
  function createProgress() {
1996
2306
  return {
1997
2307
  execution: createCounts(),
2308
+ filesPlanned: 0,
1998
2309
  filesTotal: 0,
1999
- final: false,
2000
2310
  jobsCompleted: 0,
2001
2311
  jobsStarted: 0,
2002
- jobsTotal: 0
2312
+ jobsTotal: 0,
2313
+ planningComplete: false
2003
2314
  };
2004
2315
  }
2005
2316
  function createRows(state, options, now) {
@@ -2038,7 +2349,7 @@ function createRuleGroups(state) {
2038
2349
  }
2039
2350
  function estimateEta(durationMs, terminal, planned) {
2040
2351
  if (terminal <= 0 || planned <= 0 || durationMs <= 0) return "eta ?";
2041
- return `eta ~${formatDuration(durationMs * (planned - terminal) / terminal)}`;
2352
+ return `eta ~${formatDuration$1(durationMs * (planned - terminal) / terminal)}`;
2042
2353
  }
2043
2354
  function fitRow(row, columns) {
2044
2355
  if (columns <= 0) return "";
@@ -2092,18 +2403,23 @@ function formatContentRows(groups, state, options, now, contentBudget) {
2092
2403
  }
2093
2404
  return rows;
2094
2405
  }
2095
- function formatDuration(ms) {
2096
- return `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
2406
+ function formatDuration$1(ms) {
2407
+ const durationMs = Math.max(ms, 0);
2408
+ if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)}s`;
2409
+ return formatDuration(intervalToDuration({
2410
+ end: durationMs,
2411
+ start: 0
2412
+ }));
2097
2413
  }
2098
2414
  function formatFooters(state, options, now) {
2099
2415
  const terminal = terminalCount(state.execution);
2100
2416
  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)";
2417
+ const progressBar = state.progress.planningComplete ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
2418
+ const eta = state.progress.planningComplete ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
2419
+ const projectedTokens = state.progress.planningComplete && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
2420
+ const discovering = state.progress.planningComplete ? "" : " jobs (discovering)";
2105
2421
  return [
2106
- fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
2422
+ fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration$1(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
2107
2423
  fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
2108
2424
  fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
2109
2425
  ];
@@ -2134,7 +2450,7 @@ function formatTargetRow(jobState, options, now, last) {
2134
2450
  const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
2135
2451
  const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
2136
2452
  const startedAt = jobState.retry?.startedAt ?? jobState.startedAt ?? now;
2137
- return fitRow(`${last ? " └─" : " ├─"} ${inputPath} > ${target} ${jobState.retry ? `${jobState.retry.attempt}/${jobState.retry.maxAttempts} retrying elapsed ${formatDuration(now - startedAt)}` : `running ${formatDuration(now - startedAt)}`}`, options.columns);
2453
+ 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);
2138
2454
  }
2139
2455
  function styleRow(row, state, warnCount, errorCount, options) {
2140
2456
  let styledRow = row;
@@ -2289,6 +2605,7 @@ function createRenderingProgressReporter(summary, renderer) {
2289
2605
  //#endregion
2290
2606
  //#region src/cli/commands/lint/discovery.ts
2291
2607
  const minimatchOptions = { dot: true };
2608
+ const directoryBufferSize = 256;
2292
2609
  var NoFilesFoundError = class extends Error {
2293
2610
  globInputPaths;
2294
2611
  pattern;
@@ -2376,15 +2693,16 @@ async function resolveInputFiles(options) {
2376
2693
  })) continue;
2377
2694
  directories.push(input);
2378
2695
  if (!hasFilePatterns) continue;
2379
- const directoryFiles = (await walkFiles(path, {
2696
+ const directoryFiles = [];
2697
+ for await (const file of walkFiles(path, {
2380
2698
  cwd: options.cwd,
2381
2699
  gitignore: options.gitignore,
2382
2700
  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);
2701
+ })) {
2702
+ const relativePath = normalizeRelativePath(options.cwd, file);
2703
+ if (hasFilePatterns && matchesDiscoveryFile(relativePath, options.config, { cwd: options.cwd })) directoryFiles.push(relativePath);
2387
2704
  }
2705
+ for (const file of directoryFiles.sort()) candidates.push(file);
2388
2706
  continue;
2389
2707
  }
2390
2708
  if (options.globInputPaths && isGlobPattern(input)) {
@@ -2397,7 +2715,7 @@ async function resolveInputFiles(options) {
2397
2715
  pattern: input
2398
2716
  });
2399
2717
  if (matches.length === 0 && options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
2400
- candidates.push(...matches);
2718
+ for (const match of matches) candidates.push(match);
2401
2719
  continue;
2402
2720
  }
2403
2721
  if (options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
@@ -2412,20 +2730,19 @@ async function searchGlob(options) {
2412
2730
  const rootStats = await statPath(root);
2413
2731
  const pattern = normalizeGlobPattern(options.cwd, options.pattern);
2414
2732
  if (!rootStats?.isDirectory()) return [];
2415
- const files = (await walkFiles(root, {
2733
+ const candidates = [];
2734
+ for await (const file of walkFiles(root, {
2416
2735
  cwd: options.cwd,
2417
2736
  gitignore: options.gitignore,
2418
2737
  ignoredPatterns: options.ignoredPatterns
2419
- })).sort();
2420
- const candidates = [];
2421
- for (const file of files) {
2738
+ })) {
2422
2739
  const relativePath = normalizeRelativePath(options.cwd, file);
2423
2740
  if (matchesGlob(relativePath, pattern) && shouldIncludeGlobCandidate(relativePath, options.config, {
2424
2741
  cwd: options.cwd,
2425
2742
  hasFilePatterns: options.hasFilePatterns
2426
2743
  })) candidates.push(relativePath);
2427
2744
  }
2428
- return candidates;
2745
+ return candidates.sort();
2429
2746
  }
2430
2747
  function shouldFilterGitignoredFiles(config) {
2431
2748
  return normalizeConfig(config).some((item) => item.ignore?.gitignore === true);
@@ -2446,18 +2763,27 @@ async function statPath(path) {
2446
2763
  throw error;
2447
2764
  }
2448
2765
  }
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;
2766
+ /**
2767
+ * Streams directory entries so unmatched trees never accumulate into recursive child arrays.
2768
+ * The consumer owns result ordering because filesystem iteration order is not stable.
2769
+ */
2770
+ async function* walkFiles(root, options) {
2771
+ const pendingDirectories = [root];
2772
+ while (pendingDirectories.length > 0) {
2773
+ const currentDirectory = pendingDirectories.pop();
2774
+ if (currentDirectory === void 0) continue;
2775
+ const childDirectories = [];
2776
+ const directory = await opendir(currentDirectory, { bufferSize: directoryBufferSize });
2777
+ for await (const entry of directory) {
2778
+ const path = resolve(currentDirectory, entry.name);
2779
+ if (entry.isDirectory()) {
2780
+ if (!await shouldPruneDirectory(path, options)) childDirectories.push(path);
2781
+ continue;
2782
+ }
2783
+ if (entry.isFile()) yield path;
2457
2784
  }
2458
- if (entry.isFile()) files.push(path);
2785
+ for (const path of childDirectories.reverse()) pendingDirectories.push(path);
2459
2786
  }
2460
- return files;
2461
2787
  }
2462
2788
  //#endregion
2463
2789
  //#region src/cli/commands/lint/errors.ts
@@ -3007,6 +3333,9 @@ function mergeProgressReporters(base, extra) {
3007
3333
  onJobStart: (payload) => {
3008
3334
  deliverBoth((reporter) => reporter.onJobStart?.(payload));
3009
3335
  },
3336
+ onPlanningEnd: (payload) => {
3337
+ deliverBoth((reporter) => reporter.onPlanningEnd?.(payload));
3338
+ },
3010
3339
  onPrepareEnd: (payload) => {
3011
3340
  deliverBoth((reporter) => reporter.onPrepareEnd?.(payload));
3012
3341
  },
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-3Ov9-ZVf.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.2.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -20,6 +20,7 @@
20
20
  "@moeru/std": "^0.1.0-beta.20",
21
21
  "cac": "^7.0.0",
22
22
  "cli-spinners": "^3.4.0",
23
+ "date-fns": "^4.4.0",
23
24
  "fast-string-truncated-width": "^3.0.3",
24
25
  "fast-string-width": "^3.0.2",
25
26
  "gitignore-fs": "^2.2.3",
@@ -27,8 +28,8 @@
27
28
  "pathe": "^2.0.3",
28
29
  "table": "^6.9.0",
29
30
  "tinyrainbow": "^3.1.0",
30
- "@alint-js/config": "0.1.4",
31
- "@alint-js/core": "0.1.4"
31
+ "@alint-js/config": "0.2.0",
32
+ "@alint-js/core": "0.2.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@pnpm/find-workspace-dir": "^1000.1.5",
@@ -38,6 +39,7 @@
38
39
  },
39
40
  "scripts": {
40
41
  "alint": "node ./dist/bin/index.mjs",
42
+ "bench": "vitest bench --config vitest.config.ts",
41
43
  "build": "tsdown",
42
44
  "build:binary": "tsdown --config tsdown.binary.config.ts --config-loader unrun",
43
45
  "typecheck": "tsc -p tsconfig.json --noEmit",