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