@alint-js/cli 0.0.1 → 0.0.4

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.
@@ -0,0 +1,1099 @@
1
+ import process from "node:process";
2
+ import { stat } from "node:fs/promises";
3
+ import { inspect } from "node:util";
4
+ import Gitignore from "gitignore-fs";
5
+ import c, { createColors } from "tinyrainbow";
6
+ import { getGlobalSetupConfigPath, getProjectSetupConfigPath, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
7
+ import { AlintRunError, runAlint } from "@alint-js/core";
8
+ import { errorMessageFrom } from "@moeru/std/error";
9
+ import { cac } from "cac";
10
+ import { resolve } from "pathe";
11
+ import { getBorderCharacters, table } from "table";
12
+ import cliSpinners from "cli-spinners";
13
+ import { relative } from "node:path";
14
+ //#region src/cli/provider-registry.ts
15
+ function buildModelsUrl(endpoint) {
16
+ return new URL("models", endpoint.endsWith("/") ? endpoint : `${endpoint}/`).toString();
17
+ }
18
+ function createProviderId(endpoint, existingIds) {
19
+ let base = "provider";
20
+ try {
21
+ base = new URL(endpoint).hostname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider";
22
+ } catch {
23
+ base = "provider";
24
+ }
25
+ if (!existingIds.has(base)) return base;
26
+ for (let index = 2;; index += 1) {
27
+ const candidate = `${base}-${index}`;
28
+ if (!existingIds.has(candidate)) return candidate;
29
+ }
30
+ }
31
+ function findModel(config, request) {
32
+ return flattenModels(config).find(({ model }) => model.id === request || model.name === request || (model.aliases ?? []).includes(request));
33
+ }
34
+ function flattenModels(config) {
35
+ return config.providers.flatMap((provider) => provider.models.map((model) => ({
36
+ model,
37
+ provider
38
+ })));
39
+ }
40
+ function formatModelList(config) {
41
+ return formatTable([[
42
+ "id",
43
+ "provider",
44
+ "name"
45
+ ], ...flattenModels(config).map(({ model, provider }) => [
46
+ model.id,
47
+ provider.id,
48
+ model.name ?? model.id
49
+ ])]);
50
+ }
51
+ function formatModelShow(candidate) {
52
+ const { model, provider } = candidate;
53
+ const lines = [
54
+ `id: ${model.id}`,
55
+ `name: ${model.name ?? model.id}`,
56
+ `provider: ${provider.id}`,
57
+ `endpoint: ${provider.endpoint}`
58
+ ];
59
+ if (model.aliases?.length) lines.push(`aliases: ${model.aliases.join(", ")}`);
60
+ if (model.capabilities?.length) lines.push(`capabilities: ${model.capabilities.join(", ")}`);
61
+ if (model.size !== void 0) lines.push(`size: ${model.size}`);
62
+ if (model.contextWindow !== void 0) lines.push(`contextWindow: ${model.contextWindow}`);
63
+ if (model.defaultParams !== void 0) lines.push(`defaultParams: ${JSON.stringify(model.defaultParams)}`);
64
+ return `${lines.join("\n")}\n`;
65
+ }
66
+ function formatProviderList(config) {
67
+ return formatTable([[
68
+ "id",
69
+ "type",
70
+ "endpoint",
71
+ "models"
72
+ ], ...config.providers.map((provider) => [
73
+ provider.id,
74
+ provider.type,
75
+ provider.endpoint,
76
+ String(provider.models.length)
77
+ ])]);
78
+ }
79
+ function formatProviderShow(provider) {
80
+ const lines = [
81
+ `id: ${provider.id}`,
82
+ `type: ${provider.type}`,
83
+ `endpoint: ${provider.endpoint}`,
84
+ `models: ${provider.models.map((model) => model.id).join(", ")}`
85
+ ];
86
+ const headerKeys = Object.keys(provider.headers ?? {});
87
+ if (headerKeys.length > 0) lines.push(`headers: ${headerKeys.join(", ")}`);
88
+ return `${lines.join("\n")}\n`;
89
+ }
90
+ function parseHeaderList(headers) {
91
+ if (headers.length === 0) return;
92
+ const parsedHeaders = {};
93
+ for (const header of headers) {
94
+ const separatorIndex = header.indexOf("=");
95
+ if (separatorIndex <= 0) throw new Error(`Invalid provider header "${header}". Expected Key=Value.`);
96
+ parsedHeaders[header.slice(0, separatorIndex)] = header.slice(separatorIndex + 1);
97
+ }
98
+ return parsedHeaders;
99
+ }
100
+ async function probeModels(endpoint, headers = {}) {
101
+ const response = await fetch(buildModelsUrl(endpoint), { headers });
102
+ if (!response.ok) throw new Error(`GET ${buildModelsUrl(endpoint)} returned ${response.status}.`);
103
+ const body = await response.json();
104
+ if (!Array.isArray(body.data)) throw new TypeError("Expected OpenAI-compatible models response with data array.");
105
+ return body.data.map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0);
106
+ }
107
+ function formatTable(rows) {
108
+ if (rows.length <= 1) return "";
109
+ return table(rows, {
110
+ border: getBorderCharacters("void"),
111
+ columnDefault: {
112
+ paddingLeft: 0,
113
+ paddingRight: 2
114
+ },
115
+ drawHorizontalLine: () => false
116
+ });
117
+ }
118
+ //#endregion
119
+ //#region src/cli/commands/setup/interactive.ts
120
+ const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
121
+ const backValue = "__alint_back__";
122
+ function formatProbeModelsFailure(endpoint, error) {
123
+ const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
124
+ return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
125
+ }
126
+ function isBackInput(value) {
127
+ return value.trim() === "..";
128
+ }
129
+ async function runInteractiveSetup(io) {
130
+ if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
131
+ io.stderr.write(nonTtyMessage);
132
+ return 2;
133
+ }
134
+ const prompts = await import("@clack/prompts");
135
+ const cancelPrompt = () => {
136
+ prompts.cancel("Setup cancelled.");
137
+ return 1;
138
+ };
139
+ prompts.intro("alint setup");
140
+ const draft = {};
141
+ let step = "scope";
142
+ while (true) {
143
+ if (step === "scope") {
144
+ const scope = await prompts.select({
145
+ message: "Where should alint write setup config?",
146
+ options: [{
147
+ label: "Global",
148
+ value: "global"
149
+ }, {
150
+ label: "Local project",
151
+ value: "local"
152
+ }]
153
+ });
154
+ if (prompts.isCancel(scope)) return cancelPrompt();
155
+ draft.scope = scope;
156
+ step = "source";
157
+ continue;
158
+ }
159
+ if (step === "source") {
160
+ const source = await prompts.select({
161
+ message: "Choose provider setup mode.",
162
+ options: withBackOption([
163
+ {
164
+ label: "Custom OpenAI-compatible provider",
165
+ value: "custom"
166
+ },
167
+ {
168
+ label: "Ollama",
169
+ value: "ollama"
170
+ },
171
+ {
172
+ label: "Manual model entry",
173
+ value: "manual"
174
+ }
175
+ ])
176
+ });
177
+ if (prompts.isCancel(source)) return cancelPrompt();
178
+ if (source === backValue) {
179
+ step = "scope";
180
+ continue;
181
+ }
182
+ draft.source = source;
183
+ step = "endpoint";
184
+ continue;
185
+ }
186
+ if (step === "endpoint") {
187
+ const endpoint = await promptEndpoint(prompts, draft.source ?? "custom");
188
+ if (prompts.isCancel(endpoint)) return cancelPrompt();
189
+ if (typeof endpoint !== "string") return cancelPrompt();
190
+ if (isBackInput(endpoint)) {
191
+ step = "source";
192
+ continue;
193
+ }
194
+ draft.endpoint = endpoint;
195
+ step = "providerId";
196
+ continue;
197
+ }
198
+ if (step === "providerId") {
199
+ const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
200
+ const providerId = await prompts.text({
201
+ defaultValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
202
+ message: "Provider id",
203
+ placeholder: "Type .. to go back",
204
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
205
+ });
206
+ if (prompts.isCancel(providerId)) return cancelPrompt();
207
+ if (typeof providerId !== "string") return cancelPrompt();
208
+ if (isBackInput(providerId)) {
209
+ step = "endpoint";
210
+ continue;
211
+ }
212
+ draft.providerId = providerId;
213
+ step = "headers";
214
+ continue;
215
+ }
216
+ if (step === "headers") {
217
+ const headerInput = await prompts.text({
218
+ defaultValue: draft.headerInput ?? "",
219
+ message: "Headers",
220
+ placeholder: "Authorization=Bearer token, X-Test=true; type .. to go back",
221
+ validate: (value) => {
222
+ if (isBackInput(value ?? "")) return;
223
+ try {
224
+ parseHeaderList(splitHeaderInput(value ?? ""));
225
+ return;
226
+ } catch {
227
+ return "Headers must be comma-separated Key=Value entries.";
228
+ }
229
+ }
230
+ });
231
+ if (prompts.isCancel(headerInput)) return cancelPrompt();
232
+ if (typeof headerInput !== "string") return cancelPrompt();
233
+ if (isBackInput(headerInput)) {
234
+ step = "providerId";
235
+ continue;
236
+ }
237
+ draft.headerInput = headerInput;
238
+ draft.headers = parseHeaderList(splitHeaderInput(headerInput));
239
+ draft.discoveredModels = draft.source === "manual" ? [] : await probeModelsWithSpinner(prompts, draft.endpoint ?? "", draft.headers);
240
+ step = "models";
241
+ continue;
242
+ }
243
+ if (step === "models") {
244
+ const selectedModels = await promptModels(prompts, draft.discoveredModels ?? []);
245
+ if (prompts.isCancel(selectedModels)) return cancelPrompt();
246
+ if (selectedModels === backValue) {
247
+ step = "headers";
248
+ continue;
249
+ }
250
+ if (!Array.isArray(selectedModels)) return cancelPrompt();
251
+ draft.selectedModels = selectedModels;
252
+ step = "defaultAlias";
253
+ continue;
254
+ }
255
+ if (step === "defaultAlias") {
256
+ const addDefaultAlias = await prompts.select({
257
+ message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
258
+ options: withBackOption([{
259
+ label: "Yes",
260
+ value: "yes"
261
+ }, {
262
+ label: "No",
263
+ value: "no"
264
+ }])
265
+ });
266
+ if (prompts.isCancel(addDefaultAlias)) return cancelPrompt();
267
+ if (addDefaultAlias === backValue) {
268
+ step = "models";
269
+ continue;
270
+ }
271
+ draft.addDefaultAlias = addDefaultAlias === "yes";
272
+ step = "confirm";
273
+ continue;
274
+ }
275
+ const nextProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? [], draft.addDefaultAlias ?? true);
276
+ const confirmed = await prompts.select({
277
+ message: [
278
+ `Write ${draft.scope} setup config?`,
279
+ `Provider: ${nextProvider.id}`,
280
+ `Endpoint: ${nextProvider.endpoint}`,
281
+ `Models: ${(draft.selectedModels ?? []).join(", ")}`
282
+ ].join("\n"),
283
+ options: withBackOption([{
284
+ label: "Yes",
285
+ value: "yes"
286
+ }, {
287
+ label: "No",
288
+ value: "no"
289
+ }])
290
+ });
291
+ if (prompts.isCancel(confirmed)) return cancelPrompt();
292
+ if (confirmed === backValue) {
293
+ step = "defaultAlias";
294
+ continue;
295
+ }
296
+ if (confirmed === "no") return cancelPrompt();
297
+ const configPath = getConfigPath(io, draft.scope ?? "global");
298
+ await writeSetupConfig(configPath, mergeSetupConfigs(await loadSetupConfig(configPath), {
299
+ providers: [nextProvider],
300
+ version: 1
301
+ }));
302
+ prompts.outro(`Wrote ${configPath}`);
303
+ return 0;
304
+ }
305
+ }
306
+ function withBackOption(options) {
307
+ return [...options, {
308
+ label: "Back",
309
+ value: backValue
310
+ }];
311
+ }
312
+ function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
313
+ return {
314
+ endpoint,
315
+ headers,
316
+ id: providerId,
317
+ models: modelIds.map((modelId, index) => ({
318
+ aliases: index === 0 && addDefaultAlias ? ["default"] : void 0,
319
+ id: modelId,
320
+ name: modelId
321
+ })),
322
+ type: "openai-compatible"
323
+ };
324
+ }
325
+ function getConfigPath(io, scope) {
326
+ return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
327
+ }
328
+ async function probeModelsWithSpinner(prompts, endpoint, headers) {
329
+ const spinner = prompts.spinner();
330
+ spinner.start("Probing models");
331
+ try {
332
+ const models = await probeModels(endpoint, headers ?? {});
333
+ spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
334
+ return models;
335
+ } catch (error) {
336
+ spinner.stop(formatProbeModelsFailure(endpoint, error));
337
+ return [];
338
+ }
339
+ }
340
+ async function promptEndpoint(prompts, source) {
341
+ return prompts.text({
342
+ defaultValue: source === "ollama" ? "http://localhost:11434/v1" : void 0,
343
+ message: "Provider endpoint",
344
+ placeholder: source === "ollama" ? "http://localhost:11434/v1; type .. to go back" : "https://example.test/v1; type .. to go back",
345
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
346
+ });
347
+ }
348
+ async function promptModels(prompts, discoveredModels) {
349
+ if (discoveredModels.length > 0) {
350
+ const selectedModels = await prompts.multiselect({
351
+ message: "Select models",
352
+ options: withBackOption(discoveredModels.map((model) => ({
353
+ label: model,
354
+ value: model
355
+ }))),
356
+ required: true
357
+ });
358
+ return Array.isArray(selectedModels) && selectedModels.includes(backValue) ? backValue : selectedModels;
359
+ }
360
+ const modelInput = await prompts.text({
361
+ message: "Models",
362
+ placeholder: "qwen:8b, qwen:32b; type .. to go back",
363
+ validate: (value) => isBackInput(value ?? "") || splitModelInput(value ?? "").length > 0 ? void 0 : "At least one model is required."
364
+ });
365
+ if (prompts.isCancel(modelInput)) return modelInput;
366
+ return isBackInput(modelInput) ? backValue : splitModelInput(modelInput);
367
+ }
368
+ function splitHeaderInput(value) {
369
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
370
+ }
371
+ function splitModelInput(value) {
372
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
373
+ }
374
+ //#endregion
375
+ //#region src/cli/commands/setup/index.ts
376
+ async function runSetupCommand(options, io) {
377
+ if (!options.providerEndpoint) {
378
+ if (options.noInteractive !== true) return runInteractiveSetup({
379
+ ...io,
380
+ stdin: io.stdin ?? process.stdin
381
+ });
382
+ io.stderr.write("setup requires --provider-endpoint in --no-interactive mode.\n");
383
+ return 2;
384
+ }
385
+ if (!options.providerId) {
386
+ io.stderr.write("setup requires --provider-id in --no-interactive mode.\n");
387
+ return 2;
388
+ }
389
+ const setupConfigPath = options.local ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
390
+ await writeSetupConfig(setupConfigPath, mergeSetupConfigs(await loadSetupConfig(setupConfigPath), createSetupConfig(options.providerId, options.providerEndpoint, options)));
391
+ return 0;
392
+ }
393
+ function createSetupConfig(providerId, providerEndpoint, options) {
394
+ const models = toArray$1(options.providerModel).map((model) => ({
395
+ id: model,
396
+ name: model
397
+ }));
398
+ return {
399
+ providers: [{
400
+ endpoint: providerEndpoint,
401
+ headers: parseHeaderList(toArray$1(options.providerHeader)),
402
+ id: providerId,
403
+ models,
404
+ type: "openai-compatible"
405
+ }],
406
+ version: 1
407
+ };
408
+ }
409
+ function toArray$1(value) {
410
+ if (value === void 0) return [];
411
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
412
+ }
413
+ //#endregion
414
+ //#region src/cli/reporters/json.ts
415
+ function formatJson(result) {
416
+ return `${JSON.stringify(result, null, 2)}\n`;
417
+ }
418
+ //#endregion
419
+ //#region src/cli/reporters/stylish.ts
420
+ const colors$1 = createColors({ force: true });
421
+ function formatStylish(input, options = {}) {
422
+ const diagnostics = Array.isArray(input) ? input : input.diagnostics;
423
+ const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
424
+ if (diagnostics.length === 0) return "";
425
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
426
+ for (const diagnostic of diagnostics) {
427
+ const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
428
+ if (fileDiagnostics) {
429
+ fileDiagnostics.push(diagnostic);
430
+ continue;
431
+ }
432
+ diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
433
+ }
434
+ const lines = [];
435
+ const style = createStyle(options.color === true);
436
+ for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
437
+ lines.push(style.file(filePath));
438
+ for (const diagnostic of fileDiagnostics) {
439
+ const line = diagnostic.loc?.start.line ?? 0;
440
+ const column = diagnostic.loc?.start.column ?? 0;
441
+ const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
442
+ lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
443
+ }
444
+ lines.push("");
445
+ }
446
+ lines.push("", formatSummary(diagnostics, totalTokens, style));
447
+ return `${lines.join("\n")}\n`;
448
+ }
449
+ function countDiagnostics$2(diagnostics, severity) {
450
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
451
+ }
452
+ function createStyle(color) {
453
+ if (!color) return {
454
+ error: identity,
455
+ file: identity,
456
+ location: identity,
457
+ ruleId: identity,
458
+ summaryToken: identity,
459
+ warning: identity
460
+ };
461
+ return {
462
+ error: colors$1.red,
463
+ file: colors$1.underline,
464
+ location: colors$1.dim,
465
+ ruleId: colors$1.dim,
466
+ summaryToken: colors$1.cyan,
467
+ warning: colors$1.yellow
468
+ };
469
+ }
470
+ function formatSummary(diagnostics, totalTokens, style) {
471
+ const warnCount = countDiagnostics$2(diagnostics, "warn");
472
+ const errorCount = countDiagnostics$2(diagnostics, "error");
473
+ const tokens = totalTokens === void 0 ? void 0 : `${totalTokens.toLocaleString("en-US")} tokens`;
474
+ const problemSummary = [style.warning(`${warnCount} warn`), style.error(`${errorCount} error`)].join(" / ");
475
+ if (tokens === void 0) return problemSummary;
476
+ return `${problemSummary} | ${style.summaryToken(tokens)}`;
477
+ }
478
+ function identity(value) {
479
+ return value;
480
+ }
481
+ //#endregion
482
+ //#region src/cli/reporters/index.ts
483
+ function formatDiagnostics(format, result, options = {}) {
484
+ if (format === "json") return formatJson(result);
485
+ if (format === "stylish") return formatStylish(result, { color: options.color });
486
+ throw new Error(`Unknown reporter "${format}".`);
487
+ }
488
+ //#endregion
489
+ //#region src/cli/reporters/progress/plain.ts
490
+ function createPlainProgressReporter(options) {
491
+ const writeLine = (line) => options.write(`${line}\n`);
492
+ return {
493
+ onRuleStart: (payload) => {
494
+ const target = payload.path.target.name ? `${payload.path.target.kind} ${payload.path.target.name}` : payload.path.target.kind;
495
+ writeLine(`scan ${payload.path.file.path} > ${target} > ${payload.path.rule.id}`);
496
+ },
497
+ onRunEnd: (payload) => {
498
+ const warnCount = countDiagnostics$1(payload.diagnostics, "warn");
499
+ const errorCount = countDiagnostics$1(payload.diagnostics, "error");
500
+ const state = payload.errored > 0 ? "failed" : "finished";
501
+ const cached = payload.cached > 0 ? `, ${payload.cached} cached` : "";
502
+ const errored = payload.errored > 0 ? `, ${payload.errored} errored` : "";
503
+ writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens${cached}${errored}`);
504
+ },
505
+ onRunStart: (payload) => {
506
+ writeLine(`alint started: ${payload.filesTotal} files, ${payload.rulesTotal} rules, ${payload.planned} planned executions`);
507
+ }
508
+ };
509
+ }
510
+ function countDiagnostics$1(diagnostics, severity) {
511
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
512
+ }
513
+ //#endregion
514
+ //#region src/cli/reporters/progress/summary.ts
515
+ const colors = createColors({ force: true });
516
+ function createSummaryProgressReporter(options) {
517
+ const now = () => options.clock?.() ?? Date.now();
518
+ const state = {
519
+ cached: 0,
520
+ completed: 0,
521
+ diagnostics: [],
522
+ errored: 0,
523
+ files: /* @__PURE__ */ new Map(),
524
+ planned: 0,
525
+ spinnerIndex: 0,
526
+ totalTokens: 0
527
+ };
528
+ return {
529
+ getRows: () => createRows(state, options, now()),
530
+ onDiagnostic: (payload) => {
531
+ state.diagnostics = payload.diagnostics;
532
+ },
533
+ onFileEnd: (payload) => {
534
+ const file = getFileState(state, payload.file);
535
+ file.endedAt = payload.endedAt ?? now();
536
+ file.rule = void 0;
537
+ file.target = void 0;
538
+ },
539
+ onFileStart: (payload) => {
540
+ const file = getFileState(state, payload.file);
541
+ file.startedAt = payload.startedAt ?? now();
542
+ file.endedAt = void 0;
543
+ },
544
+ onRuleEnd: (payload) => {
545
+ const file = getFileState(state, payload.path.file);
546
+ if (payload.cache === "hit") {
547
+ state.cached += 1;
548
+ file.cached += 1;
549
+ }
550
+ if (payload.state === "completed") {
551
+ state.completed += 1;
552
+ file.completed += 1;
553
+ }
554
+ if (payload.state === "errored") {
555
+ state.errored += 1;
556
+ file.errored += 1;
557
+ }
558
+ if (file.rule?.id === payload.path.rule.id) file.rule = void 0;
559
+ },
560
+ onRuleStart: (payload) => {
561
+ const file = getFileState(state, payload.path.file);
562
+ file.startedAt ??= payload.startedAt ?? now();
563
+ file.rule = {
564
+ id: payload.path.rule.id,
565
+ startedAt: payload.startedAt ?? now(),
566
+ target: formatTarget(payload)
567
+ };
568
+ file.target = file.rule.target;
569
+ },
570
+ onRunEnd: (payload) => {
571
+ state.cached = payload.cached;
572
+ state.completed = payload.completed;
573
+ state.diagnostics = payload.diagnostics;
574
+ state.endedAt = payload.endedAt ?? now();
575
+ state.errored = payload.errored;
576
+ state.planned = payload.planned;
577
+ state.runStartedAt = payload.startedAt ?? state.runStartedAt;
578
+ state.totalTokens = payload.usage.totalTokens;
579
+ },
580
+ onRunStart: (payload) => {
581
+ state.cached = 0;
582
+ state.completed = 0;
583
+ state.diagnostics = [];
584
+ state.endedAt = void 0;
585
+ state.errored = 0;
586
+ state.files = /* @__PURE__ */ new Map();
587
+ state.planned = payload.planned;
588
+ state.runStartedAt = payload.startedAt ?? now();
589
+ state.spinnerIndex = 0;
590
+ state.totalTokens = 0;
591
+ for (const file of payload.files ?? []) state.files.set(file.path, createFileState(file));
592
+ },
593
+ onTargetEnd: (payload) => {
594
+ const file = getFileState(state, payload.path.file);
595
+ const target = formatTarget(payload);
596
+ if (file.target === target) file.target = void 0;
597
+ },
598
+ onTargetStart: (payload) => {
599
+ const file = getFileState(state, payload.path.file);
600
+ file.startedAt ??= payload.startedAt ?? now();
601
+ file.target = formatTarget(payload);
602
+ },
603
+ onUsage: (payload) => {
604
+ state.totalTokens = payload.total.totalTokens;
605
+ },
606
+ tick: () => {
607
+ state.spinnerIndex = (state.spinnerIndex + 1) % Math.max(options.spinnerFrames.length, 1);
608
+ }
609
+ };
610
+ }
611
+ function countDiagnostics(diagnostics, severity) {
612
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
613
+ }
614
+ function countQueuedFiles(state) {
615
+ return [...state.files.values()].filter((file) => file.startedAt === void 0 && file.endedAt === void 0 && (file.file.planned ?? 0) > 0).length;
616
+ }
617
+ function createFileState(file) {
618
+ return {
619
+ cached: 0,
620
+ completed: 0,
621
+ errored: 0,
622
+ file
623
+ };
624
+ }
625
+ function createRows(state, options, now) {
626
+ const rows = getActiveFiles(state).flatMap((file) => formatFileRows(file, state, options, now));
627
+ const queued = countQueuedFiles(state);
628
+ const warnCount = countDiagnostics(state.diagnostics, "warn");
629
+ const errorCount = countDiagnostics(state.diagnostics, "error");
630
+ const footer = formatFooter(state, warnCount, errorCount, queued, options, now);
631
+ if (queued > 0) rows.push(formatQueuedRow(queued, options));
632
+ if (rows.length === 0) rows.push(formatIdleRow(state, options));
633
+ rows.push("", footer);
634
+ return options.color ? rows.map((row) => styleRow(row, state, warnCount, errorCount, options)) : rows;
635
+ }
636
+ function escapeRegExp(value) {
637
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
638
+ }
639
+ function estimateTotal(elapsedMs, completed, planned) {
640
+ if (completed <= 0 || planned <= 0) return;
641
+ return elapsedMs * planned / completed;
642
+ }
643
+ function fitRow(row, columns) {
644
+ if (columns <= 0) return "";
645
+ if (row.length <= columns) return row;
646
+ if (columns === 1) return "…";
647
+ return `${row.slice(0, columns - 1)}…`;
648
+ }
649
+ function formatDuration(ms) {
650
+ if (ms === void 0 || !Number.isFinite(ms)) return "?";
651
+ return `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
652
+ }
653
+ function formatEstimatedDuration(ms) {
654
+ return `~${formatDuration(ms)}`;
655
+ }
656
+ function formatFilePath(filePath, cwd) {
657
+ if (!cwd) return filePath;
658
+ return relative(cwd, filePath) || filePath;
659
+ }
660
+ function formatFileRows(file, state, options, now) {
661
+ const firstRow = formatFileSummaryRow(file, state, options);
662
+ if (!file.rule) return [firstRow];
663
+ const elapsed = now - file.rule.startedAt;
664
+ const done = file.completed + file.cached + file.errored;
665
+ const estimated = estimateTotal(file.startedAt === void 0 ? elapsed : now - file.startedAt, done, file.file.planned ?? 0);
666
+ return [firstRow, fitRow(` ${file.rule.target} > ${file.rule.id} (${formatDuration(elapsed)}, ${formatEstimatedDuration(estimated)})`, options.columns)];
667
+ }
668
+ function formatFileSummaryRow(file, state, options) {
669
+ const prefix = `${options.spinnerFrames[state.spinnerIndex] ?? ""} ${formatFilePath(file.file.path, options.cwd)}`;
670
+ const counter = `${file.completed}/${file.cached}/${file.errored}/${file.file.planned ?? 0}`;
671
+ return fitRow(`${prefix}${" ".repeat(Math.max(1, options.columns - prefix.length - counter.length))}${counter}`, options.columns);
672
+ }
673
+ function formatFooter(state, warnCount, errorCount, queued, options, now) {
674
+ const endedAt = state.endedAt ?? now;
675
+ const elapsed = state.runStartedAt === void 0 ? void 0 : endedAt - state.runStartedAt;
676
+ const completed = state.completed + state.cached + state.errored;
677
+ const estimated = elapsed === void 0 ? void 0 : estimateTotal(elapsed, completed, state.planned);
678
+ const estimatedTokens = completed > 0 && state.planned > 0 ? Math.ceil(state.totalTokens * state.planned / completed).toLocaleString("en-US") : "?";
679
+ return fitRow([
680
+ `${formatDuration(elapsed)} -> ${formatEstimatedDuration(estimated)}`,
681
+ `${state.totalTokens.toLocaleString("en-US")} tokens -> ~${estimatedTokens} tokens`,
682
+ `${queued} queued / ${state.cached} cached / ${warnCount} warn / ${errorCount} error`
683
+ ].join(" | "), options.columns);
684
+ }
685
+ function formatIdleRow(state, options) {
686
+ const prefix = `${options.spinnerFrames[state.spinnerIndex] ?? ""} alint`;
687
+ const counter = `${state.completed}/${state.cached}/${state.errored}/${state.planned}`;
688
+ return fitRow(`${prefix}${" ".repeat(Math.max(1, options.columns - prefix.length - counter.length))}${counter}`, options.columns);
689
+ }
690
+ function formatQueuedRow(queued, options) {
691
+ return fitRow(` ${queued} ${queued === 1 ? "file" : "files"} queued`, options.columns);
692
+ }
693
+ function formatTarget(payload) {
694
+ return payload.path.target.name ? `${payload.path.target.kind} ${payload.path.target.name}` : payload.path.target.kind;
695
+ }
696
+ function getActiveFiles(state) {
697
+ return [...state.files.values()].filter((file) => file.startedAt !== void 0 && file.endedAt === void 0).sort((left, right) => left.file.index - right.file.index);
698
+ }
699
+ function getFileState(state, file) {
700
+ const existingFile = state.files.get(file.path);
701
+ if (existingFile) {
702
+ existingFile.file = {
703
+ ...existingFile.file,
704
+ ...file,
705
+ planned: file.planned ?? existingFile.file.planned
706
+ };
707
+ return existingFile;
708
+ }
709
+ const nextFile = createFileState({
710
+ ...file,
711
+ planned: file.planned ?? (state.files.size === 0 ? state.planned : void 0)
712
+ });
713
+ state.files.set(file.path, nextFile);
714
+ return nextFile;
715
+ }
716
+ function replaceFirst(row, search, replacement) {
717
+ if (search.length === 0) return row;
718
+ return row.replace(new RegExp(escapeRegExp(search)), replacement);
719
+ }
720
+ function styleRow(row, state, warnCount, errorCount, options) {
721
+ let styledRow = row;
722
+ const spinner = options.spinnerFrames[state.spinnerIndex] ?? "";
723
+ if (spinner) styledRow = replaceFirst(styledRow, spinner, colors.cyan(spinner));
724
+ styledRow = styledRow.replace(/\|/g, colors.gray("|")).replace(`${warnCount} warn`, colors.yellow(`${warnCount} warn`)).replace(`${errorCount} error`, (errorCount > 0 ? colors.red : colors.gray)(`${errorCount} error`)).replace(/(\d+\/\d+\/\d+\/\d+)/, (match) => state.errored > 0 ? colors.red(match) : colors.gray(match)).replace(/(\d[\d,]* tokens)/g, (match) => colors.cyan(match));
725
+ return styledRow;
726
+ }
727
+ //#endregion
728
+ //#region src/cli/reporters/progress/tty.ts
729
+ const clearCurrentLine = "\r\x1B[K";
730
+ const clearPreviousLine = "\r\x1B[1A\x1B[K";
731
+ function createTtyProgressRenderer(options) {
732
+ let interval;
733
+ let previousRows = 0;
734
+ const clearPreviousFrame = () => {
735
+ if (previousRows === 0) return;
736
+ let sequence = clearCurrentLine;
737
+ for (let row = 1; row < previousRows; row += 1) sequence += clearPreviousLine;
738
+ options.write(sequence);
739
+ previousRows = 0;
740
+ };
741
+ const render = () => {
742
+ clearPreviousFrame();
743
+ const rows = options.getRows();
744
+ if (rows.length === 0) return;
745
+ options.write(rows.join("\n"));
746
+ previousRows = rows.length;
747
+ };
748
+ const write = (chunk) => {
749
+ const wasRendering = interval !== void 0;
750
+ clearPreviousFrame();
751
+ options.write(chunk);
752
+ if (wasRendering) render();
753
+ };
754
+ return {
755
+ finish: () => {
756
+ if (interval) {
757
+ options.clearInterval(interval);
758
+ interval = void 0;
759
+ }
760
+ clearPreviousFrame();
761
+ },
762
+ render,
763
+ start: () => {
764
+ if (!interval) {
765
+ interval = options.createInterval(render, options.intervalMs);
766
+ if (isUnrefableInterval(interval)) interval.unref();
767
+ }
768
+ render();
769
+ },
770
+ write
771
+ };
772
+ }
773
+ function isUnrefableInterval(interval) {
774
+ if (typeof interval !== "object" || interval === null || !("unref" in interval)) return false;
775
+ return typeof interval.unref === "function";
776
+ }
777
+ //#endregion
778
+ //#region src/cli/reporters/progress/index.ts
779
+ function createCliProgressReporter(options) {
780
+ if (!options.isTty) return {
781
+ dispose: () => {},
782
+ reporter: createPlainProgressReporter({ write: options.write }),
783
+ write: options.write
784
+ };
785
+ const summary = createSummaryProgressReporter({
786
+ color: options.color,
787
+ columns: options.columns,
788
+ cwd: options.cwd,
789
+ spinnerFrames: cliSpinners.dots.frames
790
+ });
791
+ const renderer = createTtyProgressRenderer({
792
+ clearInterval: (handle) => globalThis.clearInterval(handle),
793
+ createInterval: (callback, intervalMs) => globalThis.setInterval(() => {
794
+ summary.tick();
795
+ callback();
796
+ }, intervalMs),
797
+ getRows: summary.getRows,
798
+ intervalMs: 120,
799
+ write: options.write
800
+ });
801
+ const reporter = createRenderingProgressReporter(summary, renderer);
802
+ return {
803
+ dispose: renderer.finish,
804
+ reporter,
805
+ write: renderer.write
806
+ };
807
+ }
808
+ function createRenderingProgressReporter(summary, renderer) {
809
+ return {
810
+ onDiagnostic: (payload) => {
811
+ summary.onDiagnostic?.(payload);
812
+ renderer.render();
813
+ },
814
+ onFileEnd: (payload) => {
815
+ summary.onFileEnd?.(payload);
816
+ renderer.render();
817
+ },
818
+ onFileStart: (payload) => {
819
+ summary.onFileStart?.(payload);
820
+ renderer.render();
821
+ },
822
+ onRuleEnd: (payload) => {
823
+ summary.onRuleEnd?.(payload);
824
+ renderer.render();
825
+ },
826
+ onRuleStart: (payload) => {
827
+ summary.onRuleStart?.(payload);
828
+ renderer.render();
829
+ },
830
+ onRunEnd: (payload) => {
831
+ summary.onRunEnd?.(payload);
832
+ renderer.render();
833
+ },
834
+ onRunStart: (payload) => {
835
+ summary.onRunStart?.(payload);
836
+ renderer.start();
837
+ },
838
+ onTargetEnd: (payload) => {
839
+ summary.onTargetEnd?.(payload);
840
+ renderer.render();
841
+ },
842
+ onTargetStart: (payload) => {
843
+ summary.onTargetStart?.(payload);
844
+ renderer.render();
845
+ },
846
+ onUsage: (payload) => {
847
+ summary.onUsage?.(payload);
848
+ renderer.render();
849
+ }
850
+ };
851
+ }
852
+ //#endregion
853
+ //#region src/cli/cli.ts
854
+ async function executeCli(argv, io) {
855
+ const cli = cac("alint");
856
+ const setupNoInteractive = argv.includes("-N") || argv.includes("--no-interactive");
857
+ let pendingResult;
858
+ cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").help();
859
+ cli.command("setup", "Write alint provider configuration").option("--local", "Write project-local config").option("-N, --no-interactive", "Disable interactive setup").option("--provider-endpoint <endpoint>", "Provider endpoint").option("--provider-id <id>", "Provider id").option("--provider-model <model>", "Provider model").option("--provider-header <Key=Value>", "Provider header").action((options) => {
860
+ pendingResult = runSetupCommand({
861
+ ...options,
862
+ noInteractive: setupNoInteractive
863
+ }, io);
864
+ return pendingResult;
865
+ });
866
+ cli.command("config [...args]", "Manage alint configuration").option("--endpoint <url>", "Provider endpoint").option("--provider-header <Key=Value>", "Provider header").action((args, options) => {
867
+ pendingResult = runConfigCommand(args, options, io);
868
+ return pendingResult;
869
+ });
870
+ cli.command("[...files]", "Run alint").action((files = [], options) => {
871
+ pendingResult = runDefaultCommand(files, options, io);
872
+ return pendingResult;
873
+ });
874
+ const restoreConsole = interceptConsoleOutput(shouldCaptureHelp(argv) ? io.stdout : io.stderr);
875
+ try {
876
+ cli.parse(argv);
877
+ return await (pendingResult ?? Promise.resolve(0));
878
+ } finally {
879
+ restoreConsole();
880
+ }
881
+ }
882
+ async function assertConfigExists(cwd, configPath) {
883
+ const resolvedConfigPath = resolve(cwd, configPath);
884
+ try {
885
+ if (!(await stat(resolvedConfigPath)).isFile()) throw new Error(`Config file "${configPath}" is not a file.`);
886
+ } catch (error) {
887
+ if (isNodeError(error) && error.code === "ENOENT") throw new Error(`Config file "${configPath}" does not exist.`);
888
+ throw error;
889
+ }
890
+ }
891
+ function formatRunError(error, color) {
892
+ return `${color ? c.red("error") : "error"} ${formatRunErrorContext(error)}\n Rule running failed due to ${error.failure?.message ?? error.message}\n`;
893
+ }
894
+ function formatRunErrorContext(error) {
895
+ const failure = error.failure;
896
+ if (!failure) return "alint run failed";
897
+ const target = failure.target ? failure.target.name ? `${failure.target.kind} ${failure.target.name}` : failure.target.kind : void 0;
898
+ return [
899
+ failure.filePath,
900
+ target,
901
+ failure.ruleId
902
+ ].filter(Boolean).join(" > ");
903
+ }
904
+ function interceptConsoleOutput(stdout) {
905
+ const cliConsole = globalThis.console;
906
+ const originalConsoleDebug = cliConsole.debug;
907
+ const originalConsoleDir = cliConsole.dir;
908
+ const originalConsoleInfo = console.info;
909
+ const originalConsoleLog = cliConsole.log;
910
+ const writeConsoleLine = (...args) => {
911
+ stdout.write(`${args.map(String).join(" ")}\n`);
912
+ };
913
+ const writeConsoleDir = (item, options) => {
914
+ stdout.write(`${inspect(item, options)}\n`);
915
+ };
916
+ cliConsole.debug = writeConsoleLine;
917
+ cliConsole.dir = writeConsoleDir;
918
+ console.info = writeConsoleLine;
919
+ cliConsole.log = writeConsoleLine;
920
+ return () => {
921
+ cliConsole.debug = originalConsoleDebug;
922
+ cliConsole.dir = originalConsoleDir;
923
+ console.info = originalConsoleInfo;
924
+ cliConsole.log = originalConsoleLog;
925
+ };
926
+ }
927
+ function isNodeError(error) {
928
+ return error instanceof Error && "code" in error;
929
+ }
930
+ async function loadMergedSetupConfig(io) {
931
+ const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
932
+ const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
933
+ const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
934
+ return mergeSetupConfigs(globalSetupConfig, projectSetupConfig);
935
+ }
936
+ function mergeRunnerCacheConfig(setupCache, configCache) {
937
+ if (configCache === void 0) return setupCache;
938
+ if (typeof configCache === "boolean") return configCache;
939
+ if (typeof setupCache === "object") return {
940
+ ...setupCache,
941
+ ...configCache
942
+ };
943
+ return configCache;
944
+ }
945
+ function parsePositiveIntegerOption(value, label) {
946
+ if (value === void 0) return;
947
+ const parsed = Number(value);
948
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
949
+ return parsed;
950
+ }
951
+ async function resolveLintFiles(files, config, cwd) {
952
+ if (config.ignore?.gitignore !== true || files.length === 0) return files;
953
+ const gitignore = new Gitignore();
954
+ const lintFiles = [];
955
+ for (const file of files) {
956
+ if (await gitignore.ignores(resolve(cwd, file))) continue;
957
+ lintFiles.push(file);
958
+ }
959
+ return lintFiles;
960
+ }
961
+ function resolveRunnerCacheConfig(setupCache, configCache, options) {
962
+ if (options.cache === false) return false;
963
+ const configuredCache = mergeRunnerCacheConfig(setupCache, configCache);
964
+ if (options.cacheLocation !== void 0) return typeof configuredCache === "object" ? {
965
+ ...configuredCache,
966
+ location: options.cacheLocation
967
+ } : { location: options.cacheLocation };
968
+ return configuredCache;
969
+ }
970
+ function resolveRunnerConfig(setupConfig, config, options) {
971
+ const cache = resolveRunnerCacheConfig(setupConfig.runner?.cache, config.runner?.cache, options);
972
+ const fileConcurrency = parsePositiveIntegerOption(options.fileConcurrency, "--file-concurrency");
973
+ const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
974
+ const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
975
+ const runner = {
976
+ ...setupConfig.runner ?? {},
977
+ ...config.runner ?? {},
978
+ cache,
979
+ fileConcurrency: fileConcurrency ?? config.runner?.fileConcurrency ?? setupConfig.runner?.fileConcurrency,
980
+ ruleConcurrency: ruleConcurrency ?? config.runner?.ruleConcurrency ?? setupConfig.runner?.ruleConcurrency,
981
+ timeoutMs: timeoutMs ?? config.runner?.timeoutMs ?? setupConfig.runner?.timeoutMs
982
+ };
983
+ return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
984
+ }
985
+ async function runConfigCommand(args, options, io) {
986
+ if (args[0] === "models" && args[1] === "probe" && args.length === 2) return runModelsProbeCommand(options, io);
987
+ if (args[0] === "models" && args[1] === "ls" && args.length === 2) return runModelsListCommand(io);
988
+ if (args[0] === "models" && args[1] === "show" && args.length === 3) return runModelsShowCommand(args[2], io);
989
+ if (args[0] === "providers" && args[1] === "ls" && args.length === 2) return runProvidersListCommand(io);
990
+ if (args[0] === "providers" && args[1] === "show" && args.length === 3) return runProvidersShowCommand(args[2], io);
991
+ if (args[0] === "providers" && args[1] === "probe" && args.length === 2) return runProvidersProbeCommand(options, io);
992
+ io.stderr.write(`unknown config command: ${args.join(" ")}\n`);
993
+ return 2;
994
+ }
995
+ async function runDefaultCommand(files, options, io) {
996
+ if (options.config) await assertConfigExists(io.cwd, options.config);
997
+ const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
998
+ const lintFiles = await resolveLintFiles(files, config, io.cwd);
999
+ const runner = resolveRunnerConfig(setupConfig, config, options);
1000
+ const progress = shouldEnableProgress(options, io) ? createCliProgressReporter({
1001
+ color: io.stderr.isTTY === true,
1002
+ columns: io.stderr.columns ?? 80,
1003
+ cwd: io.cwd,
1004
+ isTty: io.stderr.isTTY === true,
1005
+ write: (chunk) => io.stderr.write(chunk)
1006
+ }) : void 0;
1007
+ const restoreProgressConsole = progress ? interceptConsoleOutput({ write: progress.write }) : void 0;
1008
+ let result;
1009
+ try {
1010
+ result = await runAlint({
1011
+ config,
1012
+ cwd: io.cwd,
1013
+ files: lintFiles,
1014
+ modelOverride: options.model,
1015
+ progress: progress?.reporter,
1016
+ runner,
1017
+ setupConfig
1018
+ });
1019
+ } catch (error) {
1020
+ restoreProgressConsole?.();
1021
+ progress?.dispose();
1022
+ if (error instanceof AlintRunError) {
1023
+ io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
1024
+ return 2;
1025
+ }
1026
+ throw error;
1027
+ }
1028
+ restoreProgressConsole?.();
1029
+ progress?.dispose();
1030
+ io.stdout.write(formatDiagnostics(options.format, result, { color: io.stdout.isTTY === true }));
1031
+ return result.diagnostics.length > 0 ? 1 : 0;
1032
+ }
1033
+ async function runModelsListCommand(io) {
1034
+ io.stdout.write(formatModelList(await loadMergedSetupConfig(io)));
1035
+ return 0;
1036
+ }
1037
+ async function runModelsProbeCommand(options, io) {
1038
+ if (!options.endpoint) {
1039
+ io.stderr.write("config models probe requires --endpoint.\n");
1040
+ return 2;
1041
+ }
1042
+ try {
1043
+ const models = await probeModels(options.endpoint, parseHeaderList(toArray(options.providerHeader)) ?? {});
1044
+ io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
1045
+ return 0;
1046
+ } catch (error) {
1047
+ io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
1048
+ return 2;
1049
+ }
1050
+ }
1051
+ async function runModelsShowCommand(model, io) {
1052
+ const candidate = findModel(await loadMergedSetupConfig(io), model);
1053
+ if (candidate === void 0) {
1054
+ io.stderr.write(`unknown model "${model}".\n`);
1055
+ return 2;
1056
+ }
1057
+ io.stdout.write(formatModelShow(candidate));
1058
+ return 0;
1059
+ }
1060
+ async function runProvidersListCommand(io) {
1061
+ io.stdout.write(formatProviderList(await loadMergedSetupConfig(io)));
1062
+ return 0;
1063
+ }
1064
+ async function runProvidersProbeCommand(options, io) {
1065
+ if (!options.endpoint) {
1066
+ io.stderr.write("config providers probe requires --endpoint.\n");
1067
+ return 2;
1068
+ }
1069
+ try {
1070
+ const models = await probeModels(options.endpoint, parseHeaderList(toArray(options.providerHeader)) ?? {});
1071
+ io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
1072
+ return 0;
1073
+ } catch (error) {
1074
+ io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
1075
+ return 2;
1076
+ }
1077
+ }
1078
+ async function runProvidersShowCommand(providerId, io) {
1079
+ const provider = (await loadMergedSetupConfig(io)).providers.find((item) => item.id === providerId);
1080
+ if (provider === void 0) {
1081
+ io.stderr.write(`unknown provider "${providerId}".\n`);
1082
+ return 2;
1083
+ }
1084
+ io.stdout.write(formatProviderShow(provider));
1085
+ return 0;
1086
+ }
1087
+ function shouldCaptureHelp(argv) {
1088
+ return argv.includes("--help") || argv.includes("-h");
1089
+ }
1090
+ function shouldEnableProgress(options, io) {
1091
+ if (options.progress !== void 0) return options.progress;
1092
+ return options.format === "stylish" && io.stderr.isTTY === true;
1093
+ }
1094
+ function toArray(value) {
1095
+ if (value === void 0) return [];
1096
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
1097
+ }
1098
+ //#endregion
1099
+ export { formatJson as i, formatDiagnostics as n, formatStylish as r, executeCli as t };