@alint-js/cli 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,224 @@
1
1
  import process from "node:process";
2
- import { stat } from "node:fs/promises";
3
2
  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
3
  import { cac } from "cac";
10
- import { resolve } from "pathe";
4
+ import { getGlobalSetupConfigPath, getProjectSetupConfigPath, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
5
+ import { AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForFile, runAlint } from "@alint-js/core";
6
+ import { relative, resolve } from "pathe";
11
7
  import { getBorderCharacters, table } from "table";
8
+ import { errorMessageFrom } from "@moeru/std/error";
9
+ import { readFile, readdir, stat } from "node:fs/promises";
10
+ import c, { createColors } from "tinyrainbow";
12
11
  import cliSpinners from "cli-spinners";
13
- import { relative } from "node:path";
12
+ import { relative as relative$1 } from "node:path";
13
+ import Gitignore from "gitignore-fs";
14
+ import { minimatch } from "minimatch";
15
+ import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
16
+ //#region src/cli/commands/command.ts
17
+ function defineCommand(node) {
18
+ return node;
19
+ }
20
+ function registerCommandTree(cli, nodes, context, setPendingResult, help = {}) {
21
+ for (const node of nodes) registerRootCommand(cli, node, context, setPendingResult);
22
+ cli.globalCommand.helpCallback = (sections) => formatCommandHelp(sections, nodes, cli.rawArgs, help);
23
+ }
24
+ function collectCommandOptions(node) {
25
+ const options = /* @__PURE__ */ new Map();
26
+ for (const option of node.options ?? []) options.set(option.flags, option);
27
+ for (const child of node.children ?? []) for (const option of collectCommandOptions(child)) options.set(option.flags, option);
28
+ return [...options.values()];
29
+ }
30
+ function commandPattern(node) {
31
+ if (node.default) return node.arguments ?? node.name;
32
+ return [node.name, node.children ? "[...args]" : node.arguments].filter(Boolean).join(" ");
33
+ }
34
+ function dispatchCommand(context, node, args, options, path) {
35
+ const [subcommand, ...restArgs] = args;
36
+ const child = node.children?.find((item) => item.name === subcommand || item.alias?.includes(subcommand ?? ""));
37
+ if (child) return dispatchCommand(context, child, restArgs, options, [...path, child.name]);
38
+ if (!node.action) return Promise.resolve(reportUnknownCommand(context, path, args));
39
+ return Promise.resolve(node.action(context, ...parseCommandArguments(node, args), options));
40
+ }
41
+ function formatChildCommandHelp(parentPath, child) {
42
+ const parts = [...parentPath, child.name];
43
+ if (!child.children && child.arguments) parts.push(child.arguments);
44
+ return {
45
+ description: child.description,
46
+ pattern: parts.join(" ")
47
+ };
48
+ }
49
+ function formatCommandHelp(sections, nodes, argv, rootHelp) {
50
+ const helpPath = resolveHelpPath(nodes, argv);
51
+ const node = helpPath.at(-1)?.node;
52
+ if (!node) return insertExamplesSection(insertHelpSection(sections, rootHelp), rootHelp);
53
+ const path = helpPath.map((item) => item.node.name);
54
+ const normalizedSections = rewriteUsageSection(insertExamplesSection(insertHelpSection(sections.filter((section) => section.title !== "Options"), node), node), path, node);
55
+ if (!node.children) {
56
+ const optionSection = formatOptionsSection(node.options ?? []);
57
+ return optionSection ? [...normalizedSections, optionSection] : normalizedSections;
58
+ }
59
+ const commands = (node.children ?? []).map((child) => formatChildCommandHelp(path, child));
60
+ const longestCommand = Math.max(...commands.map((command) => command.pattern.length));
61
+ const commandSection = {
62
+ body: commands.map((command) => ` ${command.pattern.padEnd(longestCommand)} ${command.description}`).join("\n"),
63
+ title: "Commands"
64
+ };
65
+ const usageIndex = normalizedSections.findIndex((section) => section.title === "Usage");
66
+ if (usageIndex === -1) return [commandSection, ...normalizedSections];
67
+ return [
68
+ ...normalizedSections.slice(0, usageIndex + 1),
69
+ commandSection,
70
+ ...normalizedSections.slice(usageIndex + 1)
71
+ ];
72
+ }
73
+ function formatExamples(examples) {
74
+ return examples.map((example) => example.split("\n").map((line) => ` ${line}`).join("\n")).join("\n\n");
75
+ }
76
+ function formatOptionDescription(option) {
77
+ if (option.config?.default === void 0) return option.description;
78
+ return `${option.description} (default: ${option.config.default})`;
79
+ }
80
+ function formatOptionsSection(options) {
81
+ if (options.length === 0) return;
82
+ const rows = options.map((option) => ({
83
+ description: formatOptionDescription(option),
84
+ flags: option.flags
85
+ }));
86
+ const longestFlag = Math.max(...rows.map((row) => row.flags.length));
87
+ return {
88
+ body: rows.map((row) => ` ${row.flags.padEnd(longestFlag)} ${row.description}`).join("\n"),
89
+ title: "Options"
90
+ };
91
+ }
92
+ function formatUnknownCommand(path, args) {
93
+ return [...path, ...args].filter(Boolean).join(" ");
94
+ }
95
+ function formatUsagePattern(path, node) {
96
+ const parts = [...path];
97
+ if (!node.children && node.arguments) parts.push(node.arguments);
98
+ return parts.join(" ");
99
+ }
100
+ function insertExamplesSection(sections, node) {
101
+ if (!node.examples?.length) return sections;
102
+ const usageIndex = sections.findIndex((section) => section.title === "Usage");
103
+ const examplesSection = {
104
+ body: formatExamples(node.examples),
105
+ title: "Examples"
106
+ };
107
+ if (usageIndex === -1) return [...sections, examplesSection];
108
+ return [
109
+ ...sections.slice(0, usageIndex),
110
+ examplesSection,
111
+ ...sections.slice(usageIndex)
112
+ ];
113
+ }
114
+ function insertHelpSection(sections, node) {
115
+ const help = node.help ?? node.description;
116
+ if (!help) return sections;
117
+ const usageIndex = sections.findIndex((section) => section.title === "Usage");
118
+ const helpSection = { body: help };
119
+ if (usageIndex === -1) return [...sections, helpSection];
120
+ return [
121
+ ...sections.slice(0, usageIndex),
122
+ helpSection,
123
+ ...sections.slice(usageIndex)
124
+ ];
125
+ }
126
+ function parseCommandArguments(node, args) {
127
+ if (!node.arguments) return [];
128
+ const parts = node.arguments.split(/\s+/u).filter(Boolean);
129
+ const values = [];
130
+ let argIndex = 0;
131
+ for (const part of parts) {
132
+ if (part.startsWith("[...") || part.startsWith("<...")) {
133
+ values.push(args.slice(argIndex));
134
+ argIndex = args.length;
135
+ continue;
136
+ }
137
+ if (part.startsWith("<") && argIndex >= args.length) throw new Error(`Missing required argument ${part}.`);
138
+ values.push(args[argIndex]);
139
+ argIndex += 1;
140
+ }
141
+ return values;
142
+ }
143
+ function registerRootCommand(cli, node, context, setPendingResult) {
144
+ const command = cli.command(commandPattern(node), node.description);
145
+ if (node.allowUnknownOptions || node.children) command.allowUnknownOptions();
146
+ for (const alias of node.alias ?? []) command.alias(alias);
147
+ for (const option of collectCommandOptions(node)) command.option(option.flags, option.description, option.config);
148
+ command.action((...args) => {
149
+ const options = args.at(-1);
150
+ return setPendingResult(node.children ? dispatchCommand(context, node, args[0] ?? [], options, [node.name]) : Promise.resolve(node.action?.(context, ...args.slice(0, -1), options) ?? 0));
151
+ });
152
+ }
153
+ function reportUnknownCommand(context, path, args) {
154
+ context.io.stderr.write(`unknown command: ${formatUnknownCommand(path, args)}\n`);
155
+ return 2;
156
+ }
157
+ function resolveHelpPath(nodes, argv) {
158
+ const path = [];
159
+ let currentNodes = nodes;
160
+ let skipNext = false;
161
+ for (const arg of argv.slice(2)) {
162
+ if (skipNext) {
163
+ skipNext = false;
164
+ continue;
165
+ }
166
+ if (arg.startsWith("-")) {
167
+ skipNext = shouldSkipOptionValue(arg);
168
+ continue;
169
+ }
170
+ const node = currentNodes.find((item) => item.name === arg || item.alias?.includes(arg));
171
+ if (!node) break;
172
+ path.push({ node });
173
+ currentNodes = node.children ?? [];
174
+ }
175
+ return path;
176
+ }
177
+ function rewriteUsageSection(sections, path, node) {
178
+ return sections.map((section) => section.title === "Usage" ? {
179
+ ...section,
180
+ body: ` $ alint ${formatUsagePattern(path, node)}`
181
+ } : section);
182
+ }
183
+ function shouldSkipOptionValue(arg) {
184
+ if (arg.includes("=")) return false;
185
+ return [
186
+ "--cache-location",
187
+ "--config",
188
+ "--file-concurrency",
189
+ "--format",
190
+ "--model",
191
+ "--provider-endpoint",
192
+ "--provider-header",
193
+ "--provider-id",
194
+ "--provider-model",
195
+ "--rule-concurrency",
196
+ "--timeout-ms",
197
+ "-f",
198
+ "-l"
199
+ ].includes(arg);
200
+ }
201
+ //#endregion
202
+ //#region src/cli/commands/config/inspect.ts
203
+ const inspect$2 = defineCommand({
204
+ action: (context, file, options) => runConfigInspectCommand(file, options.config, context.io),
205
+ arguments: "<file>",
206
+ description: "Inspect resolved config for a file",
207
+ name: "inspect"
208
+ });
209
+ async function runConfigInspectCommand(file, configPath, io) {
210
+ const config = await loadAlintConfig(io.cwd, configPath);
211
+ const result = resolveConfigForFile(resolve(io.cwd, file), config, { cwd: io.cwd });
212
+ io.stdout.write(`file: ${file}\n`);
213
+ io.stdout.write(`ignored: ${result.ignored ? "yes" : "no"}\n`);
214
+ io.stdout.write("matched:\n");
215
+ for (const item of result.matched) io.stdout.write(` - ${item.name ?? "<anonymous>"}\n`);
216
+ io.stdout.write(`language: ${result.config.language ?? "<inferred>"}\n`);
217
+ io.stdout.write("rules:\n");
218
+ for (const [id, entry] of Object.entries(result.config.rules)) io.stdout.write(` ${id}: ${Array.isArray(entry) ? entry[0] : entry}\n`);
219
+ return 0;
220
+ }
221
+ //#endregion
14
222
  //#region src/cli/provider-registry.ts
15
223
  function buildModelsUrl(endpoint) {
16
224
  return new URL("models", endpoint.endsWith("/") ? endpoint : `${endpoint}/`).toString();
@@ -116,760 +324,1254 @@ function formatTable(rows) {
116
324
  });
117
325
  }
118
326
  //#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}`;
327
+ //#region src/cli/commands/config/setup-config.ts
328
+ async function loadMergedSetupConfig(io) {
329
+ const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
330
+ const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
331
+ const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
332
+ return mergeSetupConfigs(globalSetupConfig, projectSetupConfig);
125
333
  }
126
- function isBackInput(value) {
127
- return value.trim() === "..";
334
+ //#endregion
335
+ //#region src/cli/commands/config/models/ls.ts
336
+ const ls$1 = defineCommand({
337
+ async action(context) {
338
+ context.io.stdout.write(formatModelList(await loadMergedSetupConfig(context.io)));
339
+ return 0;
340
+ },
341
+ alias: ["ls"],
342
+ description: "List configured models",
343
+ name: "list"
344
+ });
345
+ //#endregion
346
+ //#region src/cli/commands/config/probe.ts
347
+ function providerHeadersFromOptions(options) {
348
+ return parseHeaderList(toArray$1(options.providerHeader)) ?? {};
128
349
  }
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;
350
+ function toArray$1(value) {
351
+ if (value === void 0) return [];
352
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
353
+ }
354
+ //#endregion
355
+ //#region src/cli/commands/config/models/models.ts
356
+ const models = defineCommand({
357
+ children: [
358
+ defineCommand({
359
+ async action(context, options) {
360
+ if (!options.endpoint) {
361
+ context.io.stderr.write("config models probe requires --endpoint.\n");
362
+ return 2;
363
+ }
364
+ try {
365
+ const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
366
+ context.io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
367
+ return 0;
368
+ } catch (error) {
369
+ context.io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
370
+ return 2;
371
+ }
372
+ },
373
+ description: "Probe OpenAI-compatible models",
374
+ examples: [["# Probe a provider endpoint and print available model ids", "alint config models probe --endpoint https://openrouter.ai/api/v1"].join("\n"), ["# Probe an endpoint that requires an authorization header", "alint config models probe --endpoint https://api.example.com/v1 --provider-header \"Authorization=Bearer $TOKEN\""].join("\n")],
375
+ help: ["Probe an OpenAI-compatible models endpoint before saving it in setup config.", "The command calls the provider models endpoint using the supplied endpoint and optional headers, then prints the model ids returned by that provider."].join("\n\n"),
376
+ name: "probe",
377
+ options: [{
378
+ description: "Provider endpoint",
379
+ flags: "--endpoint <url>"
380
+ }, {
381
+ description: "Provider header",
382
+ flags: "--provider-header <Key=Value>"
383
+ }]
384
+ }),
385
+ ls$1,
386
+ defineCommand({
387
+ async action(context, model) {
388
+ const candidate = findModel(await loadMergedSetupConfig(context.io), model);
389
+ if (candidate === void 0) {
390
+ context.io.stderr.write(`unknown model "${model}".\n`);
391
+ return 2;
392
+ }
393
+ context.io.stdout.write(formatModelShow(candidate));
394
+ return 0;
395
+ },
396
+ arguments: "<model>",
397
+ description: "Show configured model",
398
+ name: "show"
399
+ })
400
+ ],
401
+ description: "Manage configured models",
402
+ help: ["Inspect, list, and probe model entries from alint setup configuration.", "Model entries describe the provider/model ids alint can use for model-backed rules. Probe endpoints before saving them when you want to verify what model ids a provider exposes."].join("\n\n"),
403
+ name: "models"
404
+ });
405
+ //#endregion
406
+ //#region src/cli/commands/config/providers/ls.ts
407
+ const ls = defineCommand({
408
+ async action(context) {
409
+ context.io.stdout.write(formatProviderList(await loadMergedSetupConfig(context.io)));
410
+ return 0;
411
+ },
412
+ alias: ["ls"],
413
+ description: "List configured providers",
414
+ name: "list"
415
+ });
416
+ //#endregion
417
+ //#region src/cli/commands/config/providers/probe.ts
418
+ const probe = defineCommand({
419
+ async action(context, options) {
420
+ if (!options.endpoint) {
421
+ context.io.stderr.write("config providers probe requires --endpoint.\n");
422
+ return 2;
158
423
  }
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"
424
+ try {
425
+ const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
426
+ context.io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
427
+ return 0;
428
+ } catch (error) {
429
+ context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
430
+ return 2;
431
+ }
432
+ },
433
+ description: "Probe provider reachability",
434
+ name: "probe",
435
+ options: [{
436
+ description: "Provider endpoint",
437
+ flags: "--endpoint <url>"
438
+ }, {
439
+ description: "Provider header",
440
+ flags: "--provider-header <Key=Value>"
441
+ }]
442
+ });
443
+ //#endregion
444
+ //#region src/cli/commands/config/config.ts
445
+ const config = defineCommand({
446
+ children: [
447
+ inspect$2,
448
+ models,
449
+ defineCommand({
450
+ children: [
451
+ ls,
452
+ defineCommand({
453
+ async action(context, providerId) {
454
+ const provider = (await loadMergedSetupConfig(context.io)).providers.find((item) => item.id === providerId);
455
+ if (provider === void 0) {
456
+ context.io.stderr.write(`unknown provider "${providerId}".\n`);
457
+ return 2;
458
+ }
459
+ context.io.stdout.write(formatProviderShow(provider));
460
+ return 0;
170
461
  },
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";
462
+ arguments: "<provider>",
463
+ description: "Show configured provider",
464
+ name: "show"
465
+ }),
466
+ probe
467
+ ],
468
+ description: "Manage configured providers",
469
+ name: "providers"
470
+ })
471
+ ],
472
+ description: "Manage alint configuration",
473
+ examples: [
474
+ ["# Show the effective alint config for a file", "alint config inspect src/index.ts"].join("\n"),
475
+ ["# Inspect a file using a custom config path", "alint --config alint.config.ts config inspect src/index.ts"].join("\n"),
476
+ [
477
+ "# List configured providers and models",
478
+ "alint config providers list",
479
+ "alint config models list"
480
+ ].join("\n"),
481
+ [
482
+ "# Show one configured provider or model by id",
483
+ "alint config providers show openrouter",
484
+ "alint config models show z-ai/glm-5.2"
485
+ ].join("\n"),
486
+ [
487
+ "# Probe an OpenAI-compatible endpoint before saving it",
488
+ "alint config providers probe --endpoint https://openrouter.ai/api/v1",
489
+ "alint config models probe --endpoint https://openrouter.ai/api/v1"
490
+ ].join("\n")
491
+ ],
492
+ help: ["Inspect and update alint setup/configuration state.", "Use these commands to understand the effective config for a file, inspect saved provider/model setup, and probe OpenAI-compatible endpoints before using them in model-backed rules."].join("\n\n"),
493
+ name: "config"
494
+ });
495
+ //#endregion
496
+ //#region src/cli/reporters/json.ts
497
+ function formatJson(result) {
498
+ return `${JSON.stringify(result, null, 2)}\n`;
499
+ }
500
+ //#endregion
501
+ //#region src/cli/reporters/stylish.ts
502
+ const colors$1 = createColors({ force: true });
503
+ function formatStylish(input, options = {}) {
504
+ const diagnostics = Array.isArray(input) ? input : input.diagnostics;
505
+ const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
506
+ if (diagnostics.length === 0) return "";
507
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
508
+ for (const diagnostic of diagnostics) {
509
+ const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
510
+ if (fileDiagnostics) {
511
+ fileDiagnostics.push(diagnostic);
184
512
  continue;
185
513
  }
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;
514
+ diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
515
+ }
516
+ const lines = [];
517
+ const style = createStyle(options.color === true);
518
+ for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
519
+ lines.push(style.file(filePath));
520
+ for (const diagnostic of fileDiagnostics) {
521
+ const line = diagnostic.loc?.start.line ?? 0;
522
+ const column = diagnostic.loc?.start.column ?? 0;
523
+ const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
524
+ lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
197
525
  }
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;
526
+ lines.push("");
527
+ }
528
+ lines.push("", formatSummary(diagnostics, totalTokens, style));
529
+ return `${lines.join("\n")}\n`;
530
+ }
531
+ function countDiagnostics$2(diagnostics, severity) {
532
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
533
+ }
534
+ function createStyle(color) {
535
+ if (!color) return {
536
+ error: identity,
537
+ file: identity,
538
+ location: identity,
539
+ ruleId: identity,
540
+ summaryToken: identity,
541
+ warning: identity
542
+ };
543
+ return {
544
+ error: colors$1.red,
545
+ file: colors$1.underline,
546
+ location: colors$1.dim,
547
+ ruleId: colors$1.dim,
548
+ summaryToken: colors$1.cyan,
549
+ warning: colors$1.yellow
550
+ };
551
+ }
552
+ function formatSummary(diagnostics, totalTokens, style) {
553
+ const warnCount = countDiagnostics$2(diagnostics, "warn");
554
+ const errorCount = countDiagnostics$2(diagnostics, "error");
555
+ const tokens = totalTokens === void 0 ? void 0 : `${totalTokens.toLocaleString("en-US")} tokens`;
556
+ const problemSummary = [style.warning(`${warnCount} warn`), style.error(`${errorCount} error`)].join(" / ");
557
+ if (tokens === void 0) return problemSummary;
558
+ return `${problemSummary} | ${style.summaryToken(tokens)}`;
559
+ }
560
+ function identity(value) {
561
+ return value;
562
+ }
563
+ //#endregion
564
+ //#region src/cli/reporters/index.ts
565
+ function formatDiagnostics(format, result, options = {}) {
566
+ if (format === "json") return formatJson(result);
567
+ if (format === "stylish") return formatStylish(result, { color: options.color });
568
+ throw new Error(`Unknown reporter "${format}".`);
569
+ }
570
+ //#endregion
571
+ //#region src/cli/reporters/progress/plain.ts
572
+ function createPlainProgressReporter(options) {
573
+ const writeLine = (line) => options.write(`${line}\n`);
574
+ return {
575
+ onRuleStart: (payload) => {
576
+ const target = payload.path.target.name ? `${payload.path.target.kind} ${payload.path.target.name}` : payload.path.target.kind;
577
+ writeLine(`scan ${payload.path.file.path} > ${target} > ${payload.path.rule.id}`);
578
+ },
579
+ onRunEnd: (payload) => {
580
+ const warnCount = countDiagnostics$1(payload.diagnostics, "warn");
581
+ const errorCount = countDiagnostics$1(payload.diagnostics, "error");
582
+ const state = payload.errored > 0 ? "failed" : "finished";
583
+ const cached = payload.cached > 0 ? `, ${payload.cached} cached` : "";
584
+ const errored = payload.errored > 0 ? `, ${payload.errored} errored` : "";
585
+ writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens${cached}${errored}`);
586
+ },
587
+ onRunStart: (payload) => {
588
+ writeLine(`alint started: ${payload.filesTotal} files, ${payload.rulesTotal} rules, ${payload.planned} planned executions`);
215
589
  }
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;
590
+ };
591
+ }
592
+ function countDiagnostics$1(diagnostics, severity) {
593
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
594
+ }
595
+ //#endregion
596
+ //#region src/cli/reporters/progress/summary.ts
597
+ const colors = createColors({ force: true });
598
+ function createSummaryProgressReporter(options) {
599
+ const now = () => options.clock?.() ?? Date.now();
600
+ const state = {
601
+ cached: 0,
602
+ completed: 0,
603
+ diagnostics: [],
604
+ errored: 0,
605
+ files: /* @__PURE__ */ new Map(),
606
+ planned: 0,
607
+ spinnerIndex: 0,
608
+ totalTokens: 0
609
+ };
610
+ return {
611
+ getRows: () => createRows(state, options, now()),
612
+ onDiagnostic: (payload) => {
613
+ state.diagnostics = payload.diagnostics;
614
+ },
615
+ onFileEnd: (payload) => {
616
+ const file = getFileState(state, payload.file);
617
+ file.endedAt = payload.endedAt ?? now();
618
+ file.rule = void 0;
619
+ file.target = void 0;
620
+ },
621
+ onFileStart: (payload) => {
622
+ const file = getFileState(state, payload.file);
623
+ file.startedAt = payload.startedAt ?? now();
624
+ file.endedAt = void 0;
625
+ },
626
+ onRuleEnd: (payload) => {
627
+ const file = getFileState(state, payload.path.file);
628
+ if (payload.cache === "hit") {
629
+ state.cached += 1;
630
+ file.cached += 1;
236
631
  }
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;
632
+ if (payload.state === "completed") {
633
+ state.completed += 1;
634
+ file.completed += 1;
249
635
  }
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;
636
+ if (payload.state === "errored") {
637
+ state.errored += 1;
638
+ file.errored += 1;
270
639
  }
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;
640
+ if (file.rule?.id === payload.path.rule.id) file.rule = void 0;
641
+ },
642
+ onRuleStart: (payload) => {
643
+ const file = getFileState(state, payload.path.file);
644
+ file.startedAt ??= payload.startedAt ?? now();
645
+ file.rule = {
646
+ id: payload.path.rule.id,
647
+ startedAt: payload.startedAt ?? now(),
648
+ target: formatTarget(payload)
649
+ };
650
+ file.target = file.rule.target;
651
+ },
652
+ onRunEnd: (payload) => {
653
+ state.cached = payload.cached;
654
+ state.completed = payload.completed;
655
+ state.diagnostics = payload.diagnostics;
656
+ state.endedAt = payload.endedAt ?? now();
657
+ state.errored = payload.errored;
658
+ state.planned = payload.planned;
659
+ state.runStartedAt = payload.startedAt ?? state.runStartedAt;
660
+ state.totalTokens = payload.usage.totalTokens;
661
+ },
662
+ onRunStart: (payload) => {
663
+ state.cached = 0;
664
+ state.completed = 0;
665
+ state.diagnostics = [];
666
+ state.endedAt = void 0;
667
+ state.errored = 0;
668
+ state.files = /* @__PURE__ */ new Map();
669
+ state.planned = payload.planned;
670
+ state.runStartedAt = payload.startedAt ?? now();
671
+ state.spinnerIndex = 0;
672
+ state.totalTokens = 0;
673
+ for (const file of payload.files ?? []) state.files.set(file.path, createFileState(file));
674
+ },
675
+ onTargetEnd: (payload) => {
676
+ const file = getFileState(state, payload.path.file);
677
+ const target = formatTarget(payload);
678
+ if (file.target === target) file.target = void 0;
679
+ },
680
+ onTargetStart: (payload) => {
681
+ const file = getFileState(state, payload.path.file);
682
+ file.startedAt ??= payload.startedAt ?? now();
683
+ file.target = formatTarget(payload);
684
+ },
685
+ onUsage: (payload) => {
686
+ state.totalTokens = payload.total.totalTokens;
687
+ },
688
+ tick: () => {
689
+ state.spinnerIndex = (state.spinnerIndex + 1) % Math.max(options.spinnerFrames.length, 1);
295
690
  }
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
- }
691
+ };
305
692
  }
306
- function withBackOption(options) {
307
- return [...options, {
308
- label: "Back",
309
- value: backValue
310
- }];
693
+ function countDiagnostics(diagnostics, severity) {
694
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
311
695
  }
312
- function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
696
+ function countQueuedFiles(state) {
697
+ return [...state.files.values()].filter((file) => file.startedAt === void 0 && file.endedAt === void 0 && (file.file.planned ?? 0) > 0).length;
698
+ }
699
+ function createFileState(file) {
313
700
  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"
701
+ cached: 0,
702
+ completed: 0,
703
+ errored: 0,
704
+ file
323
705
  };
324
706
  }
325
- function getConfigPath(io, scope) {
326
- return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
707
+ function createRows(state, options, now) {
708
+ const rows = getActiveFiles(state).flatMap((file) => formatFileRows(file, state, options, now));
709
+ const queued = countQueuedFiles(state);
710
+ const warnCount = countDiagnostics(state.diagnostics, "warn");
711
+ const errorCount = countDiagnostics(state.diagnostics, "error");
712
+ const footer = formatFooter(state, warnCount, errorCount, queued, options, now);
713
+ if (queued > 0) rows.push(formatQueuedRow(queued, options));
714
+ if (rows.length === 0) rows.push(formatIdleRow(state, options));
715
+ rows.push("", footer);
716
+ return options.color ? rows.map((row) => styleRow(row, state, warnCount, errorCount, options)) : rows;
327
717
  }
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
- }
718
+ function escapeRegExp(value) {
719
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
339
720
  }
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
- });
721
+ function estimateTotal(elapsedMs, completed, planned) {
722
+ if (completed <= 0 || planned <= 0) return;
723
+ return elapsedMs * planned / completed;
347
724
  }
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);
725
+ function fitRow(row, columns) {
726
+ if (columns <= 0) return "";
727
+ if (row.length <= columns) return row;
728
+ if (columns === 1) return "…";
729
+ return `${row.slice(0, columns - 1)}…`;
367
730
  }
368
- function splitHeaderInput(value) {
369
- return value.split(",").map((item) => item.trim()).filter(Boolean);
731
+ function formatDuration(ms) {
732
+ if (ms === void 0 || !Number.isFinite(ms)) return "?";
733
+ return `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
370
734
  }
371
- function splitModelInput(value) {
372
- return value.split(",").map((item) => item.trim()).filter(Boolean);
735
+ function formatEstimatedDuration(ms) {
736
+ return `~${formatDuration(ms)}`;
373
737
  }
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;
738
+ function formatFilePath(filePath, cwd) {
739
+ if (!cwd) return filePath;
740
+ return relative$1(cwd, filePath) || filePath;
392
741
  }
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
- };
742
+ function formatFileRows(file, state, options, now) {
743
+ const firstRow = formatFileSummaryRow(file, state, options);
744
+ if (!file.rule) return [firstRow];
745
+ const elapsed = now - file.rule.startedAt;
746
+ const done = file.completed + file.cached + file.errored;
747
+ const estimated = estimateTotal(file.startedAt === void 0 ? elapsed : now - file.startedAt, done, file.file.planned ?? 0);
748
+ return [firstRow, fitRow(` ${file.rule.target} > ${file.rule.id} (${formatDuration(elapsed)}, ${formatEstimatedDuration(estimated)})`, options.columns)];
408
749
  }
409
- function toArray$1(value) {
410
- if (value === void 0) return [];
411
- return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
750
+ function formatFileSummaryRow(file, state, options) {
751
+ const prefix = `${options.spinnerFrames[state.spinnerIndex] ?? ""} ${formatFilePath(file.file.path, options.cwd)}`;
752
+ const counter = `${file.completed}/${file.cached}/${file.errored}/${file.file.planned ?? 0}`;
753
+ return fitRow(`${prefix}${" ".repeat(Math.max(1, options.columns - prefix.length - counter.length))}${counter}`, options.columns);
412
754
  }
413
- //#endregion
414
- //#region src/cli/reporters/json.ts
415
- function formatJson(result) {
416
- return `${JSON.stringify(result, null, 2)}\n`;
755
+ function formatFooter(state, warnCount, errorCount, queued, options, now) {
756
+ const endedAt = state.endedAt ?? now;
757
+ const elapsed = state.runStartedAt === void 0 ? void 0 : endedAt - state.runStartedAt;
758
+ const completed = state.completed + state.cached + state.errored;
759
+ const estimated = elapsed === void 0 ? void 0 : estimateTotal(elapsed, completed, state.planned);
760
+ const estimatedTokens = completed > 0 && state.planned > 0 ? Math.ceil(state.totalTokens * state.planned / completed).toLocaleString("en-US") : "?";
761
+ return fitRow([
762
+ `${formatDuration(elapsed)} -> ${formatEstimatedDuration(estimated)}`,
763
+ `${state.totalTokens.toLocaleString("en-US")} tokens -> ~${estimatedTokens} tokens`,
764
+ `${queued} queued / ${state.cached} cached / ${warnCount} warn / ${errorCount} error`
765
+ ].join(" | "), options.columns);
417
766
  }
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`;
767
+ function formatIdleRow(state, options) {
768
+ const prefix = `${options.spinnerFrames[state.spinnerIndex] ?? ""} alint`;
769
+ const counter = `${state.completed}/${state.cached}/${state.errored}/${state.planned}`;
770
+ return fitRow(`${prefix}${" ".repeat(Math.max(1, options.columns - prefix.length - counter.length))}${counter}`, options.columns);
448
771
  }
449
- function countDiagnostics$2(diagnostics, severity) {
450
- return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
772
+ function formatQueuedRow(queued, options) {
773
+ return fitRow(` ${queued} ${queued === 1 ? "file" : "files"} queued`, options.columns);
451
774
  }
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
- };
775
+ function formatTarget(payload) {
776
+ return payload.path.target.name ? `${payload.path.target.kind} ${payload.path.target.name}` : payload.path.target.kind;
469
777
  }
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)}`;
778
+ function getActiveFiles(state) {
779
+ return [...state.files.values()].filter((file) => file.startedAt !== void 0 && file.endedAt === void 0).sort((left, right) => left.file.index - right.file.index);
477
780
  }
478
- function identity(value) {
479
- return value;
781
+ function getFileState(state, file) {
782
+ const existingFile = state.files.get(file.path);
783
+ if (existingFile) {
784
+ existingFile.file = {
785
+ ...existingFile.file,
786
+ ...file,
787
+ planned: file.planned ?? existingFile.file.planned
788
+ };
789
+ return existingFile;
790
+ }
791
+ const nextFile = createFileState({
792
+ ...file,
793
+ planned: file.planned ?? (state.files.size === 0 ? state.planned : void 0)
794
+ });
795
+ state.files.set(file.path, nextFile);
796
+ return nextFile;
480
797
  }
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}".`);
798
+ function replaceFirst(row, search, replacement) {
799
+ if (search.length === 0) return row;
800
+ return row.replace(new RegExp(escapeRegExp(search)), replacement);
801
+ }
802
+ function styleRow(row, state, warnCount, errorCount, options) {
803
+ let styledRow = row;
804
+ const spinner = options.spinnerFrames[state.spinnerIndex] ?? "";
805
+ if (spinner) styledRow = replaceFirst(styledRow, spinner, colors.cyan(spinner));
806
+ 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));
807
+ return styledRow;
487
808
  }
488
809
  //#endregion
489
- //#region src/cli/reporters/progress/plain.ts
490
- function createPlainProgressReporter(options) {
491
- const writeLine = (line) => options.write(`${line}\n`);
810
+ //#region src/cli/reporters/progress/tty.ts
811
+ const clearCurrentLine = "\r\x1B[K";
812
+ const clearPreviousLine = "\r\x1B[1A\x1B[K";
813
+ function createTtyProgressRenderer(options) {
814
+ let interval;
815
+ let previousRows = 0;
816
+ const clearPreviousFrame = () => {
817
+ if (previousRows === 0) return;
818
+ let sequence = clearCurrentLine;
819
+ for (let row = 1; row < previousRows; row += 1) sequence += clearPreviousLine;
820
+ options.write(sequence);
821
+ previousRows = 0;
822
+ };
823
+ const render = () => {
824
+ clearPreviousFrame();
825
+ const rows = options.getRows();
826
+ if (rows.length === 0) return;
827
+ options.write(rows.join("\n"));
828
+ previousRows = rows.length;
829
+ };
830
+ const write = (chunk) => {
831
+ const wasRendering = interval !== void 0;
832
+ clearPreviousFrame();
833
+ options.write(chunk);
834
+ if (wasRendering) render();
835
+ };
492
836
  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}`);
837
+ finish: () => {
838
+ if (interval) {
839
+ options.clearInterval(interval);
840
+ interval = void 0;
841
+ }
842
+ clearPreviousFrame();
496
843
  },
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}`);
844
+ render,
845
+ start: () => {
846
+ if (!interval) {
847
+ interval = options.createInterval(render, options.intervalMs);
848
+ if (isUnrefableInterval(interval)) interval.unref();
849
+ }
850
+ render();
504
851
  },
505
- onRunStart: (payload) => {
506
- writeLine(`alint started: ${payload.filesTotal} files, ${payload.rulesTotal} rules, ${payload.planned} planned executions`);
507
- }
852
+ write
508
853
  };
509
854
  }
510
- function countDiagnostics$1(diagnostics, severity) {
511
- return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
855
+ function isUnrefableInterval(interval) {
856
+ if (typeof interval !== "object" || interval === null || !("unref" in interval)) return false;
857
+ return typeof interval.unref === "function";
512
858
  }
513
859
  //#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
- };
860
+ //#region src/cli/reporters/progress/index.ts
861
+ function createCliProgressReporter(options) {
862
+ if (!options.isTty) return {
863
+ dispose: () => {},
864
+ reporter: createPlainProgressReporter({ write: options.write }),
865
+ write: options.write
866
+ };
867
+ const summary = createSummaryProgressReporter({
868
+ color: options.color,
869
+ columns: options.columns,
870
+ cwd: options.cwd,
871
+ spinnerFrames: cliSpinners.dots.frames
872
+ });
873
+ const renderer = createTtyProgressRenderer({
874
+ clearInterval: (handle) => globalThis.clearInterval(handle),
875
+ createInterval: (callback, intervalMs) => globalThis.setInterval(() => {
876
+ summary.tick();
877
+ callback();
878
+ }, intervalMs),
879
+ getRows: summary.getRows,
880
+ intervalMs: 120,
881
+ write: options.write
882
+ });
883
+ const reporter = createRenderingProgressReporter(summary, renderer);
884
+ return {
885
+ dispose: renderer.finish,
886
+ reporter,
887
+ write: renderer.write
888
+ };
889
+ }
890
+ function createRenderingProgressReporter(summary, renderer) {
528
891
  return {
529
- getRows: () => createRows(state, options, now()),
530
892
  onDiagnostic: (payload) => {
531
- state.diagnostics = payload.diagnostics;
893
+ summary.onDiagnostic?.(payload);
894
+ renderer.render();
532
895
  },
533
896
  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;
897
+ summary.onFileEnd?.(payload);
898
+ renderer.render();
538
899
  },
539
900
  onFileStart: (payload) => {
540
- const file = getFileState(state, payload.file);
541
- file.startedAt = payload.startedAt ?? now();
542
- file.endedAt = void 0;
901
+ summary.onFileStart?.(payload);
902
+ renderer.render();
543
903
  },
544
904
  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;
905
+ summary.onRuleEnd?.(payload);
906
+ renderer.render();
559
907
  },
560
908
  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;
909
+ summary.onRuleStart?.(payload);
910
+ renderer.render();
569
911
  },
570
912
  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;
913
+ summary.onRunEnd?.(payload);
914
+ renderer.render();
579
915
  },
580
916
  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));
917
+ summary.onRunStart?.(payload);
918
+ renderer.start();
592
919
  },
593
920
  onTargetEnd: (payload) => {
594
- const file = getFileState(state, payload.path.file);
595
- const target = formatTarget(payload);
596
- if (file.target === target) file.target = void 0;
921
+ summary.onTargetEnd?.(payload);
922
+ renderer.render();
597
923
  },
598
924
  onTargetStart: (payload) => {
599
- const file = getFileState(state, payload.path.file);
600
- file.startedAt ??= payload.startedAt ?? now();
601
- file.target = formatTarget(payload);
925
+ summary.onTargetStart?.(payload);
926
+ renderer.render();
602
927
  },
603
928
  onUsage: (payload) => {
604
- state.totalTokens = payload.total.totalTokens;
605
- },
606
- tick: () => {
607
- state.spinnerIndex = (state.spinnerIndex + 1) % Math.max(options.spinnerFrames.length, 1);
929
+ summary.onUsage?.(payload);
930
+ renderer.render();
608
931
  }
609
932
  };
610
933
  }
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)}…`;
934
+ //#endregion
935
+ //#region src/cli/commands/lint/discovery.ts
936
+ async function resolveLintFiles(files, config, cwd) {
937
+ const gitignore = shouldFilterGitignoredFiles(config) ? new Gitignore() : void 0;
938
+ const candidates = files.length > 0 ? files : await discoverLintFiles(config, cwd, gitignore);
939
+ if (!gitignore || candidates.length === 0) return candidates;
940
+ const lintFiles = [];
941
+ for (const file of candidates) {
942
+ if (await gitignore.ignores(resolve(cwd, file))) continue;
943
+ lintFiles.push(file);
944
+ }
945
+ return lintFiles;
648
946
  }
649
- function formatDuration(ms) {
650
- if (ms === void 0 || !Number.isFinite(ms)) return "?";
651
- return `${(Math.max(ms, 0) / 1e3).toFixed(1)}s`;
947
+ function collectGlobalIgnorePatterns(config) {
948
+ return normalizeConfig(config).flatMap((item) => isGlobalIgnoreItem(item) ? [...item.ignores] : []);
949
+ }
950
+ async function discoverLintFiles(config, cwd, gitignore) {
951
+ if (!hasDiscoveryFilePatterns(config)) return [];
952
+ const candidates = (await walkFiles(cwd, {
953
+ cwd,
954
+ gitignore,
955
+ ignoredPatterns: collectGlobalIgnorePatterns(config)
956
+ })).map((file) => normalizeRelativePath(cwd, file)).filter((file) => matchesDiscoveryFile(file, config, { cwd }));
957
+ return [...new Set(candidates)].sort();
958
+ }
959
+ function isGlobalIgnoreItem(item) {
960
+ const keys = Object.keys(item).filter((key) => item[key] !== void 0);
961
+ return item.ignores !== void 0 && keys.every((key) => key === "ignores" || key === "name");
962
+ }
963
+ function matchesIgnoredDirectory(relativePath, patterns) {
964
+ return patterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }) || minimatch(`${relativePath}/`, pattern, { dot: true }) || minimatch(`${relativePath}/__alint__`, pattern, { dot: true }));
965
+ }
966
+ function normalizeRelativePath(cwd, filePath) {
967
+ return relative(cwd, filePath).replaceAll("\\", "/");
968
+ }
969
+ function shouldFilterGitignoredFiles(config) {
970
+ return normalizeConfig(config).some((item) => item.ignore?.gitignore === true);
971
+ }
972
+ async function shouldPruneDirectory(path, options) {
973
+ if (matchesIgnoredDirectory(normalizeRelativePath(options.cwd, path), options.ignoredPatterns)) return true;
974
+ return await options.gitignore?.ignores(path) === true;
975
+ }
976
+ async function walkFiles(root, options) {
977
+ const entries = await readdir(root, { withFileTypes: true });
978
+ const files = [];
979
+ for (const entry of entries) {
980
+ const path = resolve(root, entry.name);
981
+ if (entry.isDirectory()) {
982
+ if (!await shouldPruneDirectory(path, options)) files.push(...await walkFiles(path, options));
983
+ continue;
984
+ }
985
+ if (entry.isFile()) files.push(path);
986
+ }
987
+ return files;
652
988
  }
653
- function formatEstimatedDuration(ms) {
654
- return `~${formatDuration(ms)}`;
989
+ //#endregion
990
+ //#region src/cli/commands/lint/errors.ts
991
+ function formatRunError(error, color) {
992
+ return `${color ? c.red("error") : "error"} ${formatRunErrorContext(error)}\n Rule running failed due to ${error.failure?.message ?? error.message}\n`;
655
993
  }
656
- function formatFilePath(filePath, cwd) {
657
- if (!cwd) return filePath;
658
- return relative(cwd, filePath) || filePath;
994
+ function formatRunErrorContext(error) {
995
+ const failure = error.failure;
996
+ if (!failure) return "alint run failed";
997
+ const target = failure.target ? failure.target.name ? `${failure.target.kind} ${failure.target.name}` : failure.target.kind : void 0;
998
+ return [
999
+ failure.filePath,
1000
+ target,
1001
+ failure.ruleId
1002
+ ].filter(Boolean).join(" > ");
659
1003
  }
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)];
1004
+ //#endregion
1005
+ //#region src/cli/commands/lint/runner.ts
1006
+ function resolveConfigRunner(config) {
1007
+ return normalizeConfig(config).reduce((merged, item) => item.runner ? {
1008
+ ...merged,
1009
+ ...item.runner
1010
+ } : merged, void 0);
667
1011
  }
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);
1012
+ function resolveRunnerConfig(setupConfig, config, options) {
1013
+ const cache = resolveRunnerCacheConfig(setupConfig.runner?.cache, config.runner?.cache, options);
1014
+ const fileConcurrency = parsePositiveIntegerOption(options.fileConcurrency, "--file-concurrency");
1015
+ const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
1016
+ const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
1017
+ const runner = {
1018
+ ...setupConfig.runner ?? {},
1019
+ ...config.runner ?? {},
1020
+ cache,
1021
+ fileConcurrency: fileConcurrency ?? config.runner?.fileConcurrency ?? setupConfig.runner?.fileConcurrency,
1022
+ ruleConcurrency: ruleConcurrency ?? config.runner?.ruleConcurrency ?? setupConfig.runner?.ruleConcurrency,
1023
+ timeoutMs: timeoutMs ?? config.runner?.timeoutMs ?? setupConfig.runner?.timeoutMs
1024
+ };
1025
+ return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
672
1026
  }
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);
1027
+ function mergeRunnerCacheConfig(setupCache, configCache) {
1028
+ if (configCache === void 0) return setupCache;
1029
+ if (typeof configCache === "boolean") return configCache;
1030
+ if (typeof setupCache === "object") return {
1031
+ ...setupCache,
1032
+ ...configCache
1033
+ };
1034
+ return configCache;
684
1035
  }
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);
1036
+ function parsePositiveIntegerOption(value, label) {
1037
+ if (value === void 0) return;
1038
+ const parsed = Number(value);
1039
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
1040
+ return parsed;
689
1041
  }
690
- function formatQueuedRow(queued, options) {
691
- return fitRow(` ${queued} ${queued === 1 ? "file" : "files"} queued`, options.columns);
1042
+ function resolveRunnerCacheConfig(setupCache, configCache, options) {
1043
+ if (options.cache === false) return false;
1044
+ const configuredCache = mergeRunnerCacheConfig(setupCache, configCache);
1045
+ if (options.cacheLocation !== void 0) return typeof configuredCache === "object" ? {
1046
+ ...configuredCache,
1047
+ location: options.cacheLocation
1048
+ } : { location: options.cacheLocation };
1049
+ return configuredCache;
692
1050
  }
693
- function formatTarget(payload) {
694
- return payload.path.target.name ? `${payload.path.target.kind} ${payload.path.target.name}` : payload.path.target.kind;
1051
+ //#endregion
1052
+ //#region src/cli/commands/lint/index.ts
1053
+ const lint = defineCommand({
1054
+ action: (context, files = [], options) => runLintCommand(files, {
1055
+ ...options,
1056
+ outputLanguage: options.lang ?? context.globalOptions.outputLanguage
1057
+ }, context.io, context.interceptConsoleOutput),
1058
+ alias: ["!"],
1059
+ arguments: "[...files]",
1060
+ default: true,
1061
+ description: "Run alint",
1062
+ name: "lint"
1063
+ });
1064
+ async function assertConfigExists(cwd, configPath) {
1065
+ const resolvedConfigPath = resolve(cwd, configPath);
1066
+ try {
1067
+ if (!(await stat(resolvedConfigPath)).isFile()) throw new Error(`Config file "${configPath}" is not a file.`);
1068
+ } catch (error) {
1069
+ if (isNodeError(error) && error.code === "ENOENT") throw new Error(`Config file "${configPath}" does not exist.`);
1070
+ throw error;
1071
+ }
695
1072
  }
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);
1073
+ function isNodeError(error) {
1074
+ return error instanceof Error && "code" in error;
698
1075
  }
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;
1076
+ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1077
+ if (options.config) await assertConfigExists(io.cwd, options.config);
1078
+ const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
1079
+ const lintFiles = await resolveLintFiles(files, config, io.cwd);
1080
+ const runner = resolveRunnerConfig(setupConfig, { runner: resolveConfigRunner(config) }, options);
1081
+ const progress = shouldEnableProgress(options, io) ? createCliProgressReporter({
1082
+ color: io.stderr.isTTY === true,
1083
+ columns: io.stderr.columns ?? 80,
1084
+ cwd: io.cwd,
1085
+ isTty: io.stderr.isTTY === true,
1086
+ write: (chunk) => io.stderr.write(chunk)
1087
+ }) : void 0;
1088
+ const restoreProgressConsole = progress ? interceptConsoleOutput({ write: progress.write }) : void 0;
1089
+ let result;
1090
+ try {
1091
+ result = await runAlint({
1092
+ config,
1093
+ cwd: io.cwd,
1094
+ files: lintFiles,
1095
+ modelOverride: options.model,
1096
+ outputLanguage: options.outputLanguage,
1097
+ progress: progress?.reporter,
1098
+ runner,
1099
+ setupConfig
1100
+ });
1101
+ } catch (error) {
1102
+ restoreProgressConsole?.();
1103
+ progress?.dispose();
1104
+ if (error instanceof AlintRunError) {
1105
+ io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
1106
+ return 2;
1107
+ }
1108
+ throw error;
708
1109
  }
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;
1110
+ restoreProgressConsole?.();
1111
+ progress?.dispose();
1112
+ io.stdout.write(formatDiagnostics(options.format, result, { color: io.stdout.isTTY === true }));
1113
+ return result.diagnostics.length > 0 ? 1 : 0;
715
1114
  }
716
- function replaceFirst(row, search, replacement) {
717
- if (search.length === 0) return row;
718
- return row.replace(new RegExp(escapeRegExp(search)), replacement);
1115
+ function shouldEnableProgress(options, io) {
1116
+ if (options.progress !== void 0) return options.progress;
1117
+ return options.format === "stylish" && io.stderr.isTTY === true;
719
1118
  }
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;
1119
+ //#endregion
1120
+ //#region src/cli/commands/output-inspect/inspect.ts
1121
+ const inspect$1 = defineCommand({
1122
+ action: (context, file, options) => inspectOutputFile(context.io, file, options),
1123
+ arguments: "<file>",
1124
+ description: "Inspect saved alint JSON output",
1125
+ examples: [
1126
+ ["# Pretty-print saved JSON output", "alint output inspect alint-output.json"].join("\n"),
1127
+ ["# Validate and reprint normalized JSON", "alint output inspect alint-output.json --format json"].join("\n"),
1128
+ [
1129
+ "# Save a run as JSON, then inspect it later",
1130
+ "alint --format json src > alint-output.json",
1131
+ "alint output inspect alint-output.json"
1132
+ ].join("\n")
1133
+ ],
1134
+ help: ["Read a saved alint JSON run result and render it with a reporter.", "Defaults to the human-friendly stylish reporter, which groups diagnostics by file and prints the same summary style as a normal alint run. Use `--format json` to validate the file and reprint normalized JSON."].join("\n\n"),
1135
+ name: "inspect",
1136
+ options: [{
1137
+ config: { default: "stylish" },
1138
+ description: "Reporter used to render the parsed run result. One of: stylish, json",
1139
+ flags: "-f, --format <format>"
1140
+ }]
1141
+ });
1142
+ async function inspectOutputFile(io, file, options) {
1143
+ const filePath = resolve(io.cwd, file);
1144
+ let text;
1145
+ try {
1146
+ text = await readFile(filePath, "utf8");
1147
+ } catch (error) {
1148
+ io.stderr.write(`Could not read output file "${file}": ${errorMessageFrom$1(error)}\n`);
1149
+ return 2;
1150
+ }
1151
+ let parsed;
1152
+ try {
1153
+ parsed = JSON.parse(text);
1154
+ } catch (error) {
1155
+ io.stderr.write(`Could not parse output file "${file}": ${errorMessageFrom$1(error)}\n`);
1156
+ return 2;
1157
+ }
1158
+ if (!isRunResult(parsed)) {
1159
+ io.stderr.write(`Invalid alint output "${file}": expected a run result with diagnostics and usage.\n`);
1160
+ return 2;
1161
+ }
1162
+ try {
1163
+ io.stdout.write(formatDiagnostics(options.format, parsed, { color: io.stdout.isTTY === true }));
1164
+ } catch (error) {
1165
+ io.stderr.write(`${errorMessageFrom$1(error)}\n`);
1166
+ return 2;
1167
+ }
1168
+ return parsed.diagnostics.length > 0 ? 1 : 0;
1169
+ }
1170
+ function isDiagnostic(value) {
1171
+ if (!isRecord(value)) return false;
1172
+ if (typeof value.filePath !== "string" || typeof value.message !== "string" || typeof value.ruleId !== "string" || value.severity !== "warn" && value.severity !== "error") return false;
1173
+ if (value.loc === void 0) return true;
1174
+ if (!isRecord(value.loc)) return false;
1175
+ if (value.loc.start === void 0) return true;
1176
+ if (!isRecord(value.loc.start)) return false;
1177
+ return (value.loc.start.line === void 0 || typeof value.loc.start.line === "number") && (value.loc.start.column === void 0 || typeof value.loc.start.column === "number");
1178
+ }
1179
+ function isRecord(value) {
1180
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1181
+ }
1182
+ function isRunResult(value) {
1183
+ if (!isRecord(value)) return false;
1184
+ if (!Array.isArray(value.diagnostics) || !value.diagnostics.every(isDiagnostic)) return false;
1185
+ return isUsage(value.usage);
1186
+ }
1187
+ function isUsage(value) {
1188
+ return isRecord(value) && typeof value.inputTokens === "number" && typeof value.outputTokens === "number" && Array.isArray(value.records) && typeof value.totalTokens === "number";
726
1189
  }
727
1190
  //#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();
1191
+ //#region src/cli/commands/output-inspect/output.ts
1192
+ const output = defineCommand({
1193
+ children: [inspect$1],
1194
+ description: "Inspect alint output files",
1195
+ help: ["Inspect saved alint run outputs without rerunning rules or model calls.", "Use this when you already have JSON from `alint --format json` and want to render, validate, or transform that result for humans or tools."].join("\n\n"),
1196
+ name: "output"
1197
+ });
1198
+ //#endregion
1199
+ //#region src/cli/commands/setup/interactive.ts
1200
+ const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
1201
+ const backValue = "__alint_back__";
1202
+ function formatProbeModelsFailure(endpoint, error) {
1203
+ const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
1204
+ return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
1205
+ }
1206
+ function isBackInput(value) {
1207
+ return value.trim() === "..";
1208
+ }
1209
+ async function runInteractiveSetup(io) {
1210
+ if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
1211
+ io.stderr.write(nonTtyMessage);
1212
+ return 2;
1213
+ }
1214
+ const prompts = await import("@clack/prompts");
1215
+ const cancelPrompt = () => {
1216
+ prompts.cancel("Setup cancelled.");
1217
+ return 1;
753
1218
  };
754
- return {
755
- finish: () => {
756
- if (interval) {
757
- options.clearInterval(interval);
758
- interval = void 0;
1219
+ prompts.intro("alint setup");
1220
+ const draft = {};
1221
+ let step = "scope";
1222
+ while (true) {
1223
+ if (step === "scope") {
1224
+ const scope = await prompts.select({
1225
+ message: "Where should alint write setup config?",
1226
+ options: [{
1227
+ label: "Global",
1228
+ value: "global"
1229
+ }, {
1230
+ label: "Local project",
1231
+ value: "local"
1232
+ }]
1233
+ });
1234
+ if (prompts.isCancel(scope)) return cancelPrompt();
1235
+ draft.scope = scope;
1236
+ step = "source";
1237
+ continue;
1238
+ }
1239
+ if (step === "source") {
1240
+ const source = await prompts.select({
1241
+ message: "Choose provider setup mode.",
1242
+ options: withBackOption([
1243
+ {
1244
+ label: "Custom OpenAI-compatible provider",
1245
+ value: "custom"
1246
+ },
1247
+ {
1248
+ label: "Ollama",
1249
+ value: "ollama"
1250
+ },
1251
+ {
1252
+ label: "Manual model entry",
1253
+ value: "manual"
1254
+ }
1255
+ ])
1256
+ });
1257
+ if (prompts.isCancel(source)) return cancelPrompt();
1258
+ if (source === backValue) {
1259
+ step = "scope";
1260
+ continue;
759
1261
  }
760
- clearPreviousFrame();
761
- },
762
- render,
763
- start: () => {
764
- if (!interval) {
765
- interval = options.createInterval(render, options.intervalMs);
766
- if (isUnrefableInterval(interval)) interval.unref();
1262
+ draft.source = source;
1263
+ step = "endpoint";
1264
+ continue;
1265
+ }
1266
+ if (step === "endpoint") {
1267
+ const endpoint = await promptEndpoint(prompts, draft.source ?? "custom");
1268
+ if (prompts.isCancel(endpoint)) return cancelPrompt();
1269
+ if (typeof endpoint !== "string") return cancelPrompt();
1270
+ if (isBackInput(endpoint)) {
1271
+ step = "source";
1272
+ continue;
767
1273
  }
768
- render();
769
- },
770
- write
771
- };
1274
+ draft.endpoint = endpoint;
1275
+ step = "providerId";
1276
+ continue;
1277
+ }
1278
+ if (step === "providerId") {
1279
+ const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
1280
+ const providerId = await prompts.text({
1281
+ defaultValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
1282
+ message: "Provider id",
1283
+ placeholder: "Type .. to go back",
1284
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
1285
+ });
1286
+ if (prompts.isCancel(providerId)) return cancelPrompt();
1287
+ if (typeof providerId !== "string") return cancelPrompt();
1288
+ if (isBackInput(providerId)) {
1289
+ step = "endpoint";
1290
+ continue;
1291
+ }
1292
+ draft.providerId = providerId;
1293
+ step = "headers";
1294
+ continue;
1295
+ }
1296
+ if (step === "headers") {
1297
+ const headerInput = await prompts.text({
1298
+ defaultValue: draft.headerInput ?? "",
1299
+ message: "Headers",
1300
+ placeholder: "Authorization=Bearer token, X-Test=true; type .. to go back",
1301
+ validate: (value) => {
1302
+ if (isBackInput(value ?? "")) return;
1303
+ try {
1304
+ parseHeaderList(splitHeaderInput(value ?? ""));
1305
+ return;
1306
+ } catch {
1307
+ return "Headers must be comma-separated Key=Value entries.";
1308
+ }
1309
+ }
1310
+ });
1311
+ if (prompts.isCancel(headerInput)) return cancelPrompt();
1312
+ if (typeof headerInput !== "string") return cancelPrompt();
1313
+ if (isBackInput(headerInput)) {
1314
+ step = "providerId";
1315
+ continue;
1316
+ }
1317
+ draft.headerInput = headerInput;
1318
+ draft.headers = parseHeaderList(splitHeaderInput(headerInput));
1319
+ draft.discoveredModels = draft.source === "manual" ? [] : await probeModelsWithSpinner(prompts, draft.endpoint ?? "", draft.headers);
1320
+ step = "models";
1321
+ continue;
1322
+ }
1323
+ if (step === "models") {
1324
+ const selectedModels = await promptModels(prompts, draft.discoveredModels ?? []);
1325
+ if (prompts.isCancel(selectedModels)) return cancelPrompt();
1326
+ if (selectedModels === backValue) {
1327
+ step = "headers";
1328
+ continue;
1329
+ }
1330
+ if (!Array.isArray(selectedModels)) return cancelPrompt();
1331
+ draft.selectedModels = selectedModels;
1332
+ step = "defaultAlias";
1333
+ continue;
1334
+ }
1335
+ if (step === "defaultAlias") {
1336
+ const addDefaultAlias = await prompts.select({
1337
+ message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
1338
+ options: withBackOption([{
1339
+ label: "Yes",
1340
+ value: "yes"
1341
+ }, {
1342
+ label: "No",
1343
+ value: "no"
1344
+ }])
1345
+ });
1346
+ if (prompts.isCancel(addDefaultAlias)) return cancelPrompt();
1347
+ if (addDefaultAlias === backValue) {
1348
+ step = "models";
1349
+ continue;
1350
+ }
1351
+ draft.addDefaultAlias = addDefaultAlias === "yes";
1352
+ step = "confirm";
1353
+ continue;
1354
+ }
1355
+ const nextProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? [], draft.addDefaultAlias ?? true);
1356
+ const confirmed = await prompts.select({
1357
+ message: [
1358
+ `Write ${draft.scope} setup config?`,
1359
+ `Provider: ${nextProvider.id}`,
1360
+ `Endpoint: ${nextProvider.endpoint}`,
1361
+ `Models: ${(draft.selectedModels ?? []).join(", ")}`
1362
+ ].join("\n"),
1363
+ options: withBackOption([{
1364
+ label: "Yes",
1365
+ value: "yes"
1366
+ }, {
1367
+ label: "No",
1368
+ value: "no"
1369
+ }])
1370
+ });
1371
+ if (prompts.isCancel(confirmed)) return cancelPrompt();
1372
+ if (confirmed === backValue) {
1373
+ step = "defaultAlias";
1374
+ continue;
1375
+ }
1376
+ if (confirmed === "no") return cancelPrompt();
1377
+ const configPath = getConfigPath(io, draft.scope ?? "global");
1378
+ await writeSetupConfig(configPath, mergeSetupConfigs(await loadSetupConfig(configPath), {
1379
+ providers: [nextProvider],
1380
+ version: 1
1381
+ }));
1382
+ prompts.outro(`Wrote ${configPath}`);
1383
+ return 0;
1384
+ }
772
1385
  }
773
- function isUnrefableInterval(interval) {
774
- if (typeof interval !== "object" || interval === null || !("unref" in interval)) return false;
775
- return typeof interval.unref === "function";
1386
+ function withBackOption(options) {
1387
+ return [...options, {
1388
+ label: "Back",
1389
+ value: backValue
1390
+ }];
776
1391
  }
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
1392
+ function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
1393
+ return {
1394
+ endpoint,
1395
+ headers,
1396
+ id: providerId,
1397
+ models: modelIds.map((modelId, index) => ({
1398
+ aliases: index === 0 && addDefaultAlias ? ["default"] : void 0,
1399
+ id: modelId,
1400
+ name: modelId
1401
+ })),
1402
+ type: "openai-compatible"
784
1403
  };
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
1404
+ }
1405
+ function getConfigPath(io, scope) {
1406
+ return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
1407
+ }
1408
+ async function probeModelsWithSpinner(prompts, endpoint, headers) {
1409
+ const spinner = prompts.spinner();
1410
+ spinner.start("Probing models");
1411
+ try {
1412
+ const models = await probeModels(endpoint, headers ?? {});
1413
+ spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
1414
+ return models;
1415
+ } catch (error) {
1416
+ spinner.stop(formatProbeModelsFailure(endpoint, error));
1417
+ return [];
1418
+ }
1419
+ }
1420
+ async function promptEndpoint(prompts, source) {
1421
+ return prompts.text({
1422
+ defaultValue: source === "ollama" ? "http://localhost:11434/v1" : void 0,
1423
+ message: "Provider endpoint",
1424
+ placeholder: source === "ollama" ? "http://localhost:11434/v1; type .. to go back" : "https://example.test/v1; type .. to go back",
1425
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
800
1426
  });
801
- const reporter = createRenderingProgressReporter(summary, renderer);
802
- return {
803
- dispose: renderer.finish,
804
- reporter,
805
- write: renderer.write
806
- };
807
1427
  }
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();
1428
+ async function promptModels(prompts, discoveredModels) {
1429
+ if (discoveredModels.length > 0) {
1430
+ const selectedModels = await prompts.multiselect({
1431
+ message: "Select models",
1432
+ options: withBackOption(discoveredModels.map((model) => ({
1433
+ label: model,
1434
+ value: model
1435
+ }))),
1436
+ required: true
1437
+ });
1438
+ return Array.isArray(selectedModels) && selectedModels.includes(backValue) ? backValue : selectedModels;
1439
+ }
1440
+ const modelInput = await prompts.text({
1441
+ message: "Models",
1442
+ placeholder: "qwen:8b, qwen:32b; type .. to go back",
1443
+ validate: (value) => isBackInput(value ?? "") || splitModelInput(value ?? "").length > 0 ? void 0 : "At least one model is required."
1444
+ });
1445
+ if (prompts.isCancel(modelInput)) return modelInput;
1446
+ return isBackInput(modelInput) ? backValue : splitModelInput(modelInput);
1447
+ }
1448
+ function splitHeaderInput(value) {
1449
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1450
+ }
1451
+ function splitModelInput(value) {
1452
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1453
+ }
1454
+ //#endregion
1455
+ //#region src/cli/commands/setup/index.ts
1456
+ const setup = defineCommand({
1457
+ action: (context, options) => runSetupCommand({
1458
+ ...options,
1459
+ noInteractive: context.setupNoInteractive
1460
+ }, context.io),
1461
+ description: "Write alint provider configuration",
1462
+ examples: [
1463
+ ["# Configure a provider interactively", "alint setup"].join("\n"),
1464
+ ["# Write a project-local provider config without prompts", "alint setup --local -N --provider-id openrouter --provider-endpoint https://openrouter.ai/api/v1 --provider-model z-ai/glm-5.2"].join("\n"),
1465
+ ["# Add an authorization header for an OpenAI-compatible provider", "alint setup --local -N --provider-id openrouter --provider-endpoint https://openrouter.ai/api/v1 --provider-model z-ai/glm-5.2 --provider-header \"Authorization=Bearer $OPENROUTER_API_KEY\""].join("\n")
1466
+ ],
1467
+ help: ["Write alint provider and model configuration.", "Run without flags for interactive setup, or use `-N` with provider flags in scripts and CI. Use `--local` when the provider config should live with the current project instead of the user-level setup file."].join("\n\n"),
1468
+ name: "setup",
1469
+ options: [
1470
+ {
1471
+ description: "Write project-local config",
1472
+ flags: "--local"
829
1473
  },
830
- onRunEnd: (payload) => {
831
- summary.onRunEnd?.(payload);
832
- renderer.render();
1474
+ {
1475
+ description: "Disable interactive setup",
1476
+ flags: "-N, --no-interactive"
833
1477
  },
834
- onRunStart: (payload) => {
835
- summary.onRunStart?.(payload);
836
- renderer.start();
1478
+ {
1479
+ description: "Provider endpoint",
1480
+ flags: "--provider-endpoint <endpoint>"
837
1481
  },
838
- onTargetEnd: (payload) => {
839
- summary.onTargetEnd?.(payload);
840
- renderer.render();
1482
+ {
1483
+ description: "Provider id",
1484
+ flags: "--provider-id <id>"
841
1485
  },
842
- onTargetStart: (payload) => {
843
- summary.onTargetStart?.(payload);
844
- renderer.render();
1486
+ {
1487
+ description: "Provider model",
1488
+ flags: "--provider-model <model>"
845
1489
  },
846
- onUsage: (payload) => {
847
- summary.onUsage?.(payload);
848
- renderer.render();
1490
+ {
1491
+ description: "Provider header",
1492
+ flags: "--provider-header <Key=Value>"
849
1493
  }
1494
+ ]
1495
+ });
1496
+ function createSetupConfig(providerId, providerEndpoint, options) {
1497
+ const models = toArray(options.providerModel).map((model) => ({
1498
+ id: model,
1499
+ name: model
1500
+ }));
1501
+ return {
1502
+ providers: [{
1503
+ endpoint: providerEndpoint,
1504
+ headers: parseHeaderList(toArray(options.providerHeader)),
1505
+ id: providerId,
1506
+ models,
1507
+ type: "openai-compatible"
1508
+ }],
1509
+ version: 1
850
1510
  };
851
1511
  }
1512
+ async function runSetupCommand(options, io) {
1513
+ if (!options.providerEndpoint) {
1514
+ if (options.noInteractive !== true) return runInteractiveSetup({
1515
+ ...io,
1516
+ stdin: io.stdin ?? process.stdin
1517
+ });
1518
+ io.stderr.write("setup requires --provider-endpoint in --no-interactive mode.\n");
1519
+ return 2;
1520
+ }
1521
+ if (!options.providerId) {
1522
+ io.stderr.write("setup requires --provider-id in --no-interactive mode.\n");
1523
+ return 2;
1524
+ }
1525
+ const setupConfigPath = options.local ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
1526
+ await writeSetupConfig(setupConfigPath, mergeSetupConfigs(await loadSetupConfig(setupConfigPath), createSetupConfig(options.providerId, options.providerEndpoint, options)));
1527
+ return 0;
1528
+ }
1529
+ function toArray(value) {
1530
+ if (value === void 0) return [];
1531
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
1532
+ }
1533
+ //#endregion
1534
+ //#region src/cli/commands/index.ts
1535
+ const commandTree = [
1536
+ setup,
1537
+ config,
1538
+ output,
1539
+ lint
1540
+ ];
852
1541
  //#endregion
853
1542
  //#region src/cli/cli.ts
854
1543
  async function executeCli(argv, io) {
855
1544
  const cli = cac("alint");
856
1545
  const setupNoInteractive = argv.includes("-N") || argv.includes("--no-interactive");
1546
+ const globalOptions = { outputLanguage: parseStringOption(argv, ["--lang", "-l"]) };
857
1547
  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;
1548
+ const setPendingResult = (result) => {
1549
+ pendingResult = result;
1550
+ return result;
1551
+ };
1552
+ 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("-l, --lang <language>", "Ask model-backed rules to write diagnostics in this language").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();
1553
+ registerCommandTree(cli, commandTree, {
1554
+ globalOptions,
1555
+ interceptConsoleOutput,
1556
+ io,
1557
+ setupNoInteractive
1558
+ }, setPendingResult, {
1559
+ examples: [
1560
+ ["# Configure a provider interactively", "alint setup"].join("\n"),
1561
+ ["# Run alint on source files with the default stylish reporter", "alint src"].join("\n"),
1562
+ [
1563
+ "# Run alint and save machine-readable JSON for later inspection",
1564
+ "alint --format json src > alint-output.json",
1565
+ "alint output inspect alint-output.json"
1566
+ ].join("\n"),
1567
+ ["# Inspect the effective config that applies to a file", "alint config inspect src/index.ts"].join("\n"),
1568
+ [
1569
+ "# List configured providers and models",
1570
+ "alint config providers list",
1571
+ "alint config models list"
1572
+ ].join("\n")
1573
+ ],
1574
+ help: ["AI-assisted linting for source files, saved run outputs, and provider/model setup.", "Start with `alint setup` to configure a model provider, run `alint <files>` to analyze files, and use `alint output inspect` to read saved JSON output without rerunning rules."].join("\n\n")
873
1575
  });
874
1576
  const restoreConsole = interceptConsoleOutput(shouldCaptureHelp(argv) ? io.stdout : io.stderr);
875
1577
  try {
@@ -879,28 +1581,6 @@ async function executeCli(argv, io) {
879
1581
  restoreConsole();
880
1582
  }
881
1583
  }
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
1584
  function interceptConsoleOutput(stdout) {
905
1585
  const cliConsole = globalThis.console;
906
1586
  const originalConsoleDebug = cliConsole.debug;
@@ -924,176 +1604,18 @@ function interceptConsoleOutput(stdout) {
924
1604
  cliConsole.log = originalConsoleLog;
925
1605
  };
926
1606
  }
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;
1607
+ function parseStringOption(argv, flags) {
1608
+ for (let index = 0; index < argv.length; index += 1) {
1609
+ const value = argv[index];
1610
+ for (const flag of flags) {
1611
+ const equalsPrefix = `${flag}=`;
1612
+ if (value?.startsWith(equalsPrefix)) return value.slice(equalsPrefix.length);
1613
+ if (value === flag) return argv[index + 1];
1025
1614
  }
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
1615
  }
1077
1616
  }
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
1617
  function shouldCaptureHelp(argv) {
1088
1618
  return argv.includes("--help") || argv.includes("-h");
1089
1619
  }
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
1620
  //#endregion
1099
1621
  export { formatJson as i, formatDiagnostics as n, formatStylish as r, executeCli as t };