@alint-js/cli 0.0.5 → 0.0.6

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-CoS-NVz_.mjs";
2
+ import { t as executeCli } from "../cli-C4PnKXoK.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -1,16 +1,96 @@
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 { 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
+ //#region src/cli/commands/command.ts
16
+ function defineCommand(node) {
17
+ return node;
18
+ }
19
+ function registerCommandTree(cli, nodes, context, setPendingResult) {
20
+ for (const node of nodes) registerRootCommand(cli, node, context, setPendingResult);
21
+ }
22
+ function collectCommandOptions(node) {
23
+ const options = /* @__PURE__ */ new Map();
24
+ for (const option of node.options ?? []) options.set(option.flags, option);
25
+ for (const child of node.children ?? []) for (const option of collectCommandOptions(child)) options.set(option.flags, option);
26
+ return [...options.values()];
27
+ }
28
+ function commandPattern(node) {
29
+ if (node.default) return node.arguments ?? node.name;
30
+ return [node.name, node.children ? "[...args]" : node.arguments].filter(Boolean).join(" ");
31
+ }
32
+ function dispatchCommand(context, node, args, options, path) {
33
+ const [subcommand, ...restArgs] = args;
34
+ const child = node.children?.find((item) => item.name === subcommand || item.alias?.includes(subcommand ?? ""));
35
+ if (child) return dispatchCommand(context, child, restArgs, options, [...path, child.name]);
36
+ if (!node.action) return Promise.resolve(reportUnknownCommand(context, path, args));
37
+ return Promise.resolve(node.action(context, ...parseCommandArguments(node, args), options));
38
+ }
39
+ function formatUnknownCommand(path, args) {
40
+ return [...path, ...args].filter(Boolean).join(" ");
41
+ }
42
+ function parseCommandArguments(node, args) {
43
+ if (!node.arguments) return [];
44
+ const parts = node.arguments.split(/\s+/u).filter(Boolean);
45
+ const values = [];
46
+ let argIndex = 0;
47
+ for (const part of parts) {
48
+ if (part.startsWith("[...") || part.startsWith("<...")) {
49
+ values.push(args.slice(argIndex));
50
+ argIndex = args.length;
51
+ continue;
52
+ }
53
+ if (part.startsWith("<") && argIndex >= args.length) throw new Error(`Missing required argument ${part}.`);
54
+ values.push(args[argIndex]);
55
+ argIndex += 1;
56
+ }
57
+ return values;
58
+ }
59
+ function registerRootCommand(cli, node, context, setPendingResult) {
60
+ const command = cli.command(commandPattern(node), node.description);
61
+ if (node.allowUnknownOptions || node.children) command.allowUnknownOptions();
62
+ for (const alias of node.alias ?? []) command.alias(alias);
63
+ for (const option of collectCommandOptions(node)) command.option(option.flags, option.description, option.config);
64
+ command.action((...args) => {
65
+ const options = args.at(-1);
66
+ return setPendingResult(node.children ? dispatchCommand(context, node, args[0] ?? [], options, [node.name]) : Promise.resolve(node.action?.(context, ...args.slice(0, -1), options) ?? 0));
67
+ });
68
+ }
69
+ function reportUnknownCommand(context, path, args) {
70
+ context.io.stderr.write(`unknown config command: ${formatUnknownCommand(path, args)}\n`);
71
+ return 2;
72
+ }
73
+ //#endregion
74
+ //#region src/cli/commands/config/inspect.ts
75
+ const inspect$1 = defineCommand({
76
+ action: (context, file, options) => runConfigInspectCommand(file, options.config, context.io),
77
+ arguments: "<file>",
78
+ description: "Inspect resolved config for a file",
79
+ name: "inspect"
80
+ });
81
+ async function runConfigInspectCommand(file, configPath, io) {
82
+ const config = await loadAlintConfig(io.cwd, configPath);
83
+ const result = resolveConfigForFile(resolve(io.cwd, file), config, { cwd: io.cwd });
84
+ io.stdout.write(`file: ${file}\n`);
85
+ io.stdout.write(`ignored: ${result.ignored ? "yes" : "no"}\n`);
86
+ io.stdout.write("matched:\n");
87
+ for (const item of result.matched) io.stdout.write(` - ${item.name ?? "<anonymous>"}\n`);
88
+ io.stdout.write(`language: ${result.config.language ?? "<inferred>"}\n`);
89
+ io.stdout.write("rules:\n");
90
+ for (const [id, entry] of Object.entries(result.config.rules)) io.stdout.write(` ${id}: ${Array.isArray(entry) ? entry[0] : entry}\n`);
91
+ return 0;
92
+ }
93
+ //#endregion
14
94
  //#region src/cli/provider-registry.ts
15
95
  function buildModelsUrl(endpoint) {
16
96
  return new URL("models", endpoint.endsWith("/") ? endpoint : `${endpoint}/`).toString();
@@ -116,335 +196,186 @@ function formatTable(rows) {
116
196
  });
117
197
  }
118
198
  //#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}`;
199
+ //#region src/cli/commands/config/setup-config.ts
200
+ async function loadMergedSetupConfig(io) {
201
+ const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
202
+ const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
203
+ const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
204
+ return mergeSetupConfigs(globalSetupConfig, projectSetupConfig);
125
205
  }
126
- function isBackInput(value) {
127
- return value.trim() === "..";
206
+ //#endregion
207
+ //#region src/cli/commands/config/models/ls.ts
208
+ const ls$1 = defineCommand({
209
+ async action(context) {
210
+ context.io.stdout.write(formatModelList(await loadMergedSetupConfig(context.io)));
211
+ return 0;
212
+ },
213
+ alias: ["ls"],
214
+ description: "List configured models",
215
+ name: "list"
216
+ });
217
+ //#endregion
218
+ //#region src/cli/commands/config/probe.ts
219
+ function providerHeadersFromOptions(options) {
220
+ return parseHeaderList(toArray$1(options.providerHeader)) ?? {};
128
221
  }
129
- async function runInteractiveSetup(io) {
130
- if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
131
- io.stderr.write(nonTtyMessage);
132
- return 2;
133
- }
134
- const prompts = await import("@clack/prompts");
135
- const cancelPrompt = () => {
136
- prompts.cancel("Setup cancelled.");
137
- return 1;
138
- };
139
- prompts.intro("alint setup");
140
- const draft = {};
141
- let step = "scope";
142
- while (true) {
143
- if (step === "scope") {
144
- const scope = await prompts.select({
145
- message: "Where should alint write setup config?",
146
- options: [{
147
- label: "Global",
148
- value: "global"
149
- }, {
150
- label: "Local project",
151
- value: "local"
152
- }]
153
- });
154
- if (prompts.isCancel(scope)) return cancelPrompt();
155
- draft.scope = scope;
156
- step = "source";
157
- continue;
158
- }
159
- if (step === "source") {
160
- const source = await prompts.select({
161
- message: "Choose provider setup mode.",
162
- options: withBackOption([
163
- {
164
- label: "Custom OpenAI-compatible provider",
165
- value: "custom"
166
- },
167
- {
168
- label: "Ollama",
169
- value: "ollama"
170
- },
171
- {
172
- label: "Manual model entry",
173
- value: "manual"
174
- }
175
- ])
176
- });
177
- if (prompts.isCancel(source)) return cancelPrompt();
178
- if (source === backValue) {
179
- step = "scope";
180
- continue;
181
- }
182
- draft.source = source;
183
- step = "endpoint";
184
- continue;
185
- }
186
- if (step === "endpoint") {
187
- const endpoint = await promptEndpoint(prompts, draft.source ?? "custom");
188
- if (prompts.isCancel(endpoint)) return cancelPrompt();
189
- if (typeof endpoint !== "string") return cancelPrompt();
190
- if (isBackInput(endpoint)) {
191
- step = "source";
192
- continue;
193
- }
194
- draft.endpoint = endpoint;
195
- step = "providerId";
196
- continue;
197
- }
198
- if (step === "providerId") {
199
- const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
200
- const providerId = await prompts.text({
201
- defaultValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
202
- message: "Provider id",
203
- placeholder: "Type .. to go back",
204
- validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
205
- });
206
- if (prompts.isCancel(providerId)) return cancelPrompt();
207
- if (typeof providerId !== "string") return cancelPrompt();
208
- if (isBackInput(providerId)) {
209
- step = "endpoint";
210
- continue;
211
- }
212
- draft.providerId = providerId;
213
- step = "headers";
214
- continue;
215
- }
216
- if (step === "headers") {
217
- const headerInput = await prompts.text({
218
- defaultValue: draft.headerInput ?? "",
219
- message: "Headers",
220
- placeholder: "Authorization=Bearer token, X-Test=true; type .. to go back",
221
- validate: (value) => {
222
- if (isBackInput(value ?? "")) return;
223
- try {
224
- parseHeaderList(splitHeaderInput(value ?? ""));
225
- return;
226
- } catch {
227
- return "Headers must be comma-separated Key=Value entries.";
228
- }
222
+ function toArray$1(value) {
223
+ if (value === void 0) return [];
224
+ return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
225
+ }
226
+ //#endregion
227
+ //#region src/cli/commands/config/models/models.ts
228
+ const models = defineCommand({
229
+ children: [
230
+ defineCommand({
231
+ async action(context, options) {
232
+ if (!options.endpoint) {
233
+ context.io.stderr.write("config models probe requires --endpoint.\n");
234
+ return 2;
229
235
  }
230
- });
231
- if (prompts.isCancel(headerInput)) return cancelPrompt();
232
- if (typeof headerInput !== "string") return cancelPrompt();
233
- if (isBackInput(headerInput)) {
234
- step = "providerId";
235
- continue;
236
- }
237
- draft.headerInput = headerInput;
238
- draft.headers = parseHeaderList(splitHeaderInput(headerInput));
239
- draft.discoveredModels = draft.source === "manual" ? [] : await probeModelsWithSpinner(prompts, draft.endpoint ?? "", draft.headers);
240
- step = "models";
241
- continue;
236
+ try {
237
+ const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
238
+ context.io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
239
+ return 0;
240
+ } catch (error) {
241
+ context.io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
242
+ return 2;
243
+ }
244
+ },
245
+ description: "Probe OpenAI-compatible models",
246
+ name: "probe",
247
+ options: [{
248
+ description: "Provider endpoint",
249
+ flags: "--endpoint <url>"
250
+ }, {
251
+ description: "Provider header",
252
+ flags: "--provider-header <Key=Value>"
253
+ }]
254
+ }),
255
+ ls$1,
256
+ defineCommand({
257
+ async action(context, model) {
258
+ const candidate = findModel(await loadMergedSetupConfig(context.io), model);
259
+ if (candidate === void 0) {
260
+ context.io.stderr.write(`unknown model "${model}".\n`);
261
+ return 2;
262
+ }
263
+ context.io.stdout.write(formatModelShow(candidate));
264
+ return 0;
265
+ },
266
+ arguments: "<model>",
267
+ description: "Show configured model",
268
+ name: "show"
269
+ })
270
+ ],
271
+ description: "Manage configured models",
272
+ name: "models"
273
+ });
274
+ //#endregion
275
+ //#region src/cli/commands/config/providers/ls.ts
276
+ const ls = defineCommand({
277
+ async action(context) {
278
+ context.io.stdout.write(formatProviderList(await loadMergedSetupConfig(context.io)));
279
+ return 0;
280
+ },
281
+ alias: ["ls"],
282
+ description: "List configured providers",
283
+ name: "list"
284
+ });
285
+ //#endregion
286
+ //#region src/cli/commands/config/providers/probe.ts
287
+ const probe = defineCommand({
288
+ async action(context, options) {
289
+ if (!options.endpoint) {
290
+ context.io.stderr.write("config providers probe requires --endpoint.\n");
291
+ return 2;
242
292
  }
243
- if (step === "models") {
244
- const selectedModels = await promptModels(prompts, draft.discoveredModels ?? []);
245
- if (prompts.isCancel(selectedModels)) return cancelPrompt();
246
- if (selectedModels === backValue) {
247
- step = "headers";
248
- continue;
249
- }
250
- if (!Array.isArray(selectedModels)) return cancelPrompt();
251
- draft.selectedModels = selectedModels;
252
- step = "defaultAlias";
253
- continue;
293
+ try {
294
+ const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
295
+ context.io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
296
+ return 0;
297
+ } catch (error) {
298
+ context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
299
+ return 2;
254
300
  }
255
- if (step === "defaultAlias") {
256
- const addDefaultAlias = await prompts.select({
257
- message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
258
- options: withBackOption([{
259
- label: "Yes",
260
- value: "yes"
261
- }, {
262
- label: "No",
263
- value: "no"
264
- }])
265
- });
266
- if (prompts.isCancel(addDefaultAlias)) return cancelPrompt();
267
- if (addDefaultAlias === backValue) {
268
- step = "models";
269
- continue;
270
- }
271
- draft.addDefaultAlias = addDefaultAlias === "yes";
272
- step = "confirm";
301
+ },
302
+ description: "Probe provider reachability",
303
+ name: "probe",
304
+ options: [{
305
+ description: "Provider endpoint",
306
+ flags: "--endpoint <url>"
307
+ }, {
308
+ description: "Provider header",
309
+ flags: "--provider-header <Key=Value>"
310
+ }]
311
+ });
312
+ //#endregion
313
+ //#region src/cli/commands/config/config.ts
314
+ const config = defineCommand({
315
+ children: [
316
+ inspect$1,
317
+ models,
318
+ defineCommand({
319
+ children: [
320
+ ls,
321
+ defineCommand({
322
+ async action(context, providerId) {
323
+ const provider = (await loadMergedSetupConfig(context.io)).providers.find((item) => item.id === providerId);
324
+ if (provider === void 0) {
325
+ context.io.stderr.write(`unknown provider "${providerId}".\n`);
326
+ return 2;
327
+ }
328
+ context.io.stdout.write(formatProviderShow(provider));
329
+ return 0;
330
+ },
331
+ arguments: "<provider>",
332
+ description: "Show configured provider",
333
+ name: "show"
334
+ }),
335
+ probe
336
+ ],
337
+ description: "Manage configured providers",
338
+ name: "providers"
339
+ })
340
+ ],
341
+ description: "Manage alint configuration",
342
+ name: "config"
343
+ });
344
+ //#endregion
345
+ //#region src/cli/reporters/json.ts
346
+ function formatJson(result) {
347
+ return `${JSON.stringify(result, null, 2)}\n`;
348
+ }
349
+ //#endregion
350
+ //#region src/cli/reporters/stylish.ts
351
+ const colors$1 = createColors({ force: true });
352
+ function formatStylish(input, options = {}) {
353
+ const diagnostics = Array.isArray(input) ? input : input.diagnostics;
354
+ const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
355
+ if (diagnostics.length === 0) return "";
356
+ const diagnosticsByFile = /* @__PURE__ */ new Map();
357
+ for (const diagnostic of diagnostics) {
358
+ const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
359
+ if (fileDiagnostics) {
360
+ fileDiagnostics.push(diagnostic);
273
361
  continue;
274
362
  }
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;
363
+ diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
364
+ }
365
+ const lines = [];
366
+ const style = createStyle(options.color === true);
367
+ for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
368
+ lines.push(style.file(filePath));
369
+ for (const diagnostic of fileDiagnostics) {
370
+ const line = diagnostic.loc?.start.line ?? 0;
371
+ const column = diagnostic.loc?.start.column ?? 0;
372
+ const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
373
+ lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
295
374
  }
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;
375
+ lines.push("");
304
376
  }
305
- }
306
- function withBackOption(options) {
307
- return [...options, {
308
- label: "Back",
309
- value: backValue
310
- }];
311
- }
312
- function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
313
- return {
314
- endpoint,
315
- headers,
316
- id: providerId,
317
- models: modelIds.map((modelId, index) => ({
318
- aliases: index === 0 && addDefaultAlias ? ["default"] : void 0,
319
- id: modelId,
320
- name: modelId
321
- })),
322
- type: "openai-compatible"
323
- };
324
- }
325
- function getConfigPath(io, scope) {
326
- return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
327
- }
328
- async function probeModelsWithSpinner(prompts, endpoint, headers) {
329
- const spinner = prompts.spinner();
330
- spinner.start("Probing models");
331
- try {
332
- const models = await probeModels(endpoint, headers ?? {});
333
- spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
334
- return models;
335
- } catch (error) {
336
- spinner.stop(formatProbeModelsFailure(endpoint, error));
337
- return [];
338
- }
339
- }
340
- async function promptEndpoint(prompts, source) {
341
- return prompts.text({
342
- defaultValue: source === "ollama" ? "http://localhost:11434/v1" : void 0,
343
- message: "Provider endpoint",
344
- placeholder: source === "ollama" ? "http://localhost:11434/v1; type .. to go back" : "https://example.test/v1; type .. to go back",
345
- validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
346
- });
347
- }
348
- async function promptModels(prompts, discoveredModels) {
349
- if (discoveredModels.length > 0) {
350
- const selectedModels = await prompts.multiselect({
351
- message: "Select models",
352
- options: withBackOption(discoveredModels.map((model) => ({
353
- label: model,
354
- value: model
355
- }))),
356
- required: true
357
- });
358
- return Array.isArray(selectedModels) && selectedModels.includes(backValue) ? backValue : selectedModels;
359
- }
360
- const modelInput = await prompts.text({
361
- message: "Models",
362
- placeholder: "qwen:8b, qwen:32b; type .. to go back",
363
- validate: (value) => isBackInput(value ?? "") || splitModelInput(value ?? "").length > 0 ? void 0 : "At least one model is required."
364
- });
365
- if (prompts.isCancel(modelInput)) return modelInput;
366
- return isBackInput(modelInput) ? backValue : splitModelInput(modelInput);
367
- }
368
- function splitHeaderInput(value) {
369
- return value.split(",").map((item) => item.trim()).filter(Boolean);
370
- }
371
- function splitModelInput(value) {
372
- return value.split(",").map((item) => item.trim()).filter(Boolean);
373
- }
374
- //#endregion
375
- //#region src/cli/commands/setup/index.ts
376
- async function runSetupCommand(options, io) {
377
- if (!options.providerEndpoint) {
378
- if (options.noInteractive !== true) return runInteractiveSetup({
379
- ...io,
380
- stdin: io.stdin ?? process.stdin
381
- });
382
- io.stderr.write("setup requires --provider-endpoint in --no-interactive mode.\n");
383
- return 2;
384
- }
385
- if (!options.providerId) {
386
- io.stderr.write("setup requires --provider-id in --no-interactive mode.\n");
387
- return 2;
388
- }
389
- const setupConfigPath = options.local ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
390
- await writeSetupConfig(setupConfigPath, mergeSetupConfigs(await loadSetupConfig(setupConfigPath), createSetupConfig(options.providerId, options.providerEndpoint, options)));
391
- return 0;
392
- }
393
- function createSetupConfig(providerId, providerEndpoint, options) {
394
- const models = toArray$1(options.providerModel).map((model) => ({
395
- id: model,
396
- name: model
397
- }));
398
- return {
399
- providers: [{
400
- endpoint: providerEndpoint,
401
- headers: parseHeaderList(toArray$1(options.providerHeader)),
402
- id: providerId,
403
- models,
404
- type: "openai-compatible"
405
- }],
406
- version: 1
407
- };
408
- }
409
- function toArray$1(value) {
410
- if (value === void 0) return [];
411
- return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
412
- }
413
- //#endregion
414
- //#region src/cli/reporters/json.ts
415
- function formatJson(result) {
416
- return `${JSON.stringify(result, null, 2)}\n`;
417
- }
418
- //#endregion
419
- //#region src/cli/reporters/stylish.ts
420
- const colors$1 = createColors({ force: true });
421
- function formatStylish(input, options = {}) {
422
- const diagnostics = Array.isArray(input) ? input : input.diagnostics;
423
- const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
424
- if (diagnostics.length === 0) return "";
425
- const diagnosticsByFile = /* @__PURE__ */ new Map();
426
- for (const diagnostic of diagnostics) {
427
- const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
428
- if (fileDiagnostics) {
429
- fileDiagnostics.push(diagnostic);
430
- continue;
431
- }
432
- diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
433
- }
434
- const lines = [];
435
- const style = createStyle(options.color === true);
436
- for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
437
- lines.push(style.file(filePath));
438
- for (const diagnostic of fileDiagnostics) {
439
- const line = diagnostic.loc?.start.line ?? 0;
440
- const column = diagnostic.loc?.start.column ?? 0;
441
- const severity = diagnostic.severity === "warn" ? style.warning("warning") : style.error("error");
442
- lines.push(` ${style.location(`${line}:${column}`)} ${severity} ${diagnostic.message} ${style.ruleId(diagnostic.ruleId)}`);
443
- }
444
- lines.push("");
445
- }
446
- lines.push("", formatSummary(diagnostics, totalTokens, style));
447
- return `${lines.join("\n")}\n`;
377
+ lines.push("", formatSummary(diagnostics, totalTokens, style));
378
+ return `${lines.join("\n")}\n`;
448
379
  }
449
380
  function countDiagnostics$2(diagnostics, severity) {
450
381
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
@@ -655,7 +586,7 @@ function formatEstimatedDuration(ms) {
655
586
  }
656
587
  function formatFilePath(filePath, cwd) {
657
588
  if (!cwd) return filePath;
658
- return relative(cwd, filePath) || filePath;
589
+ return relative$1(cwd, filePath) || filePath;
659
590
  }
660
591
  function formatFileRows(file, state, options, now) {
661
592
  const firstRow = formatFileSummaryRow(file, state, options);
@@ -850,44 +781,62 @@ function createRenderingProgressReporter(summary, renderer) {
850
781
  };
851
782
  }
852
783
  //#endregion
853
- //#region src/cli/cli.ts
854
- async function executeCli(argv, io) {
855
- const cli = cac("alint");
856
- const setupNoInteractive = argv.includes("-N") || argv.includes("--no-interactive");
857
- let pendingResult;
858
- cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").help();
859
- cli.command("setup", "Write alint provider configuration").option("--local", "Write project-local config").option("-N, --no-interactive", "Disable interactive setup").option("--provider-endpoint <endpoint>", "Provider endpoint").option("--provider-id <id>", "Provider id").option("--provider-model <model>", "Provider model").option("--provider-header <Key=Value>", "Provider header").action((options) => {
860
- pendingResult = runSetupCommand({
861
- ...options,
862
- noInteractive: setupNoInteractive
863
- }, io);
864
- return pendingResult;
865
- });
866
- cli.command("config [...args]", "Manage alint configuration").option("--endpoint <url>", "Provider endpoint").option("--provider-header <Key=Value>", "Provider header").action((args, options) => {
867
- pendingResult = runConfigCommand(args, options, io);
868
- return pendingResult;
869
- });
870
- cli.command("[...files]", "Run alint").action((files = [], options) => {
871
- pendingResult = runDefaultCommand(files, options, io);
872
- return pendingResult;
873
- });
874
- const restoreConsole = interceptConsoleOutput(shouldCaptureHelp(argv) ? io.stdout : io.stderr);
875
- try {
876
- cli.parse(argv);
877
- return await (pendingResult ?? Promise.resolve(0));
878
- } finally {
879
- restoreConsole();
784
+ //#region src/cli/commands/lint/discovery.ts
785
+ async function resolveLintFiles(files, config, cwd) {
786
+ const gitignore = shouldFilterGitignoredFiles(config) ? new Gitignore() : void 0;
787
+ const candidates = files.length > 0 ? files : await discoverLintFiles(config, cwd, gitignore);
788
+ if (!gitignore || candidates.length === 0) return candidates;
789
+ const lintFiles = [];
790
+ for (const file of candidates) {
791
+ if (await gitignore.ignores(resolve(cwd, file))) continue;
792
+ lintFiles.push(file);
880
793
  }
794
+ return lintFiles;
881
795
  }
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;
796
+ function collectGlobalIgnorePatterns(config) {
797
+ return normalizeConfig(config).flatMap((item) => isGlobalIgnoreItem(item) ? [...item.ignores] : []);
798
+ }
799
+ async function discoverLintFiles(config, cwd, gitignore) {
800
+ if (!hasDiscoveryFilePatterns(config)) return [];
801
+ const candidates = (await walkFiles(cwd, {
802
+ cwd,
803
+ gitignore,
804
+ ignoredPatterns: collectGlobalIgnorePatterns(config)
805
+ })).map((file) => normalizeRelativePath(cwd, file)).filter((file) => matchesDiscoveryFile(file, config, { cwd }));
806
+ return [...new Set(candidates)].sort();
807
+ }
808
+ function isGlobalIgnoreItem(item) {
809
+ const keys = Object.keys(item).filter((key) => item[key] !== void 0);
810
+ return item.ignores !== void 0 && keys.every((key) => key === "ignores" || key === "name");
811
+ }
812
+ function matchesIgnoredDirectory(relativePath, patterns) {
813
+ return patterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }) || minimatch(`${relativePath}/`, pattern, { dot: true }) || minimatch(`${relativePath}/__alint__`, pattern, { dot: true }));
814
+ }
815
+ function normalizeRelativePath(cwd, filePath) {
816
+ return relative(cwd, filePath).replaceAll("\\", "/");
817
+ }
818
+ function shouldFilterGitignoredFiles(config) {
819
+ return normalizeConfig(config).some((item) => item.ignore?.gitignore === true);
820
+ }
821
+ async function shouldPruneDirectory(path, options) {
822
+ if (matchesIgnoredDirectory(normalizeRelativePath(options.cwd, path), options.ignoredPatterns)) return true;
823
+ return await options.gitignore?.ignores(path) === true;
824
+ }
825
+ async function walkFiles(root, options) {
826
+ const entries = await readdir(root, { withFileTypes: true });
827
+ const files = [];
828
+ for (const entry of entries) {
829
+ const path = resolve(root, entry.name);
830
+ if (entry.isDirectory()) {
831
+ if (!await shouldPruneDirectory(path, options)) files.push(...await walkFiles(path, options));
832
+ continue;
833
+ }
834
+ if (entry.isFile()) files.push(path);
889
835
  }
836
+ return files;
890
837
  }
838
+ //#endregion
839
+ //#region src/cli/commands/lint/errors.ts
891
840
  function formatRunError(error, color) {
892
841
  return `${color ? c.red("error") : "error"} ${formatRunErrorContext(error)}\n Rule running failed due to ${error.failure?.message ?? error.message}\n`;
893
842
  }
@@ -901,37 +850,28 @@ function formatRunErrorContext(error) {
901
850
  failure.ruleId
902
851
  ].filter(Boolean).join(" > ");
903
852
  }
904
- function interceptConsoleOutput(stdout) {
905
- const cliConsole = globalThis.console;
906
- const originalConsoleDebug = cliConsole.debug;
907
- const originalConsoleDir = cliConsole.dir;
908
- const originalConsoleInfo = console.info;
909
- const originalConsoleLog = cliConsole.log;
910
- const writeConsoleLine = (...args) => {
911
- stdout.write(`${args.map(String).join(" ")}\n`);
912
- };
913
- const writeConsoleDir = (item, options) => {
914
- stdout.write(`${inspect(item, options)}\n`);
915
- };
916
- cliConsole.debug = writeConsoleLine;
917
- cliConsole.dir = writeConsoleDir;
918
- console.info = writeConsoleLine;
919
- cliConsole.log = writeConsoleLine;
920
- return () => {
921
- cliConsole.debug = originalConsoleDebug;
922
- cliConsole.dir = originalConsoleDir;
923
- console.info = originalConsoleInfo;
924
- cliConsole.log = originalConsoleLog;
925
- };
926
- }
927
- function isNodeError(error) {
928
- return error instanceof Error && "code" in error;
853
+ //#endregion
854
+ //#region src/cli/commands/lint/runner.ts
855
+ function resolveConfigRunner(config) {
856
+ return normalizeConfig(config).reduce((merged, item) => item.runner ? {
857
+ ...merged,
858
+ ...item.runner
859
+ } : merged, void 0);
929
860
  }
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);
861
+ function resolveRunnerConfig(setupConfig, config, options) {
862
+ const cache = resolveRunnerCacheConfig(setupConfig.runner?.cache, config.runner?.cache, options);
863
+ const fileConcurrency = parsePositiveIntegerOption(options.fileConcurrency, "--file-concurrency");
864
+ const ruleConcurrency = parsePositiveIntegerOption(options.ruleConcurrency, "--rule-concurrency");
865
+ const timeoutMs = parsePositiveIntegerOption(options.timeoutMs, "--timeout-ms");
866
+ const runner = {
867
+ ...setupConfig.runner ?? {},
868
+ ...config.runner ?? {},
869
+ cache,
870
+ fileConcurrency: fileConcurrency ?? config.runner?.fileConcurrency ?? setupConfig.runner?.fileConcurrency,
871
+ ruleConcurrency: ruleConcurrency ?? config.runner?.ruleConcurrency ?? setupConfig.runner?.ruleConcurrency,
872
+ timeoutMs: timeoutMs ?? config.runner?.timeoutMs ?? setupConfig.runner?.timeoutMs
873
+ };
874
+ return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
935
875
  }
936
876
  function mergeRunnerCacheConfig(setupCache, configCache) {
937
877
  if (configCache === void 0) return setupCache;
@@ -948,16 +888,6 @@ function parsePositiveIntegerOption(value, label) {
948
888
  if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer.`);
949
889
  return parsed;
950
890
  }
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
891
  function resolveRunnerCacheConfig(setupCache, configCache, options) {
962
892
  if (options.cache === false) return false;
963
893
  const configuredCache = mergeRunnerCacheConfig(setupCache, configCache);
@@ -967,36 +897,33 @@ function resolveRunnerCacheConfig(setupCache, configCache, options) {
967
897
  } : { location: options.cacheLocation };
968
898
  return configuredCache;
969
899
  }
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;
900
+ //#endregion
901
+ //#region src/cli/commands/lint/index.ts
902
+ const lint = defineCommand({
903
+ action: (context, files = [], options) => runLintCommand(files, options, context.io, context.interceptConsoleOutput),
904
+ alias: ["!"],
905
+ arguments: "[...files]",
906
+ default: true,
907
+ description: "Run alint",
908
+ name: "lint"
909
+ });
910
+ async function assertConfigExists(cwd, configPath) {
911
+ const resolvedConfigPath = resolve(cwd, configPath);
912
+ try {
913
+ if (!(await stat(resolvedConfigPath)).isFile()) throw new Error(`Config file "${configPath}" is not a file.`);
914
+ } catch (error) {
915
+ if (isNodeError(error) && error.code === "ENOENT") throw new Error(`Config file "${configPath}" does not exist.`);
916
+ throw error;
917
+ }
984
918
  }
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;
919
+ function isNodeError(error) {
920
+ return error instanceof Error && "code" in error;
994
921
  }
995
- async function runDefaultCommand(files, options, io) {
922
+ async function runLintCommand(files, options, io, interceptConsoleOutput) {
996
923
  if (options.config) await assertConfigExists(io.cwd, options.config);
997
924
  const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
998
925
  const lintFiles = await resolveLintFiles(files, config, io.cwd);
999
- const runner = resolveRunnerConfig(setupConfig, config, options);
926
+ const runner = resolveRunnerConfig(setupConfig, { runner: resolveConfigRunner(config) }, options);
1000
927
  const progress = shouldEnableProgress(options, io) ? createCliProgressReporter({
1001
928
  color: io.stderr.isTTY === true,
1002
929
  columns: io.stderr.columns ?? 80,
@@ -1030,70 +957,395 @@ async function runDefaultCommand(files, options, io) {
1030
957
  io.stdout.write(formatDiagnostics(options.format, result, { color: io.stdout.isTTY === true }));
1031
958
  return result.diagnostics.length > 0 ? 1 : 0;
1032
959
  }
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
- }
960
+ function shouldEnableProgress(options, io) {
961
+ if (options.progress !== void 0) return options.progress;
962
+ return options.format === "stylish" && io.stderr.isTTY === true;
1050
963
  }
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;
964
+ //#endregion
965
+ //#region src/cli/commands/setup/interactive.ts
966
+ const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
967
+ const backValue = "__alint_back__";
968
+ function formatProbeModelsFailure(endpoint, error) {
969
+ const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
970
+ return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
1059
971
  }
1060
- async function runProvidersListCommand(io) {
1061
- io.stdout.write(formatProviderList(await loadMergedSetupConfig(io)));
1062
- return 0;
972
+ function isBackInput(value) {
973
+ return value.trim() === "..";
1063
974
  }
1064
- async function runProvidersProbeCommand(options, io) {
1065
- if (!options.endpoint) {
1066
- io.stderr.write("config providers probe requires --endpoint.\n");
975
+ async function runInteractiveSetup(io) {
976
+ if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
977
+ io.stderr.write(nonTtyMessage);
1067
978
  return 2;
1068
979
  }
1069
- try {
1070
- const models = await probeModels(options.endpoint, parseHeaderList(toArray(options.providerHeader)) ?? {});
1071
- io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
980
+ const prompts = await import("@clack/prompts");
981
+ const cancelPrompt = () => {
982
+ prompts.cancel("Setup cancelled.");
983
+ return 1;
984
+ };
985
+ prompts.intro("alint setup");
986
+ const draft = {};
987
+ let step = "scope";
988
+ while (true) {
989
+ if (step === "scope") {
990
+ const scope = await prompts.select({
991
+ message: "Where should alint write setup config?",
992
+ options: [{
993
+ label: "Global",
994
+ value: "global"
995
+ }, {
996
+ label: "Local project",
997
+ value: "local"
998
+ }]
999
+ });
1000
+ if (prompts.isCancel(scope)) return cancelPrompt();
1001
+ draft.scope = scope;
1002
+ step = "source";
1003
+ continue;
1004
+ }
1005
+ if (step === "source") {
1006
+ const source = await prompts.select({
1007
+ message: "Choose provider setup mode.",
1008
+ options: withBackOption([
1009
+ {
1010
+ label: "Custom OpenAI-compatible provider",
1011
+ value: "custom"
1012
+ },
1013
+ {
1014
+ label: "Ollama",
1015
+ value: "ollama"
1016
+ },
1017
+ {
1018
+ label: "Manual model entry",
1019
+ value: "manual"
1020
+ }
1021
+ ])
1022
+ });
1023
+ if (prompts.isCancel(source)) return cancelPrompt();
1024
+ if (source === backValue) {
1025
+ step = "scope";
1026
+ continue;
1027
+ }
1028
+ draft.source = source;
1029
+ step = "endpoint";
1030
+ continue;
1031
+ }
1032
+ if (step === "endpoint") {
1033
+ const endpoint = await promptEndpoint(prompts, draft.source ?? "custom");
1034
+ if (prompts.isCancel(endpoint)) return cancelPrompt();
1035
+ if (typeof endpoint !== "string") return cancelPrompt();
1036
+ if (isBackInput(endpoint)) {
1037
+ step = "source";
1038
+ continue;
1039
+ }
1040
+ draft.endpoint = endpoint;
1041
+ step = "providerId";
1042
+ continue;
1043
+ }
1044
+ if (step === "providerId") {
1045
+ const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
1046
+ const providerId = await prompts.text({
1047
+ defaultValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
1048
+ message: "Provider id",
1049
+ placeholder: "Type .. to go back",
1050
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
1051
+ });
1052
+ if (prompts.isCancel(providerId)) return cancelPrompt();
1053
+ if (typeof providerId !== "string") return cancelPrompt();
1054
+ if (isBackInput(providerId)) {
1055
+ step = "endpoint";
1056
+ continue;
1057
+ }
1058
+ draft.providerId = providerId;
1059
+ step = "headers";
1060
+ continue;
1061
+ }
1062
+ if (step === "headers") {
1063
+ const headerInput = await prompts.text({
1064
+ defaultValue: draft.headerInput ?? "",
1065
+ message: "Headers",
1066
+ placeholder: "Authorization=Bearer token, X-Test=true; type .. to go back",
1067
+ validate: (value) => {
1068
+ if (isBackInput(value ?? "")) return;
1069
+ try {
1070
+ parseHeaderList(splitHeaderInput(value ?? ""));
1071
+ return;
1072
+ } catch {
1073
+ return "Headers must be comma-separated Key=Value entries.";
1074
+ }
1075
+ }
1076
+ });
1077
+ if (prompts.isCancel(headerInput)) return cancelPrompt();
1078
+ if (typeof headerInput !== "string") return cancelPrompt();
1079
+ if (isBackInput(headerInput)) {
1080
+ step = "providerId";
1081
+ continue;
1082
+ }
1083
+ draft.headerInput = headerInput;
1084
+ draft.headers = parseHeaderList(splitHeaderInput(headerInput));
1085
+ draft.discoveredModels = draft.source === "manual" ? [] : await probeModelsWithSpinner(prompts, draft.endpoint ?? "", draft.headers);
1086
+ step = "models";
1087
+ continue;
1088
+ }
1089
+ if (step === "models") {
1090
+ const selectedModels = await promptModels(prompts, draft.discoveredModels ?? []);
1091
+ if (prompts.isCancel(selectedModels)) return cancelPrompt();
1092
+ if (selectedModels === backValue) {
1093
+ step = "headers";
1094
+ continue;
1095
+ }
1096
+ if (!Array.isArray(selectedModels)) return cancelPrompt();
1097
+ draft.selectedModels = selectedModels;
1098
+ step = "defaultAlias";
1099
+ continue;
1100
+ }
1101
+ if (step === "defaultAlias") {
1102
+ const addDefaultAlias = await prompts.select({
1103
+ message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
1104
+ options: withBackOption([{
1105
+ label: "Yes",
1106
+ value: "yes"
1107
+ }, {
1108
+ label: "No",
1109
+ value: "no"
1110
+ }])
1111
+ });
1112
+ if (prompts.isCancel(addDefaultAlias)) return cancelPrompt();
1113
+ if (addDefaultAlias === backValue) {
1114
+ step = "models";
1115
+ continue;
1116
+ }
1117
+ draft.addDefaultAlias = addDefaultAlias === "yes";
1118
+ step = "confirm";
1119
+ continue;
1120
+ }
1121
+ const nextProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? [], draft.addDefaultAlias ?? true);
1122
+ const confirmed = await prompts.select({
1123
+ message: [
1124
+ `Write ${draft.scope} setup config?`,
1125
+ `Provider: ${nextProvider.id}`,
1126
+ `Endpoint: ${nextProvider.endpoint}`,
1127
+ `Models: ${(draft.selectedModels ?? []).join(", ")}`
1128
+ ].join("\n"),
1129
+ options: withBackOption([{
1130
+ label: "Yes",
1131
+ value: "yes"
1132
+ }, {
1133
+ label: "No",
1134
+ value: "no"
1135
+ }])
1136
+ });
1137
+ if (prompts.isCancel(confirmed)) return cancelPrompt();
1138
+ if (confirmed === backValue) {
1139
+ step = "defaultAlias";
1140
+ continue;
1141
+ }
1142
+ if (confirmed === "no") return cancelPrompt();
1143
+ const configPath = getConfigPath(io, draft.scope ?? "global");
1144
+ await writeSetupConfig(configPath, mergeSetupConfigs(await loadSetupConfig(configPath), {
1145
+ providers: [nextProvider],
1146
+ version: 1
1147
+ }));
1148
+ prompts.outro(`Wrote ${configPath}`);
1072
1149
  return 0;
1150
+ }
1151
+ }
1152
+ function withBackOption(options) {
1153
+ return [...options, {
1154
+ label: "Back",
1155
+ value: backValue
1156
+ }];
1157
+ }
1158
+ function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
1159
+ return {
1160
+ endpoint,
1161
+ headers,
1162
+ id: providerId,
1163
+ models: modelIds.map((modelId, index) => ({
1164
+ aliases: index === 0 && addDefaultAlias ? ["default"] : void 0,
1165
+ id: modelId,
1166
+ name: modelId
1167
+ })),
1168
+ type: "openai-compatible"
1169
+ };
1170
+ }
1171
+ function getConfigPath(io, scope) {
1172
+ return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
1173
+ }
1174
+ async function probeModelsWithSpinner(prompts, endpoint, headers) {
1175
+ const spinner = prompts.spinner();
1176
+ spinner.start("Probing models");
1177
+ try {
1178
+ const models = await probeModels(endpoint, headers ?? {});
1179
+ spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
1180
+ return models;
1073
1181
  } catch (error) {
1074
- io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
1075
- return 2;
1182
+ spinner.stop(formatProbeModelsFailure(endpoint, error));
1183
+ return [];
1076
1184
  }
1077
1185
  }
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;
1186
+ async function promptEndpoint(prompts, source) {
1187
+ return prompts.text({
1188
+ defaultValue: source === "ollama" ? "http://localhost:11434/v1" : void 0,
1189
+ message: "Provider endpoint",
1190
+ placeholder: source === "ollama" ? "http://localhost:11434/v1; type .. to go back" : "https://example.test/v1; type .. to go back",
1191
+ validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
1192
+ });
1193
+ }
1194
+ async function promptModels(prompts, discoveredModels) {
1195
+ if (discoveredModels.length > 0) {
1196
+ const selectedModels = await prompts.multiselect({
1197
+ message: "Select models",
1198
+ options: withBackOption(discoveredModels.map((model) => ({
1199
+ label: model,
1200
+ value: model
1201
+ }))),
1202
+ required: true
1203
+ });
1204
+ return Array.isArray(selectedModels) && selectedModels.includes(backValue) ? backValue : selectedModels;
1083
1205
  }
1084
- io.stdout.write(formatProviderShow(provider));
1085
- return 0;
1206
+ const modelInput = await prompts.text({
1207
+ message: "Models",
1208
+ placeholder: "qwen:8b, qwen:32b; type .. to go back",
1209
+ validate: (value) => isBackInput(value ?? "") || splitModelInput(value ?? "").length > 0 ? void 0 : "At least one model is required."
1210
+ });
1211
+ if (prompts.isCancel(modelInput)) return modelInput;
1212
+ return isBackInput(modelInput) ? backValue : splitModelInput(modelInput);
1086
1213
  }
1087
- function shouldCaptureHelp(argv) {
1088
- return argv.includes("--help") || argv.includes("-h");
1214
+ function splitHeaderInput(value) {
1215
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1089
1216
  }
1090
- function shouldEnableProgress(options, io) {
1091
- if (options.progress !== void 0) return options.progress;
1092
- return options.format === "stylish" && io.stderr.isTTY === true;
1217
+ function splitModelInput(value) {
1218
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1219
+ }
1220
+ //#endregion
1221
+ //#region src/cli/commands/setup/index.ts
1222
+ const setup = defineCommand({
1223
+ action: (context, options) => runSetupCommand({
1224
+ ...options,
1225
+ noInteractive: context.setupNoInteractive
1226
+ }, context.io),
1227
+ description: "Write alint provider configuration",
1228
+ name: "setup",
1229
+ options: [
1230
+ {
1231
+ description: "Write project-local config",
1232
+ flags: "--local"
1233
+ },
1234
+ {
1235
+ description: "Disable interactive setup",
1236
+ flags: "-N, --no-interactive"
1237
+ },
1238
+ {
1239
+ description: "Provider endpoint",
1240
+ flags: "--provider-endpoint <endpoint>"
1241
+ },
1242
+ {
1243
+ description: "Provider id",
1244
+ flags: "--provider-id <id>"
1245
+ },
1246
+ {
1247
+ description: "Provider model",
1248
+ flags: "--provider-model <model>"
1249
+ },
1250
+ {
1251
+ description: "Provider header",
1252
+ flags: "--provider-header <Key=Value>"
1253
+ }
1254
+ ]
1255
+ });
1256
+ function createSetupConfig(providerId, providerEndpoint, options) {
1257
+ const models = toArray(options.providerModel).map((model) => ({
1258
+ id: model,
1259
+ name: model
1260
+ }));
1261
+ return {
1262
+ providers: [{
1263
+ endpoint: providerEndpoint,
1264
+ headers: parseHeaderList(toArray(options.providerHeader)),
1265
+ id: providerId,
1266
+ models,
1267
+ type: "openai-compatible"
1268
+ }],
1269
+ version: 1
1270
+ };
1271
+ }
1272
+ async function runSetupCommand(options, io) {
1273
+ if (!options.providerEndpoint) {
1274
+ if (options.noInteractive !== true) return runInteractiveSetup({
1275
+ ...io,
1276
+ stdin: io.stdin ?? process.stdin
1277
+ });
1278
+ io.stderr.write("setup requires --provider-endpoint in --no-interactive mode.\n");
1279
+ return 2;
1280
+ }
1281
+ if (!options.providerId) {
1282
+ io.stderr.write("setup requires --provider-id in --no-interactive mode.\n");
1283
+ return 2;
1284
+ }
1285
+ const setupConfigPath = options.local ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
1286
+ await writeSetupConfig(setupConfigPath, mergeSetupConfigs(await loadSetupConfig(setupConfigPath), createSetupConfig(options.providerId, options.providerEndpoint, options)));
1287
+ return 0;
1093
1288
  }
1094
1289
  function toArray(value) {
1095
1290
  if (value === void 0) return [];
1096
1291
  return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
1097
1292
  }
1098
1293
  //#endregion
1294
+ //#region src/cli/commands/index.ts
1295
+ const commandTree = [
1296
+ setup,
1297
+ config,
1298
+ lint
1299
+ ];
1300
+ //#endregion
1301
+ //#region src/cli/cli.ts
1302
+ async function executeCli(argv, io) {
1303
+ const cli = cac("alint");
1304
+ const setupNoInteractive = argv.includes("-N") || argv.includes("--no-interactive");
1305
+ let pendingResult;
1306
+ const setPendingResult = (result) => {
1307
+ pendingResult = result;
1308
+ return result;
1309
+ };
1310
+ 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();
1311
+ registerCommandTree(cli, commandTree, {
1312
+ interceptConsoleOutput,
1313
+ io,
1314
+ setupNoInteractive
1315
+ }, setPendingResult);
1316
+ const restoreConsole = interceptConsoleOutput(shouldCaptureHelp(argv) ? io.stdout : io.stderr);
1317
+ try {
1318
+ cli.parse(argv);
1319
+ return await (pendingResult ?? Promise.resolve(0));
1320
+ } finally {
1321
+ restoreConsole();
1322
+ }
1323
+ }
1324
+ function interceptConsoleOutput(stdout) {
1325
+ const cliConsole = globalThis.console;
1326
+ const originalConsoleDebug = cliConsole.debug;
1327
+ const originalConsoleDir = cliConsole.dir;
1328
+ const originalConsoleInfo = console.info;
1329
+ const originalConsoleLog = cliConsole.log;
1330
+ const writeConsoleLine = (...args) => {
1331
+ stdout.write(`${args.map(String).join(" ")}\n`);
1332
+ };
1333
+ const writeConsoleDir = (item, options) => {
1334
+ stdout.write(`${inspect(item, options)}\n`);
1335
+ };
1336
+ cliConsole.debug = writeConsoleLine;
1337
+ cliConsole.dir = writeConsoleDir;
1338
+ console.info = writeConsoleLine;
1339
+ cliConsole.log = writeConsoleLine;
1340
+ return () => {
1341
+ cliConsole.debug = originalConsoleDebug;
1342
+ cliConsole.dir = originalConsoleDir;
1343
+ console.info = originalConsoleInfo;
1344
+ cliConsole.log = originalConsoleLog;
1345
+ };
1346
+ }
1347
+ function shouldCaptureHelp(argv) {
1348
+ return argv.includes("--help") || argv.includes("-h");
1349
+ }
1350
+ //#endregion
1099
1351
  export { formatJson as i, formatDiagnostics as n, formatStylish as r, executeCli as t };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Diagnostic, RunResult } from "@alint-js/core";
2
2
 
3
- //#region src/cli/cli.d.ts
3
+ //#region src/cli/types.d.ts
4
4
  interface CliIo {
5
5
  cwd: string;
6
6
  env?: NodeJS.ProcessEnv;
@@ -15,6 +15,8 @@ interface CliWritable {
15
15
  isTTY?: boolean;
16
16
  write: (chunk: string) => unknown;
17
17
  }
18
+ //#endregion
19
+ //#region src/cli/cli.d.ts
18
20
  declare function executeCli(argv: string[], io: CliIo): Promise<number>;
19
21
  //#endregion
20
22
  //#region src/cli/reporters/index.d.ts
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-CoS-NVz_.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-C4PnKXoK.mjs";
2
2
  export { executeCli, formatDiagnostics, formatJson, formatStylish };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.0.5",
4
+ "version": "0.0.6",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -21,11 +21,12 @@
21
21
  "cac": "^7.0.0",
22
22
  "cli-spinners": "^3.4.0",
23
23
  "gitignore-fs": "^2.2.3",
24
+ "minimatch": "^10.2.5",
24
25
  "pathe": "^2.0.3",
25
26
  "table": "^6.9.0",
26
27
  "tinyrainbow": "^3.1.0",
27
- "@alint-js/config": "0.0.5",
28
- "@alint-js/core": "0.0.5"
28
+ "@alint-js/config": "0.0.6",
29
+ "@alint-js/core": "0.0.6"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/node": "^26.0.1",