@alint-js/cli 0.0.38 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/bin/index.mjs +2 -1
- package/dist/{cli-BQFW2w-L.mjs → cli-B_wKMxdx.mjs} +1432 -461
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { inspect } from "node:util";
|
|
3
3
|
import { cac } from "cac";
|
|
4
4
|
import { appendFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
5
|
-
import { getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
|
|
5
|
+
import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
|
|
6
6
|
import { AlintRunCancelledError, AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
|
|
7
7
|
import { isAbsolute, join, relative, resolve } from "pathe";
|
|
8
8
|
import { getBorderCharacters, table } from "table";
|
|
@@ -16,7 +16,12 @@ import Gitignore from "gitignore-fs";
|
|
|
16
16
|
import { Minimatch, minimatch } from "minimatch";
|
|
17
17
|
import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
|
|
18
18
|
//#region package.json
|
|
19
|
-
var version = "0.
|
|
19
|
+
var version = "0.1.1";
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/cli/output.ts
|
|
22
|
+
function escapeLineValue(value) {
|
|
23
|
+
return JSON.stringify(value).slice(1, -1).replace(/[\u0080-\u009F\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
24
|
+
}
|
|
20
25
|
//#endregion
|
|
21
26
|
//#region src/cli/commands/command.ts
|
|
22
27
|
function defineCommand(node) {
|
|
@@ -41,6 +46,13 @@ function dispatchCommand(context, node, args, options, path) {
|
|
|
41
46
|
const child = node.children?.find((item) => item.name === subcommand || item.alias?.includes(subcommand ?? ""));
|
|
42
47
|
if (child) return dispatchCommand(context, child, restArgs, options, [...path, child.name]);
|
|
43
48
|
if (!node.action) return Promise.resolve(reportUnknownCommand(context, path, args));
|
|
49
|
+
if (node.exactArguments === true) {
|
|
50
|
+
const unexpectedArgument = args[node.arguments?.split(/\s+/u).filter(Boolean).length ?? 0];
|
|
51
|
+
if (unexpectedArgument !== void 0) {
|
|
52
|
+
context.io.stderr.write(`unexpected argument "${escapeLineValue(unexpectedArgument)}". usage: alint ${formatUsagePattern(path, node)}.\n`);
|
|
53
|
+
return Promise.resolve(2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
44
56
|
return Promise.resolve(node.action(context, ...parseCommandArguments(node, args), options));
|
|
45
57
|
}
|
|
46
58
|
function formatChildCommandHelp(parentPath, child) {
|
|
@@ -197,6 +209,7 @@ function shouldSkipOptionValue(arg) {
|
|
|
197
209
|
"--interval",
|
|
198
210
|
"--metric",
|
|
199
211
|
"--model",
|
|
212
|
+
"--provider",
|
|
200
213
|
"--provider-endpoint",
|
|
201
214
|
"--provider-header",
|
|
202
215
|
"--provider-id",
|
|
@@ -234,6 +247,13 @@ async function runConfigInspectCommand(path, configPath, io) {
|
|
|
234
247
|
}
|
|
235
248
|
//#endregion
|
|
236
249
|
//#region src/cli/provider-registry.ts
|
|
250
|
+
function formatDuplicateModelIdentity(identity) {
|
|
251
|
+
return [
|
|
252
|
+
`model "${escapeLineValue(identity)}" is configured more than once.`,
|
|
253
|
+
"remove duplicate provider/model definitions from the setup configuration.",
|
|
254
|
+
""
|
|
255
|
+
].join("\n");
|
|
256
|
+
}
|
|
237
257
|
const providerSetupSources = [
|
|
238
258
|
{
|
|
239
259
|
defaultEndpoint: "http://127.0.0.1:8317/v1",
|
|
@@ -247,6 +267,20 @@ const providerSetupSources = [
|
|
|
247
267
|
probeModels: true,
|
|
248
268
|
value: "custom"
|
|
249
269
|
},
|
|
270
|
+
{
|
|
271
|
+
defaultEndpoint: "https://api.cerebras.ai/v1",
|
|
272
|
+
defaultProviderId: "cerebras",
|
|
273
|
+
label: "Cerebras",
|
|
274
|
+
probeModels: true,
|
|
275
|
+
value: "cerebras"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
defaultEndpoint: "https://api.groq.com/openai/v1",
|
|
279
|
+
defaultProviderId: "groq",
|
|
280
|
+
label: "Groq",
|
|
281
|
+
probeModels: true,
|
|
282
|
+
value: "groq"
|
|
283
|
+
},
|
|
250
284
|
{
|
|
251
285
|
defaultEndpoint: "http://localhost:11434/v1",
|
|
252
286
|
label: "Ollama",
|
|
@@ -277,8 +311,17 @@ function createProviderId(endpoint, existingIds) {
|
|
|
277
311
|
if (!existingIds.has(candidate)) return candidate;
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
314
|
+
function findModels(config, request) {
|
|
315
|
+
const candidates = flattenModels(config);
|
|
316
|
+
const canonicalCandidates = candidates.filter((candidate) => canonicalIdentity(candidate) === request);
|
|
317
|
+
if (canonicalCandidates.length > 0) return canonicalCandidates;
|
|
318
|
+
return candidates.filter((candidate) => {
|
|
319
|
+
return [
|
|
320
|
+
candidate.model.id,
|
|
321
|
+
candidate.model.name,
|
|
322
|
+
...candidate.model.aliases ?? []
|
|
323
|
+
].filter((name) => name !== void 0).some((name) => name === request || `${candidate.provider.id}/${name}` === request);
|
|
324
|
+
});
|
|
282
325
|
}
|
|
283
326
|
function findProviderSetupSource(value) {
|
|
284
327
|
return providerSetupSources.find((source) => source.value === value);
|
|
@@ -293,6 +336,23 @@ function flattenModels(config) {
|
|
|
293
336
|
provider
|
|
294
337
|
})));
|
|
295
338
|
}
|
|
339
|
+
function formatAmbiguousModels(request, candidates) {
|
|
340
|
+
const candidatesByIdentity = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const candidate of candidates) {
|
|
342
|
+
const identity = canonicalIdentity(candidate);
|
|
343
|
+
const matchingCandidates = candidatesByIdentity.get(identity) ?? [];
|
|
344
|
+
matchingCandidates.push(candidate);
|
|
345
|
+
candidatesByIdentity.set(identity, matchingCandidates);
|
|
346
|
+
}
|
|
347
|
+
const duplicateIdentity = [...candidatesByIdentity].find(([, matchingCandidates]) => matchingCandidates.length > 1)?.[0];
|
|
348
|
+
if (duplicateIdentity !== void 0) return formatDuplicateModelIdentity(duplicateIdentity);
|
|
349
|
+
const choices = [...candidatesByIdentity.keys()];
|
|
350
|
+
return `${[
|
|
351
|
+
`ambiguous model "${escapeLineValue(request)}".`,
|
|
352
|
+
"specify a provider-qualified model:",
|
|
353
|
+
...choices.map((choice) => ` ${escapeLineValue(choice)}`)
|
|
354
|
+
].join("\n")}\n`;
|
|
355
|
+
}
|
|
296
356
|
function formatModelList(config) {
|
|
297
357
|
return formatTable([[
|
|
298
358
|
"id",
|
|
@@ -343,13 +403,19 @@ function formatProviderShow(provider) {
|
|
|
343
403
|
if (headerKeys.length > 0) lines.push(`headers: ${headerKeys.join(", ")}`);
|
|
344
404
|
return `${lines.join("\n")}\n`;
|
|
345
405
|
}
|
|
406
|
+
const httpTokenPattern = /^[\w^`\-!#$%&'*+.|~]+$/u;
|
|
407
|
+
function isValidProviderHeaderName(name) {
|
|
408
|
+
return httpTokenPattern.test(name);
|
|
409
|
+
}
|
|
346
410
|
function parseHeaderList(headers) {
|
|
347
411
|
if (headers.length === 0) return;
|
|
348
412
|
const parsedHeaders = {};
|
|
349
413
|
for (const header of headers) {
|
|
350
414
|
const separatorIndex = header.indexOf("=");
|
|
351
|
-
if (separatorIndex <= 0) throw new Error(
|
|
352
|
-
|
|
415
|
+
if (separatorIndex <= 0) throw new Error("Invalid provider header. Expected Key=Value.");
|
|
416
|
+
const name = header.slice(0, separatorIndex);
|
|
417
|
+
if (!isValidProviderHeaderName(name)) throw new Error("Invalid provider header name. Expected an HTTP field-name token.");
|
|
418
|
+
parsedHeaders[name] = header.slice(separatorIndex + 1);
|
|
353
419
|
}
|
|
354
420
|
return parsedHeaders;
|
|
355
421
|
}
|
|
@@ -357,8 +423,11 @@ async function probeModels(endpoint, headers = {}) {
|
|
|
357
423
|
const response = await fetch(buildModelsUrl(endpoint), { headers });
|
|
358
424
|
if (!response.ok) throw new Error(`GET ${buildModelsUrl(endpoint)} returned ${response.status}.`);
|
|
359
425
|
const body = await response.json();
|
|
360
|
-
if (!Array.isArray(body.data)) throw new TypeError("Expected OpenAI-compatible models response with data array.");
|
|
361
|
-
return body.data.map((model) => model.id)
|
|
426
|
+
if (!isRecord$1(body) || !Array.isArray(body.data) || !body.data.every(isModelRecord)) throw new TypeError("Expected OpenAI-compatible models response with data array.");
|
|
427
|
+
return body.data.map((model) => model.id);
|
|
428
|
+
}
|
|
429
|
+
function canonicalIdentity(candidate) {
|
|
430
|
+
return `${candidate.provider.id}/${candidate.model.id}`;
|
|
362
431
|
}
|
|
363
432
|
function formatTable(rows) {
|
|
364
433
|
if (rows.length <= 1) return "";
|
|
@@ -371,17 +440,36 @@ function formatTable(rows) {
|
|
|
371
440
|
drawHorizontalLine: () => false
|
|
372
441
|
});
|
|
373
442
|
}
|
|
443
|
+
function isModelRecord(value) {
|
|
444
|
+
return isRecord$1(value) && typeof value.id === "string" && value.id.length > 0;
|
|
445
|
+
}
|
|
446
|
+
function isRecord$1(value) {
|
|
447
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
448
|
+
}
|
|
374
449
|
function normalizeProviderIdBase(value) {
|
|
375
450
|
return value.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider";
|
|
376
451
|
}
|
|
377
452
|
//#endregion
|
|
378
453
|
//#region src/cli/commands/config/setup-config.ts
|
|
454
|
+
function formatUnknownProvider(providerId, scope) {
|
|
455
|
+
const hint = scope === "local" ? " Remove --local to inspect global configuration." : " Add --local to inspect project-local configuration.";
|
|
456
|
+
return `unknown provider "${escapeLineValue(providerId)}" in ${scope} setup config.${hint}\n`;
|
|
457
|
+
}
|
|
379
458
|
async function loadMergedSetupConfig(io) {
|
|
380
459
|
const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
|
|
381
460
|
const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
|
|
382
461
|
const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
|
|
383
462
|
return mergeSetupConfigs(globalSetupConfig, projectSetupConfig);
|
|
384
463
|
}
|
|
464
|
+
async function loadScopedSetupConfig(io, local) {
|
|
465
|
+
const scope = local === true ? "local" : "global";
|
|
466
|
+
const path = scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
|
|
467
|
+
return {
|
|
468
|
+
config: await loadSetupConfig(path),
|
|
469
|
+
path,
|
|
470
|
+
scope
|
|
471
|
+
};
|
|
472
|
+
}
|
|
385
473
|
//#endregion
|
|
386
474
|
//#region src/cli/commands/config/models/ls.ts
|
|
387
475
|
const ls$1 = defineCommand({
|
|
@@ -396,61 +484,338 @@ const ls$1 = defineCommand({
|
|
|
396
484
|
//#endregion
|
|
397
485
|
//#region src/cli/commands/config/probe.ts
|
|
398
486
|
function providerHeadersFromOptions(options) {
|
|
399
|
-
return parseHeaderList(toArray$
|
|
487
|
+
return parseHeaderList(toArray$2(options.providerHeader)) ?? {};
|
|
400
488
|
}
|
|
401
|
-
function toArray$
|
|
489
|
+
function toArray$2(value) {
|
|
402
490
|
if (value === void 0) return [];
|
|
403
491
|
return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
|
|
404
492
|
}
|
|
405
493
|
//#endregion
|
|
494
|
+
//#region src/cli/commands/config/models/probe.ts
|
|
495
|
+
const probe$1 = defineCommand({
|
|
496
|
+
async action(context, options) {
|
|
497
|
+
if (!options.endpoint) {
|
|
498
|
+
context.io.stderr.write("config models probe requires --endpoint.\n");
|
|
499
|
+
return 2;
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
|
|
503
|
+
context.io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
|
|
504
|
+
return 0;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
context.io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
507
|
+
return 2;
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
description: "Probe OpenAI-compatible models",
|
|
511
|
+
examples: [["# Probe a provider endpoint and print available model ids", "alint config models probe --endpoint https://openrouter.ai/api/v1"].join("\n"), ["# Probe an endpoint that requires an authorization header", "alint config models probe --endpoint https://api.example.com/v1 --provider-header \"Authorization=Bearer $TOKEN\""].join("\n")],
|
|
512
|
+
help: ["Probe an OpenAI-compatible models endpoint before saving it in setup config.", "The command calls the provider models endpoint using the supplied endpoint and optional headers, then prints the model ids returned by that provider."].join("\n\n"),
|
|
513
|
+
name: "probe",
|
|
514
|
+
options: [{
|
|
515
|
+
description: "Provider endpoint",
|
|
516
|
+
flags: "--endpoint <url>"
|
|
517
|
+
}, {
|
|
518
|
+
description: "Provider header",
|
|
519
|
+
flags: "--provider-header <Key=Value>"
|
|
520
|
+
}]
|
|
521
|
+
});
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/cli/commands/config/models/prune.ts
|
|
524
|
+
const prune$1 = defineCommand({
|
|
525
|
+
action: runPruneModelsCommand,
|
|
526
|
+
description: "Remove models unavailable from providers",
|
|
527
|
+
name: "prune",
|
|
528
|
+
options: [
|
|
529
|
+
{
|
|
530
|
+
description: "Provider id",
|
|
531
|
+
flags: "--provider <id>"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
description: "Read and write project-local config",
|
|
535
|
+
flags: "--local"
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
description: "Disable interactive confirmation",
|
|
539
|
+
flags: "-N, --no-interactive"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
description: "Confirm model deletion",
|
|
543
|
+
flags: "--yes"
|
|
544
|
+
}
|
|
545
|
+
]
|
|
546
|
+
});
|
|
547
|
+
/**
|
|
548
|
+
* Removes configured models absent from their providers' model registries.
|
|
549
|
+
*
|
|
550
|
+
* Triggering workflow:
|
|
551
|
+
*
|
|
552
|
+
* {@link registerCommandTree}
|
|
553
|
+
* -> `config models prune`
|
|
554
|
+
* -> {@link runPruneModelsCommand}
|
|
555
|
+
*
|
|
556
|
+
* Upstream:
|
|
557
|
+
* - {@link dispatchCommand}
|
|
558
|
+
*
|
|
559
|
+
* Downstream:
|
|
560
|
+
* - {@link confirmPruneModels}
|
|
561
|
+
* - {@link probeModels}
|
|
562
|
+
* - {@link pruneProviderModels}
|
|
563
|
+
* - {@link writeSetupConfig}
|
|
564
|
+
*/
|
|
565
|
+
async function runPruneModelsCommand(context, options, confirm = confirmPruneModels) {
|
|
566
|
+
if (Array.isArray(options.provider)) {
|
|
567
|
+
context.io.stderr.write("config models prune accepts --provider only once.\n");
|
|
568
|
+
return 2;
|
|
569
|
+
}
|
|
570
|
+
const { config, path, scope } = await loadScopedSetupConfig(context.io, options.local);
|
|
571
|
+
const providers = options.provider === void 0 ? config.providers : config.providers.filter((provider) => provider.id === options.provider);
|
|
572
|
+
if (options.provider !== void 0 && providers.length === 0) {
|
|
573
|
+
context.io.stderr.write(formatUnknownProvider(options.provider, scope));
|
|
574
|
+
return 2;
|
|
575
|
+
}
|
|
576
|
+
const duplicateProviderId = providers.find((provider, providerIndex) => providers.findIndex((candidate) => candidate.id === provider.id) !== providerIndex)?.id;
|
|
577
|
+
if (duplicateProviderId !== void 0) {
|
|
578
|
+
context.io.stderr.write([
|
|
579
|
+
`provider "${escapeLineValue(duplicateProviderId)}" is configured more than once.`,
|
|
580
|
+
"remove duplicate provider definitions from the setup configuration.",
|
|
581
|
+
""
|
|
582
|
+
].join("\n"));
|
|
583
|
+
return 2;
|
|
584
|
+
}
|
|
585
|
+
const plans = [];
|
|
586
|
+
let hadFailure = false;
|
|
587
|
+
for (const provider of providers) {
|
|
588
|
+
let remoteModelIds;
|
|
589
|
+
try {
|
|
590
|
+
remoteModelIds = new Set(await probeModels(provider.endpoint, provider.headers));
|
|
591
|
+
} catch (error) {
|
|
592
|
+
hadFailure = true;
|
|
593
|
+
context.io.stderr.write(`failed to probe provider "${escapeLineValue(provider.id)}" at "${escapeLineValue(provider.endpoint)}": ${classifyProbeFailure(error)}.\n`);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const removedModels = provider.models.filter((model) => !remoteModelIds.has(model.id));
|
|
597
|
+
const defaultModel = removedModels.find((model) => (model.aliases ?? []).includes("default"));
|
|
598
|
+
if (defaultModel !== void 0) {
|
|
599
|
+
hadFailure = true;
|
|
600
|
+
context.io.stderr.write(`cannot prune default model "${escapeLineValue(provider.id)}/${escapeLineValue(defaultModel.id)}". select another default first.\n`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (removedModels.length > 0) plans.push({
|
|
604
|
+
provider,
|
|
605
|
+
remoteModelIds,
|
|
606
|
+
removedModels
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (plans.length === 0) {
|
|
610
|
+
context.io.stdout.write("no models to prune.\n");
|
|
611
|
+
return hadFailure ? 2 : 0;
|
|
612
|
+
}
|
|
613
|
+
context.io.stdout.write([
|
|
614
|
+
"Models to prune:",
|
|
615
|
+
...plans.flatMap((plan) => plan.removedModels.map((model) => ` ${escapeLineValue(plan.provider.id)}/${escapeLineValue(model.id)}`)),
|
|
616
|
+
""
|
|
617
|
+
].join("\n"));
|
|
618
|
+
if (context.setupNoInteractive && options.yes !== true) {
|
|
619
|
+
context.io.stderr.write("config models prune requires --yes in --no-interactive mode.\n");
|
|
620
|
+
return 2;
|
|
621
|
+
}
|
|
622
|
+
if (!context.setupNoInteractive && options.yes !== true) {
|
|
623
|
+
if (context.io.stdin?.isTTY !== true || context.io.stdout.isTTY !== true) {
|
|
624
|
+
context.io.stderr.write("config models prune requires a TTY or -N --yes.\n");
|
|
625
|
+
return 2;
|
|
626
|
+
}
|
|
627
|
+
if (await confirm() !== "confirmed") return 1;
|
|
628
|
+
}
|
|
629
|
+
await writeSetupConfig(path, applyPlans(config, plans));
|
|
630
|
+
return hadFailure ? 2 : 0;
|
|
631
|
+
}
|
|
632
|
+
function applyPlans(config, plans) {
|
|
633
|
+
return plans.reduce((nextConfig, plan) => pruneProviderModels(nextConfig, plan.provider.id, plan.remoteModelIds), config);
|
|
634
|
+
}
|
|
635
|
+
function classifyProbeFailure(error) {
|
|
636
|
+
const message = errorMessageFrom(error);
|
|
637
|
+
const httpStatus = message?.match(/^GET .+ returned ([1-5]\d\d)\.$/u)?.[1];
|
|
638
|
+
if (httpStatus !== void 0) return `provider returned HTTP ${httpStatus}`;
|
|
639
|
+
if (error instanceof TypeError && message === "Expected OpenAI-compatible models response with data array.") return "provider returned an invalid models response";
|
|
640
|
+
if (error instanceof SyntaxError) return "provider returned invalid JSON";
|
|
641
|
+
return "model request failed";
|
|
642
|
+
}
|
|
643
|
+
async function confirmPruneModels() {
|
|
644
|
+
const prompts = await import("@clack/prompts");
|
|
645
|
+
const confirmed = await prompts.confirm({ message: "Remove these configured models?" });
|
|
646
|
+
if (prompts.isCancel(confirmed)) return "cancelled";
|
|
647
|
+
return confirmed ? "confirmed" : "declined";
|
|
648
|
+
}
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/cli/commands/config/models/target.ts
|
|
651
|
+
function resolveExactModelTarget(config, request, requestedProviderId) {
|
|
652
|
+
const separatorIndex = request.indexOf("/");
|
|
653
|
+
const qualifier = separatorIndex === -1 ? void 0 : request.slice(0, separatorIndex);
|
|
654
|
+
const qualifiedProviders = qualifier === void 0 ? [] : config.providers.filter((provider) => provider.id === qualifier);
|
|
655
|
+
const qualifiedProviderId = qualifiedProviders[0]?.id;
|
|
656
|
+
const modelId = qualifiedProviders.length === 0 ? request : request.slice(separatorIndex + 1);
|
|
657
|
+
if (qualifiedProviderId !== void 0 && requestedProviderId !== void 0 && qualifiedProviderId !== requestedProviderId) return {
|
|
658
|
+
qualifiedProviderId,
|
|
659
|
+
requestedProviderId,
|
|
660
|
+
status: "conflict"
|
|
661
|
+
};
|
|
662
|
+
const requestedProviders = requestedProviderId === void 0 ? [] : config.providers.filter((provider) => provider.id === requestedProviderId);
|
|
663
|
+
if (requestedProviderId !== void 0 && requestedProviders.length === 0) return {
|
|
664
|
+
providerId: requestedProviderId,
|
|
665
|
+
status: "unknown-provider"
|
|
666
|
+
};
|
|
667
|
+
const selectedProviders = qualifiedProviders.length > 0 ? qualifiedProviders : requestedProviders;
|
|
668
|
+
const selectedProvider = selectedProviders[0];
|
|
669
|
+
if (selectedProvider !== void 0 && selectedProviders.length > 1) return {
|
|
670
|
+
providerId: selectedProvider.id,
|
|
671
|
+
status: "duplicate-provider"
|
|
672
|
+
};
|
|
673
|
+
const matchingProviders = (selectedProviders.length > 0 ? selectedProviders : config.providers).filter((provider) => provider.models.some((model) => model.id === modelId));
|
|
674
|
+
if (selectedProviders.length === 0) {
|
|
675
|
+
const duplicateProvider = matchingProviders.find((provider) => config.providers.filter((candidate) => candidate.id === provider.id).length > 1);
|
|
676
|
+
if (duplicateProvider !== void 0) return {
|
|
677
|
+
providerId: duplicateProvider.id,
|
|
678
|
+
status: "duplicate-provider"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const candidates = matchingProviders.flatMap((provider) => {
|
|
682
|
+
const model = findExactModel(provider, modelId);
|
|
683
|
+
return model === void 0 ? [] : [{
|
|
684
|
+
model,
|
|
685
|
+
provider
|
|
686
|
+
}];
|
|
687
|
+
});
|
|
688
|
+
if (candidates.length === 0) return {
|
|
689
|
+
modelId,
|
|
690
|
+
provider: selectedProviders[0],
|
|
691
|
+
status: "missing"
|
|
692
|
+
};
|
|
693
|
+
if (candidates.length > 1) return {
|
|
694
|
+
candidates: candidates.map((candidate) => ({
|
|
695
|
+
modelId: candidate.model.id,
|
|
696
|
+
providerId: candidate.provider.id
|
|
697
|
+
})),
|
|
698
|
+
status: "ambiguous"
|
|
699
|
+
};
|
|
700
|
+
return {
|
|
701
|
+
...candidates[0],
|
|
702
|
+
status: "found"
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function findExactModel(provider, modelId) {
|
|
706
|
+
const matches = provider.models.filter((model) => model.id === modelId);
|
|
707
|
+
return matches.find((model) => model.aliases?.includes("default")) ?? matches[0];
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/cli/commands/config/models/rm.ts
|
|
711
|
+
const rm$1 = defineCommand({
|
|
712
|
+
action: runRemoveModelCommand,
|
|
713
|
+
arguments: "<model-id>",
|
|
714
|
+
description: "Remove a configured model",
|
|
715
|
+
exactArguments: true,
|
|
716
|
+
name: "rm",
|
|
717
|
+
options: [{
|
|
718
|
+
description: "Provider id",
|
|
719
|
+
flags: "--provider <id>"
|
|
720
|
+
}, {
|
|
721
|
+
description: "Read and write project-local config",
|
|
722
|
+
flags: "--local"
|
|
723
|
+
}]
|
|
724
|
+
});
|
|
725
|
+
/**
|
|
726
|
+
* Removes an exact model ID from the explicitly selected setup-config scope.
|
|
727
|
+
*
|
|
728
|
+
* Triggering workflow:
|
|
729
|
+
*
|
|
730
|
+
* {@link registerCommandTree}
|
|
731
|
+
* -> `config models rm`
|
|
732
|
+
* -> {@link runRemoveModelCommand}
|
|
733
|
+
*
|
|
734
|
+
* Upstream:
|
|
735
|
+
* - {@link dispatchCommand}
|
|
736
|
+
*
|
|
737
|
+
* Downstream:
|
|
738
|
+
* - {@link resolveExactModelTarget}
|
|
739
|
+
* - {@link removeProviderModels}
|
|
740
|
+
* - {@link writeSetupConfig}
|
|
741
|
+
*/
|
|
742
|
+
async function runRemoveModelCommand(context, request, options) {
|
|
743
|
+
if (Array.isArray(options.provider)) {
|
|
744
|
+
context.io.stderr.write("config models rm accepts --provider only once.\n");
|
|
745
|
+
return 2;
|
|
746
|
+
}
|
|
747
|
+
const { config, path, scope } = await loadScopedSetupConfig(context.io, options.local);
|
|
748
|
+
const target = resolveExactModelTarget(config, request, options.provider);
|
|
749
|
+
if (target.status === "conflict") {
|
|
750
|
+
context.io.stderr.write(`model provider "${escapeLineValue(target.qualifiedProviderId)}" conflicts with --provider "${escapeLineValue(target.requestedProviderId)}".\n`);
|
|
751
|
+
return 2;
|
|
752
|
+
}
|
|
753
|
+
if (target.status === "unknown-provider") {
|
|
754
|
+
context.io.stderr.write(formatUnknownProvider(target.providerId, scope));
|
|
755
|
+
return 2;
|
|
756
|
+
}
|
|
757
|
+
if (target.status === "ambiguous") {
|
|
758
|
+
context.io.stderr.write([
|
|
759
|
+
`ambiguous model id "${escapeLineValue(request)}".`,
|
|
760
|
+
"specify <provider>/<model-id> or pass --provider <provider-id>:",
|
|
761
|
+
...target.candidates.map((candidate) => ` ${escapeLineValue(candidate.providerId)}/${escapeLineValue(candidate.modelId)}`),
|
|
762
|
+
""
|
|
763
|
+
].join("\n"));
|
|
764
|
+
return 2;
|
|
765
|
+
}
|
|
766
|
+
if (target.status === "duplicate-provider") {
|
|
767
|
+
context.io.stderr.write([
|
|
768
|
+
`provider "${escapeLineValue(target.providerId)}" is configured more than once.`,
|
|
769
|
+
"remove duplicate provider definitions from the setup configuration.",
|
|
770
|
+
""
|
|
771
|
+
].join("\n"));
|
|
772
|
+
return 2;
|
|
773
|
+
}
|
|
774
|
+
if (target.status === "missing") return 0;
|
|
775
|
+
if ((target.model.aliases ?? []).includes("default")) {
|
|
776
|
+
context.io.stderr.write(`cannot remove default model "${escapeLineValue(target.provider.id)}/${escapeLineValue(target.model.id)}". select another default first.\n`);
|
|
777
|
+
return 2;
|
|
778
|
+
}
|
|
779
|
+
await writeSetupConfig(path, removeProviderModels(config, target.provider.id, /* @__PURE__ */ new Set([target.model.id])));
|
|
780
|
+
context.io.stdout.write(`removed model: ${escapeLineValue(target.provider.id)}/${escapeLineValue(target.model.id)}\nscope: ${scope}\n`);
|
|
781
|
+
return 0;
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
406
784
|
//#region src/cli/commands/config/models/models.ts
|
|
407
785
|
const models = defineCommand({
|
|
408
786
|
children: [
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (!options.endpoint) {
|
|
412
|
-
context.io.stderr.write("config models probe requires --endpoint.\n");
|
|
413
|
-
return 2;
|
|
414
|
-
}
|
|
415
|
-
try {
|
|
416
|
-
const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
|
|
417
|
-
context.io.stdout.write(`${models.join("\n")}${models.length > 0 ? "\n" : ""}`);
|
|
418
|
-
return 0;
|
|
419
|
-
} catch (error) {
|
|
420
|
-
context.io.stderr.write(`failed to probe models: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
421
|
-
return 2;
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
description: "Probe OpenAI-compatible models",
|
|
425
|
-
examples: [["# Probe a provider endpoint and print available model ids", "alint config models probe --endpoint https://openrouter.ai/api/v1"].join("\n"), ["# Probe an endpoint that requires an authorization header", "alint config models probe --endpoint https://api.example.com/v1 --provider-header \"Authorization=Bearer $TOKEN\""].join("\n")],
|
|
426
|
-
help: ["Probe an OpenAI-compatible models endpoint before saving it in setup config.", "The command calls the provider models endpoint using the supplied endpoint and optional headers, then prints the model ids returned by that provider."].join("\n\n"),
|
|
427
|
-
name: "probe",
|
|
428
|
-
options: [{
|
|
429
|
-
description: "Provider endpoint",
|
|
430
|
-
flags: "--endpoint <url>"
|
|
431
|
-
}, {
|
|
432
|
-
description: "Provider header",
|
|
433
|
-
flags: "--provider-header <Key=Value>"
|
|
434
|
-
}]
|
|
435
|
-
}),
|
|
787
|
+
probe$1,
|
|
788
|
+
prune$1,
|
|
436
789
|
ls$1,
|
|
437
790
|
defineCommand({
|
|
438
791
|
async action(context, model) {
|
|
439
|
-
const
|
|
440
|
-
if (
|
|
792
|
+
const candidates = findModels(await loadMergedSetupConfig(context.io), model);
|
|
793
|
+
if (candidates.length === 0) {
|
|
441
794
|
context.io.stderr.write(`unknown model "${model}".\n`);
|
|
442
795
|
return 2;
|
|
443
796
|
}
|
|
444
|
-
|
|
797
|
+
if (candidates.length > 1) {
|
|
798
|
+
context.io.stderr.write(formatAmbiguousModels(model, candidates));
|
|
799
|
+
return 2;
|
|
800
|
+
}
|
|
801
|
+
context.io.stdout.write(formatModelShow(candidates[0]));
|
|
445
802
|
return 0;
|
|
446
803
|
},
|
|
447
804
|
arguments: "<model>",
|
|
448
805
|
description: "Show configured model",
|
|
449
806
|
name: "show"
|
|
450
|
-
})
|
|
807
|
+
}),
|
|
808
|
+
rm$1
|
|
451
809
|
],
|
|
452
810
|
description: "Manage configured models",
|
|
453
|
-
|
|
811
|
+
examples: [["# Remove an exact model from one provider", "alint config models rm qwen --provider ollama"].join("\n"), ["# Destructively remove models no longer reported by one provider", "alint config models prune --provider ollama -N --yes"].join("\n")],
|
|
812
|
+
help: [
|
|
813
|
+
"Inspect, list, and probe model entries from alint setup configuration. Remove exact entries when they are no longer needed.",
|
|
814
|
+
"Model entries describe the provider/model ids alint can use for model-backed rules. Probe endpoints before saving them when you want to verify what model ids a provider exposes.",
|
|
815
|
+
"Use a provider-qualified model such as ollama/qwen or pass --provider ollama when the same model id is configured for multiple providers.",
|
|
816
|
+
"Prune is destructive: it removes configured models absent from the provider response. Non-interactive pruning requires -N --yes.",
|
|
817
|
+
"Writes use global scope by default. Pass --local to select the current project's setup configuration."
|
|
818
|
+
].join("\n\n"),
|
|
454
819
|
name: "models"
|
|
455
820
|
});
|
|
456
821
|
//#endregion
|
|
@@ -492,6 +857,756 @@ const probe = defineCommand({
|
|
|
492
857
|
}]
|
|
493
858
|
});
|
|
494
859
|
//#endregion
|
|
860
|
+
//#region src/cli/commands/config/providers/set.ts
|
|
861
|
+
const set = defineCommand({
|
|
862
|
+
action: runSetProviderCommand,
|
|
863
|
+
arguments: "<key> <value>",
|
|
864
|
+
description: "Set a provider configuration field",
|
|
865
|
+
exactArguments: true,
|
|
866
|
+
name: "set",
|
|
867
|
+
options: [{
|
|
868
|
+
description: "Provider id",
|
|
869
|
+
flags: "--provider <id>"
|
|
870
|
+
}, {
|
|
871
|
+
description: "Read and write project-local config",
|
|
872
|
+
flags: "--local"
|
|
873
|
+
}]
|
|
874
|
+
});
|
|
875
|
+
/**
|
|
876
|
+
* Sets one provider field in the explicitly selected setup-config scope.
|
|
877
|
+
*
|
|
878
|
+
* Triggering workflow:
|
|
879
|
+
*
|
|
880
|
+
* {@link registerCommandTree}
|
|
881
|
+
* -> `config providers set`
|
|
882
|
+
* -> {@link runSetProviderCommand}
|
|
883
|
+
*
|
|
884
|
+
* Upstream:
|
|
885
|
+
* - {@link dispatchCommand}
|
|
886
|
+
*
|
|
887
|
+
* Downstream:
|
|
888
|
+
* - {@link setProviderEndpoint}
|
|
889
|
+
* - {@link setProviderHeader}
|
|
890
|
+
* - {@link writeSetupConfig}
|
|
891
|
+
*/
|
|
892
|
+
async function runSetProviderCommand(context, key, value, options) {
|
|
893
|
+
const providerId = scalarProvider$1(options.provider);
|
|
894
|
+
if (Array.isArray(options.provider)) {
|
|
895
|
+
context.io.stderr.write("config providers set accepts --provider only once.\n");
|
|
896
|
+
return 2;
|
|
897
|
+
}
|
|
898
|
+
if (providerId === void 0) {
|
|
899
|
+
context.io.stderr.write("config providers set requires --provider.\n");
|
|
900
|
+
return 2;
|
|
901
|
+
}
|
|
902
|
+
const { config, path, scope } = await loadScopedSetupConfig(context.io, options.local);
|
|
903
|
+
if (!config.providers.some((provider) => provider.id === providerId)) {
|
|
904
|
+
context.io.stderr.write(formatUnknownProvider(providerId, scope));
|
|
905
|
+
return 2;
|
|
906
|
+
}
|
|
907
|
+
let nextConfig;
|
|
908
|
+
if (key === "endpoint") nextConfig = setProviderEndpoint(config, providerId, value);
|
|
909
|
+
else if (key.startsWith("headers.") && key.length > 8) {
|
|
910
|
+
const headerName = key.slice(8);
|
|
911
|
+
if (!isValidProviderHeaderName(headerName)) {
|
|
912
|
+
context.io.stderr.write("invalid provider header name. expected an HTTP field-name token.\n");
|
|
913
|
+
return 2;
|
|
914
|
+
}
|
|
915
|
+
nextConfig = setProviderHeader(config, providerId, headerName, value);
|
|
916
|
+
} else {
|
|
917
|
+
context.io.stderr.write(`unsupported provider key "${escapeLineValue(key)}". expected endpoint or headers.<name>.\n`);
|
|
918
|
+
return 2;
|
|
919
|
+
}
|
|
920
|
+
await writeSetupConfig(path, nextConfig);
|
|
921
|
+
context.io.stdout.write(`provider: ${escapeLineValue(providerId)}\nkey: ${escapeLineValue(key)}\nscope: ${scope}\n`);
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
function scalarProvider$1(provider) {
|
|
925
|
+
return Array.isArray(provider) ? void 0 : provider;
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/cli/commands/config/providers/show.ts
|
|
929
|
+
const show = defineCommand({
|
|
930
|
+
async action(context, providerId) {
|
|
931
|
+
const provider = (await loadMergedSetupConfig(context.io)).providers.find((item) => item.id === providerId);
|
|
932
|
+
if (provider === void 0) {
|
|
933
|
+
context.io.stderr.write(`unknown provider "${providerId}".\n`);
|
|
934
|
+
return 2;
|
|
935
|
+
}
|
|
936
|
+
context.io.stdout.write(formatProviderShow(provider));
|
|
937
|
+
return 0;
|
|
938
|
+
},
|
|
939
|
+
arguments: "<provider>",
|
|
940
|
+
description: "Show configured provider",
|
|
941
|
+
name: "show"
|
|
942
|
+
});
|
|
943
|
+
//#endregion
|
|
944
|
+
//#region src/cli/commands/config/providers/unset.ts
|
|
945
|
+
const unset = defineCommand({
|
|
946
|
+
action: runUnsetProviderCommand,
|
|
947
|
+
arguments: "<key>",
|
|
948
|
+
description: "Unset a provider configuration field",
|
|
949
|
+
exactArguments: true,
|
|
950
|
+
name: "unset",
|
|
951
|
+
options: [{
|
|
952
|
+
description: "Provider id",
|
|
953
|
+
flags: "--provider <id>"
|
|
954
|
+
}, {
|
|
955
|
+
description: "Read and write project-local config",
|
|
956
|
+
flags: "--local"
|
|
957
|
+
}]
|
|
958
|
+
});
|
|
959
|
+
/**
|
|
960
|
+
* Unsets one provider header in the explicitly selected setup-config scope.
|
|
961
|
+
*
|
|
962
|
+
* Triggering workflow:
|
|
963
|
+
*
|
|
964
|
+
* {@link registerCommandTree}
|
|
965
|
+
* -> `config providers unset`
|
|
966
|
+
* -> {@link runUnsetProviderCommand}
|
|
967
|
+
*
|
|
968
|
+
* Upstream:
|
|
969
|
+
* - {@link dispatchCommand}
|
|
970
|
+
*
|
|
971
|
+
* Downstream:
|
|
972
|
+
* - {@link unsetProviderHeader}
|
|
973
|
+
* - {@link writeSetupConfig}
|
|
974
|
+
*/
|
|
975
|
+
async function runUnsetProviderCommand(context, key, options) {
|
|
976
|
+
const providerId = scalarProvider(options.provider);
|
|
977
|
+
if (Array.isArray(options.provider)) {
|
|
978
|
+
context.io.stderr.write("config providers unset accepts --provider only once.\n");
|
|
979
|
+
return 2;
|
|
980
|
+
}
|
|
981
|
+
if (providerId === void 0) {
|
|
982
|
+
context.io.stderr.write("config providers unset requires --provider.\n");
|
|
983
|
+
return 2;
|
|
984
|
+
}
|
|
985
|
+
const { config, path, scope } = await loadScopedSetupConfig(context.io, options.local);
|
|
986
|
+
if (!config.providers.some((provider) => provider.id === providerId)) {
|
|
987
|
+
context.io.stderr.write(formatUnknownProvider(providerId, scope));
|
|
988
|
+
return 2;
|
|
989
|
+
}
|
|
990
|
+
if (key === "endpoint") {
|
|
991
|
+
context.io.stderr.write("provider endpoint cannot be unset.\n");
|
|
992
|
+
return 2;
|
|
993
|
+
}
|
|
994
|
+
if (!key.startsWith("headers.") || key.length === 8) {
|
|
995
|
+
context.io.stderr.write(`unsupported provider key "${escapeLineValue(key)}". expected headers.<name>.\n`);
|
|
996
|
+
return 2;
|
|
997
|
+
}
|
|
998
|
+
const headerName = key.slice(8);
|
|
999
|
+
if (!isValidProviderHeaderName(headerName)) {
|
|
1000
|
+
context.io.stderr.write("invalid provider header name. expected an HTTP field-name token.\n");
|
|
1001
|
+
return 2;
|
|
1002
|
+
}
|
|
1003
|
+
await writeSetupConfig(path, unsetProviderHeader(config, providerId, headerName));
|
|
1004
|
+
context.io.stdout.write(`provider: ${escapeLineValue(providerId)}\nkey: ${escapeLineValue(key)}\nscope: ${scope}\n`);
|
|
1005
|
+
return 0;
|
|
1006
|
+
}
|
|
1007
|
+
function scalarProvider(provider) {
|
|
1008
|
+
return Array.isArray(provider) ? void 0 : provider;
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/cli/tui/provider-editor/model-selection.ts
|
|
1012
|
+
function applyDefaultAlias(config, target) {
|
|
1013
|
+
return {
|
|
1014
|
+
...config,
|
|
1015
|
+
providers: config.providers.map((provider) => ({
|
|
1016
|
+
...provider,
|
|
1017
|
+
headers: provider.headers === void 0 ? void 0 : { ...provider.headers },
|
|
1018
|
+
models: provider.models.map((model) => {
|
|
1019
|
+
const aliasesWithoutDefault = (model.aliases ?? []).filter((alias) => alias !== "default");
|
|
1020
|
+
const aliases = provider.id === target.providerId && model.id === target.modelId ? [...aliasesWithoutDefault, "default"] : aliasesWithoutDefault;
|
|
1021
|
+
return {
|
|
1022
|
+
...cloneModel(model),
|
|
1023
|
+
aliases: aliases.length > 0 ? aliases : void 0
|
|
1024
|
+
};
|
|
1025
|
+
})
|
|
1026
|
+
}))
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function applyHeaderSelection(existingHeaders, retainedNames, replacements) {
|
|
1030
|
+
const selectedNames = new Set(retainedNames.map(normalizeHeaderName));
|
|
1031
|
+
const headersByName = /* @__PURE__ */ new Map();
|
|
1032
|
+
for (const [name, value] of Object.entries(existingHeaders)) {
|
|
1033
|
+
const normalizedName = normalizeHeaderName(name);
|
|
1034
|
+
if (selectedNames.has(normalizedName)) headersByName.set(normalizedName, [name, value]);
|
|
1035
|
+
}
|
|
1036
|
+
for (const [name, value] of Object.entries(replacements)) headersByName.set(normalizeHeaderName(name), [name, value]);
|
|
1037
|
+
const headers = Object.fromEntries(headersByName.values());
|
|
1038
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
1039
|
+
}
|
|
1040
|
+
function createDefaultModelCandidates(config, newProviderId, newModelIds) {
|
|
1041
|
+
const candidates = [];
|
|
1042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1043
|
+
const newModelIdSet = new Set(newModelIds);
|
|
1044
|
+
const allModels = config.providers.flatMap((provider) => provider.models.map((model) => ({
|
|
1045
|
+
isCurrentDefault: (model.aliases ?? []).includes("default"),
|
|
1046
|
+
isNew: provider.id === newProviderId && newModelIdSet.has(model.id),
|
|
1047
|
+
model,
|
|
1048
|
+
provider,
|
|
1049
|
+
value: createDefaultModelCandidateValue(provider.id, model.id)
|
|
1050
|
+
})));
|
|
1051
|
+
const addCandidate = (candidate) => {
|
|
1052
|
+
if (candidate === void 0 || seen.has(candidate.value)) return;
|
|
1053
|
+
seen.add(candidate.value);
|
|
1054
|
+
candidates.push({
|
|
1055
|
+
isCurrentDefault: candidate.isCurrentDefault,
|
|
1056
|
+
isNew: candidate.isNew,
|
|
1057
|
+
label: `${candidate.provider.id} / ${candidate.model.id}`,
|
|
1058
|
+
modelId: candidate.model.id,
|
|
1059
|
+
providerId: candidate.provider.id,
|
|
1060
|
+
value: candidate.value
|
|
1061
|
+
});
|
|
1062
|
+
};
|
|
1063
|
+
addCandidate(allModels.find((candidate) => candidate.isCurrentDefault));
|
|
1064
|
+
for (const modelId of newModelIds) addCandidate(allModels.find((candidate) => candidate.provider.id === newProviderId && candidate.model.id === modelId));
|
|
1065
|
+
for (const candidate of allModels) addCandidate(candidate);
|
|
1066
|
+
return candidates;
|
|
1067
|
+
}
|
|
1068
|
+
function createModelOptions(provider, discoveredIds) {
|
|
1069
|
+
const discovered = new Set(discoveredIds);
|
|
1070
|
+
const existing = firstModelsById(provider);
|
|
1071
|
+
const optionIds = new Set(existing.keys());
|
|
1072
|
+
const options = [...existing.values()].map((model) => ({
|
|
1073
|
+
hint: discovered.has(model.id) ? void 0 : "not reported by provider",
|
|
1074
|
+
label: model.id,
|
|
1075
|
+
value: model.id
|
|
1076
|
+
}));
|
|
1077
|
+
for (const modelId of discoveredIds) {
|
|
1078
|
+
if (optionIds.has(modelId)) continue;
|
|
1079
|
+
optionIds.add(modelId);
|
|
1080
|
+
options.push({
|
|
1081
|
+
hint: "new",
|
|
1082
|
+
label: modelId,
|
|
1083
|
+
value: modelId
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
return options;
|
|
1087
|
+
}
|
|
1088
|
+
function modelsFromSelection(provider, selectedIds) {
|
|
1089
|
+
const existing = firstModelsById(provider);
|
|
1090
|
+
const models = [];
|
|
1091
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1092
|
+
for (const modelId of selectedIds) {
|
|
1093
|
+
if (selected.has(modelId)) continue;
|
|
1094
|
+
selected.add(modelId);
|
|
1095
|
+
const model = existing.get(modelId);
|
|
1096
|
+
models.push(model === void 0 ? {
|
|
1097
|
+
id: modelId,
|
|
1098
|
+
name: modelId
|
|
1099
|
+
} : cloneModel(model));
|
|
1100
|
+
}
|
|
1101
|
+
return models;
|
|
1102
|
+
}
|
|
1103
|
+
function normalizeHeaderName(name) {
|
|
1104
|
+
return name.toLowerCase();
|
|
1105
|
+
}
|
|
1106
|
+
function cloneModel(model) {
|
|
1107
|
+
return {
|
|
1108
|
+
...model,
|
|
1109
|
+
aliases: model.aliases === void 0 ? void 0 : [...model.aliases],
|
|
1110
|
+
capabilities: model.capabilities === void 0 ? void 0 : [...model.capabilities],
|
|
1111
|
+
defaultParams: model.defaultParams === void 0 ? void 0 : { ...model.defaultParams }
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function createDefaultModelCandidateValue(providerId, modelId) {
|
|
1115
|
+
return `${providerId}\u0000${modelId}`;
|
|
1116
|
+
}
|
|
1117
|
+
function firstModelsById(provider) {
|
|
1118
|
+
const models = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (const model of provider?.models ?? []) if (!models.has(model.id)) models.set(model.id, model);
|
|
1120
|
+
return models;
|
|
1121
|
+
}
|
|
1122
|
+
//#endregion
|
|
1123
|
+
//#region src/cli/tui/provider-editor/prompts.ts
|
|
1124
|
+
const backValue$1 = "__alint_back__";
|
|
1125
|
+
/**
|
|
1126
|
+
* Adapts Clack prompt results into the provider editor's reversible actions.
|
|
1127
|
+
*
|
|
1128
|
+
* Triggering workflow:
|
|
1129
|
+
*
|
|
1130
|
+
* {@link runProviderEditor}
|
|
1131
|
+
* -> {@link createProviderEditorPrompts}
|
|
1132
|
+
* -> `provider-editor.prompt`
|
|
1133
|
+
* -> {@link ProviderEditorPromptPort}
|
|
1134
|
+
*
|
|
1135
|
+
* Upstream:
|
|
1136
|
+
* - {@link runProviderEditor}
|
|
1137
|
+
*
|
|
1138
|
+
* Downstream:
|
|
1139
|
+
* - Clack terminal prompts and {@link probeModels}
|
|
1140
|
+
*
|
|
1141
|
+
* Cancellation and Back use tagged results, so submitted text cannot collide
|
|
1142
|
+
* with control values. Probe failures render in the spinner and return no models.
|
|
1143
|
+
*/
|
|
1144
|
+
function createProviderEditorPrompts(prompts) {
|
|
1145
|
+
return {
|
|
1146
|
+
async confirm(summary) {
|
|
1147
|
+
return selectionResult(prompts, await prompts.select({
|
|
1148
|
+
message: summary,
|
|
1149
|
+
options: withBackOption([{
|
|
1150
|
+
label: "Yes",
|
|
1151
|
+
value: "yes"
|
|
1152
|
+
}, {
|
|
1153
|
+
label: "No",
|
|
1154
|
+
value: "no"
|
|
1155
|
+
}])
|
|
1156
|
+
}));
|
|
1157
|
+
},
|
|
1158
|
+
async defaultAction(firstModelId) {
|
|
1159
|
+
return selectionResult(prompts, await prompts.select({
|
|
1160
|
+
message: `Add alias "default" to ${firstModelId}?`,
|
|
1161
|
+
options: withBackOption([
|
|
1162
|
+
{
|
|
1163
|
+
label: "Yes",
|
|
1164
|
+
value: "yes"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
label: "No",
|
|
1168
|
+
value: "no"
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
label: "Select another",
|
|
1172
|
+
value: "selectAnother"
|
|
1173
|
+
}
|
|
1174
|
+
])
|
|
1175
|
+
}));
|
|
1176
|
+
},
|
|
1177
|
+
async defaultModel(options) {
|
|
1178
|
+
return selectionResult(prompts, await prompts.select({
|
|
1179
|
+
message: "Select default model",
|
|
1180
|
+
options: withBackOption(options)
|
|
1181
|
+
}));
|
|
1182
|
+
},
|
|
1183
|
+
async endpoint(initialValue) {
|
|
1184
|
+
return textResult(prompts, await prompts.text({
|
|
1185
|
+
initialValue,
|
|
1186
|
+
message: "Provider endpoint (type .. to go back)",
|
|
1187
|
+
placeholder: initialValue ?? "https://example.test/v1",
|
|
1188
|
+
validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
|
|
1189
|
+
}));
|
|
1190
|
+
},
|
|
1191
|
+
async headerInput() {
|
|
1192
|
+
return textResult(prompts, await prompts.text({
|
|
1193
|
+
message: "Headers (leave empty to skip; type .. to go back)",
|
|
1194
|
+
placeholder: "Authorization=Bearer token, X-Test=true",
|
|
1195
|
+
validate: (value) => {
|
|
1196
|
+
if (isBackInput(value ?? "")) return;
|
|
1197
|
+
try {
|
|
1198
|
+
parseHeaderList(splitInput$1(value ?? ""));
|
|
1199
|
+
return;
|
|
1200
|
+
} catch {
|
|
1201
|
+
return "Headers must be comma-separated Key=Value entries.";
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}));
|
|
1205
|
+
},
|
|
1206
|
+
async manualModels() {
|
|
1207
|
+
const value = textResult(prompts, await prompts.text({
|
|
1208
|
+
message: "Models",
|
|
1209
|
+
placeholder: "qwen:8b, qwen:32b; type .. to go back",
|
|
1210
|
+
validate: (value) => isBackInput(value ?? "") || splitInput$1(value ?? "").length > 0 ? void 0 : "At least one model is required."
|
|
1211
|
+
}));
|
|
1212
|
+
return value.status === "submitted" ? submitted(splitInput$1(value.value)) : value;
|
|
1213
|
+
},
|
|
1214
|
+
async models(options, initialValues) {
|
|
1215
|
+
const result = await prompts.multiselect({
|
|
1216
|
+
initialValues,
|
|
1217
|
+
message: "Select models",
|
|
1218
|
+
options: withBackOption(options),
|
|
1219
|
+
required: true
|
|
1220
|
+
});
|
|
1221
|
+
if (prompts.isCancel(result)) return { status: "cancelled" };
|
|
1222
|
+
return result.includes(backValue$1) ? { status: "back" } : submitted(result);
|
|
1223
|
+
},
|
|
1224
|
+
async probe(endpoint, headers) {
|
|
1225
|
+
const spinner = prompts.spinner();
|
|
1226
|
+
spinner.start("Probing models");
|
|
1227
|
+
try {
|
|
1228
|
+
const models = await probeModels(endpoint, headers);
|
|
1229
|
+
spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
|
|
1230
|
+
return models;
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
spinner.stop(formatProbeModelsFailure(endpoint, error));
|
|
1233
|
+
return [];
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
async providerId(initialValue, editable) {
|
|
1237
|
+
if (!editable) return submitted(initialValue);
|
|
1238
|
+
return textResult(prompts, await prompts.text({
|
|
1239
|
+
initialValue,
|
|
1240
|
+
message: "Provider id (type .. to go back)",
|
|
1241
|
+
validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
|
|
1242
|
+
}));
|
|
1243
|
+
},
|
|
1244
|
+
async retainedHeaders(headerNames, initialValues) {
|
|
1245
|
+
if (headerNames.length === 0) return submitted([]);
|
|
1246
|
+
const result = await prompts.multiselect({
|
|
1247
|
+
initialValues,
|
|
1248
|
+
message: "Select existing headers to keep",
|
|
1249
|
+
options: withBackOption(headerNames.map((name) => ({
|
|
1250
|
+
label: name,
|
|
1251
|
+
value: name
|
|
1252
|
+
}))),
|
|
1253
|
+
required: false
|
|
1254
|
+
});
|
|
1255
|
+
if (prompts.isCancel(result)) return { status: "cancelled" };
|
|
1256
|
+
return result.includes(backValue$1) ? { status: "back" } : submitted(result);
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function formatProbeModelsFailure(endpoint, error) {
|
|
1261
|
+
const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
|
|
1262
|
+
return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
|
|
1263
|
+
}
|
|
1264
|
+
function isBackInput(value) {
|
|
1265
|
+
return value.trim() === "..";
|
|
1266
|
+
}
|
|
1267
|
+
function withBackOption(options) {
|
|
1268
|
+
return [...options, {
|
|
1269
|
+
label: "Back",
|
|
1270
|
+
value: backValue$1
|
|
1271
|
+
}];
|
|
1272
|
+
}
|
|
1273
|
+
function selectionResult(prompts, result) {
|
|
1274
|
+
if (prompts.isCancel(result)) return { status: "cancelled" };
|
|
1275
|
+
return result === backValue$1 ? { status: "back" } : submitted(result);
|
|
1276
|
+
}
|
|
1277
|
+
function splitInput$1(value) {
|
|
1278
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1279
|
+
}
|
|
1280
|
+
function submitted(value) {
|
|
1281
|
+
return {
|
|
1282
|
+
status: "submitted",
|
|
1283
|
+
value
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
function textResult(prompts, result) {
|
|
1287
|
+
if (prompts.isCancel(result)) return { status: "cancelled" };
|
|
1288
|
+
return isBackInput(result) ? { status: "back" } : submitted(result);
|
|
1289
|
+
}
|
|
1290
|
+
//#endregion
|
|
1291
|
+
//#region src/cli/tui/provider-editor/index.ts
|
|
1292
|
+
/**
|
|
1293
|
+
* Runs one provider create/update request as a reversible prompt state machine.
|
|
1294
|
+
*
|
|
1295
|
+
* Triggering workflow:
|
|
1296
|
+
*
|
|
1297
|
+
* {@link runInteractiveSetup}
|
|
1298
|
+
* -> {@link runProviderEditor}
|
|
1299
|
+
* -> `provider-editor.prompt-result`
|
|
1300
|
+
* -> {@link ProviderEditorResult}
|
|
1301
|
+
*
|
|
1302
|
+
* Upstream:
|
|
1303
|
+
* - {@link runInteractiveSetup}
|
|
1304
|
+
*
|
|
1305
|
+
* Downstream:
|
|
1306
|
+
* - {@link createProviderEditorPrompts} and the caller-owned setup-config write
|
|
1307
|
+
*
|
|
1308
|
+
* The editor reads the selected-scope config, changes only its in-memory draft,
|
|
1309
|
+
* and returns without persistence on Back, cancellation, or a negative confirmation.
|
|
1310
|
+
*/
|
|
1311
|
+
async function runProviderEditor(input, promptPort) {
|
|
1312
|
+
const prompts = promptPort ?? createProviderEditorPrompts(await import("@clack/prompts"));
|
|
1313
|
+
const existingProvider = input.mode === "update" ? input.existingProvider : void 0;
|
|
1314
|
+
const configuredHeaders = { ...existingProvider?.headers };
|
|
1315
|
+
const initialHeaders = applyHeaderSelection(configuredHeaders, Object.keys(configuredHeaders), {}) ?? {};
|
|
1316
|
+
const draft = {
|
|
1317
|
+
discoveredModels: [],
|
|
1318
|
+
endpoint: existingProvider?.endpoint ?? input.source.defaultEndpoint,
|
|
1319
|
+
headerValues: initialHeaders,
|
|
1320
|
+
providerId: existingProvider?.id,
|
|
1321
|
+
retainedHeaderNames: Object.keys(initialHeaders)
|
|
1322
|
+
};
|
|
1323
|
+
let step = "endpoint";
|
|
1324
|
+
while (true) {
|
|
1325
|
+
if (step === "endpoint") {
|
|
1326
|
+
const endpoint = await prompts.endpoint(draft.endpoint);
|
|
1327
|
+
if (endpoint.status === "cancelled") return { status: "cancelled" };
|
|
1328
|
+
if (endpoint.status === "back") return { status: "back" };
|
|
1329
|
+
draft.endpoint = endpoint.value.trim();
|
|
1330
|
+
step = "providerId";
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (step === "providerId") {
|
|
1334
|
+
const initialProviderId = draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(input.config.providers.map((provider) => provider.id)));
|
|
1335
|
+
const providerId = await prompts.providerId(initialProviderId, input.mode === "create");
|
|
1336
|
+
if (providerId.status === "cancelled") return { status: "cancelled" };
|
|
1337
|
+
if (providerId.status === "back") {
|
|
1338
|
+
step = "endpoint";
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
draft.providerId = providerId.value.trim();
|
|
1342
|
+
step = "retainedHeaders";
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (step === "retainedHeaders") {
|
|
1346
|
+
const headerNames = Object.keys(draft.headerValues);
|
|
1347
|
+
const retainedHeaders = await prompts.retainedHeaders(headerNames, draft.retainedHeaderNames.filter((name) => headerNames.includes(name)));
|
|
1348
|
+
if (retainedHeaders.status === "cancelled") return { status: "cancelled" };
|
|
1349
|
+
if (retainedHeaders.status === "back") {
|
|
1350
|
+
step = "providerId";
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
draft.retainedHeaderNames = retainedHeaders.value;
|
|
1354
|
+
step = "headerInput";
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (step === "headerInput") {
|
|
1358
|
+
const headerInput = await prompts.headerInput();
|
|
1359
|
+
if (headerInput.status === "cancelled") return { status: "cancelled" };
|
|
1360
|
+
if (headerInput.status === "back") {
|
|
1361
|
+
step = "retainedHeaders";
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
const replacements = parseHeaderList(splitInput(headerInput.value)) ?? {};
|
|
1365
|
+
const retainedLogicalNames = new Set([...draft.retainedHeaderNames, ...Object.keys(replacements)].map(normalizeHeaderName));
|
|
1366
|
+
draft.headerValues = applyHeaderSelection(draft.headerValues, Object.keys(draft.headerValues), replacements) ?? {};
|
|
1367
|
+
draft.retainedHeaderNames = Object.keys(draft.headerValues).filter((name) => retainedLogicalNames.has(normalizeHeaderName(name)));
|
|
1368
|
+
const headers = effectiveHeaders(draft) ?? {};
|
|
1369
|
+
draft.discoveredModels = input.source.probeModels ? await prompts.probe(draft.endpoint ?? "", headers) : [];
|
|
1370
|
+
step = "models";
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
if (step === "models") {
|
|
1374
|
+
const options = createModelOptions(existingProvider, draft.discoveredModels);
|
|
1375
|
+
const optionIds = options.map((option) => option.value);
|
|
1376
|
+
const initialValues = draft.selectedModelIds === void 0 || draft.modelOptionIds === void 0 ? optionIds : optionIds.filter((modelId) => draft.selectedModelIds?.includes(modelId) === true || !draft.modelOptionIds?.includes(modelId));
|
|
1377
|
+
draft.modelOptionIds = optionIds;
|
|
1378
|
+
const selectedModels = options.length === 0 ? await prompts.manualModels() : await prompts.models(options, initialValues);
|
|
1379
|
+
if (selectedModels.status === "cancelled") return { status: "cancelled" };
|
|
1380
|
+
if (selectedModels.status === "back") {
|
|
1381
|
+
step = "headerInput";
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
draft.selectedModelIds = selectedModels.value;
|
|
1385
|
+
step = "defaultAction";
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
if (step === "defaultAction") {
|
|
1389
|
+
const defaultAction = await prompts.defaultAction(draft.selectedModelIds?.[0] ?? "");
|
|
1390
|
+
if (defaultAction.status === "cancelled") return { status: "cancelled" };
|
|
1391
|
+
if (defaultAction.status === "back") {
|
|
1392
|
+
step = "models";
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
draft.defaultAliasTarget = defaultAction.value === "yes" ? {
|
|
1396
|
+
modelId: draft.selectedModelIds?.[0] ?? "",
|
|
1397
|
+
providerId: draft.providerId ?? ""
|
|
1398
|
+
} : void 0;
|
|
1399
|
+
const removesCurrentDefault = existingProvider?.models.some((model) => model.aliases?.includes("default") === true && !draft.selectedModelIds?.includes(model.id)) === true;
|
|
1400
|
+
step = defaultAction.value === "selectAnother" || defaultAction.value === "no" && removesCurrentDefault ? "defaultModel" : "confirm";
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const provider = createProvider(input, draft);
|
|
1404
|
+
if (step === "defaultModel") {
|
|
1405
|
+
const provisionalConfig = input.mode === "update" ? replaceSetupProvider(input.config, provider) : mergeSetupConfigs(input.config, {
|
|
1406
|
+
providers: [provider],
|
|
1407
|
+
version: 1
|
|
1408
|
+
});
|
|
1409
|
+
const existingModelIds = new Set(existingProvider?.models.map((model) => model.id) ?? []);
|
|
1410
|
+
const addedModelIds = provider.models.map((model) => model.id).filter((modelId) => !existingModelIds.has(modelId));
|
|
1411
|
+
const candidates = createDefaultModelCandidates(provisionalConfig, provider.id, addedModelIds);
|
|
1412
|
+
const selectedDefault = await prompts.defaultModel(candidates.map((candidate) => ({
|
|
1413
|
+
hint: candidate.isCurrentDefault ? "current default" : candidate.isNew ? "new" : void 0,
|
|
1414
|
+
label: candidate.label,
|
|
1415
|
+
value: candidate.value
|
|
1416
|
+
})));
|
|
1417
|
+
if (selectedDefault.status === "cancelled") return { status: "cancelled" };
|
|
1418
|
+
if (selectedDefault.status === "back") {
|
|
1419
|
+
step = "defaultAction";
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const candidate = candidates.find((item) => item.value === selectedDefault.value);
|
|
1423
|
+
if (candidate === void 0) return { status: "cancelled" };
|
|
1424
|
+
draft.defaultAliasTarget = {
|
|
1425
|
+
modelId: candidate.modelId,
|
|
1426
|
+
providerId: candidate.providerId
|
|
1427
|
+
};
|
|
1428
|
+
step = "confirm";
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
const confirmed = await prompts.confirm(createSummary(input, provider, draft.defaultAliasTarget));
|
|
1432
|
+
if (confirmed.status === "cancelled" || confirmed.status === "submitted" && confirmed.value === "no") return { status: "cancelled" };
|
|
1433
|
+
if (confirmed.status === "back") {
|
|
1434
|
+
step = "defaultAction";
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
defaultAliasTarget: draft.defaultAliasTarget,
|
|
1439
|
+
provider,
|
|
1440
|
+
status: "confirmed"
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function createProvider(input, draft) {
|
|
1445
|
+
return {
|
|
1446
|
+
...input.existingProvider,
|
|
1447
|
+
endpoint: draft.endpoint ?? "",
|
|
1448
|
+
headers: effectiveHeaders(draft),
|
|
1449
|
+
id: draft.providerId ?? "",
|
|
1450
|
+
models: modelsFromSelection(input.existingProvider, draft.selectedModelIds ?? []),
|
|
1451
|
+
type: "openai-compatible"
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function createSummary(input, provider, defaultAliasTarget) {
|
|
1455
|
+
const existingModelIds = new Set(input.existingProvider?.models.map((model) => model.id) ?? []);
|
|
1456
|
+
const nextModelIds = new Set(provider.models.map((model) => model.id));
|
|
1457
|
+
const additions = [...nextModelIds].filter((modelId) => !existingModelIds.has(modelId));
|
|
1458
|
+
const removals = [...existingModelIds].filter((modelId) => !nextModelIds.has(modelId));
|
|
1459
|
+
const defaultChange = defaultAliasTarget === void 0 ? "unchanged" : `${defaultAliasTarget.providerId}/${defaultAliasTarget.modelId}`;
|
|
1460
|
+
return [
|
|
1461
|
+
`${input.mode === "create" ? "Create" : "Update"} provider?`,
|
|
1462
|
+
`Provider: ${escapeLineValue(provider.id)}`,
|
|
1463
|
+
`Endpoint: ${escapeLineValue(provider.endpoint)}`,
|
|
1464
|
+
`Headers: ${formatSummaryValues(Object.keys(provider.headers ?? {}))}`,
|
|
1465
|
+
`Added models: ${formatSummaryValues(additions)}`,
|
|
1466
|
+
`Removed models: ${formatSummaryValues(removals)}`,
|
|
1467
|
+
`Default: ${defaultAliasTarget === void 0 ? defaultChange : escapeLineValue(defaultChange)}`
|
|
1468
|
+
].join("\n");
|
|
1469
|
+
}
|
|
1470
|
+
function effectiveHeaders(draft) {
|
|
1471
|
+
return applyHeaderSelection(draft.headerValues, draft.retainedHeaderNames, {});
|
|
1472
|
+
}
|
|
1473
|
+
function formatSummaryValues(values) {
|
|
1474
|
+
return values.length > 0 ? values.map(escapeLineValue).join(", ") : "(none)";
|
|
1475
|
+
}
|
|
1476
|
+
function splitInput(value) {
|
|
1477
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1480
|
+
//#region src/cli/commands/config/providers/update.ts
|
|
1481
|
+
const update = defineCommand({
|
|
1482
|
+
action: runUpdateProviderCommand,
|
|
1483
|
+
description: "Update a configured provider",
|
|
1484
|
+
name: "update",
|
|
1485
|
+
options: [
|
|
1486
|
+
{
|
|
1487
|
+
description: "Provider id",
|
|
1488
|
+
flags: "--provider <id>"
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
description: "Read and write project-local config",
|
|
1492
|
+
flags: "--local"
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
description: "Disable interactive editing",
|
|
1496
|
+
flags: "-N, --no-interactive"
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
description: "Replace provider endpoint before probing",
|
|
1500
|
+
flags: "--provider-endpoint <endpoint>"
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
description: "Add or replace provider header",
|
|
1504
|
+
flags: "--provider-header <Key=Value>"
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
description: "Add a model in addition to probed models",
|
|
1508
|
+
flags: "--provider-model <model>"
|
|
1509
|
+
}
|
|
1510
|
+
]
|
|
1511
|
+
});
|
|
1512
|
+
function providerUpdateSource(endpoint) {
|
|
1513
|
+
const custom = findProviderSetupSource("custom");
|
|
1514
|
+
try {
|
|
1515
|
+
return findProviderSetupSourceByEndpoint(endpoint) ?? custom;
|
|
1516
|
+
} catch {
|
|
1517
|
+
return custom;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function mergeProviderHeaders(existing, replacements) {
|
|
1521
|
+
const headers = { ...existing };
|
|
1522
|
+
for (const [replacementName, replacementValue] of Object.entries(replacements ?? {})) {
|
|
1523
|
+
for (const existingName of Object.keys(headers)) if (existingName.toLowerCase() === replacementName.toLowerCase()) delete headers[existingName];
|
|
1524
|
+
headers[replacementName] = replacementValue;
|
|
1525
|
+
}
|
|
1526
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
1527
|
+
}
|
|
1528
|
+
async function runNonInteractiveUpdate(context, options, providerEndpoint, existingProvider, config, path) {
|
|
1529
|
+
let parsedHeaders;
|
|
1530
|
+
try {
|
|
1531
|
+
parsedHeaders = parseHeaderList(toArray$1(options.providerHeader));
|
|
1532
|
+
} catch (error) {
|
|
1533
|
+
context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid provider header."}\n`);
|
|
1534
|
+
return 2;
|
|
1535
|
+
}
|
|
1536
|
+
const draft = {
|
|
1537
|
+
...existingProvider,
|
|
1538
|
+
endpoint: providerEndpoint ?? existingProvider.endpoint,
|
|
1539
|
+
headers: mergeProviderHeaders(existingProvider.headers, parsedHeaders)
|
|
1540
|
+
};
|
|
1541
|
+
let remoteModelIds;
|
|
1542
|
+
try {
|
|
1543
|
+
remoteModelIds = await probeModels(draft.endpoint, draft.headers);
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? "Unknown error."}\n`);
|
|
1546
|
+
return 2;
|
|
1547
|
+
}
|
|
1548
|
+
await writeSetupConfig(path, addProviderModels(addProviderModels(replaceSetupProvider(config, draft), draft.id, remoteModelIds), draft.id, toArray$1(options.providerModel)));
|
|
1549
|
+
return 0;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Updates one provider in the explicitly selected setup-config scope.
|
|
1553
|
+
*
|
|
1554
|
+
* Triggering workflow:
|
|
1555
|
+
*
|
|
1556
|
+
* {@link registerCommandTree}
|
|
1557
|
+
* -> `config providers update`
|
|
1558
|
+
* -> {@link runUpdateProviderCommand}
|
|
1559
|
+
*
|
|
1560
|
+
* Upstream:
|
|
1561
|
+
* - {@link dispatchCommand}
|
|
1562
|
+
*
|
|
1563
|
+
* Downstream:
|
|
1564
|
+
* - {@link runProviderEditor}
|
|
1565
|
+
* - {@link probeModels}
|
|
1566
|
+
* - {@link writeSetupConfig}
|
|
1567
|
+
*/
|
|
1568
|
+
async function runUpdateProviderCommand(context, options) {
|
|
1569
|
+
let providerEndpoint;
|
|
1570
|
+
let providerId;
|
|
1571
|
+
try {
|
|
1572
|
+
providerId = scalarOption(options.provider, "--provider");
|
|
1573
|
+
providerEndpoint = scalarOption(options.providerEndpoint, "--provider-endpoint");
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid provider update option."}\n`);
|
|
1576
|
+
return 2;
|
|
1577
|
+
}
|
|
1578
|
+
if (providerId === void 0) {
|
|
1579
|
+
context.io.stderr.write("config providers update requires --provider.\n");
|
|
1580
|
+
return 2;
|
|
1581
|
+
}
|
|
1582
|
+
const { config, path, scope } = await loadScopedSetupConfig(context.io, options.local);
|
|
1583
|
+
const existingProvider = config.providers.find((provider) => provider.id === providerId);
|
|
1584
|
+
if (existingProvider === void 0) {
|
|
1585
|
+
context.io.stderr.write(formatUnknownProvider(providerId, scope));
|
|
1586
|
+
return 2;
|
|
1587
|
+
}
|
|
1588
|
+
if (context.setupNoInteractive) return runNonInteractiveUpdate(context, options, providerEndpoint, existingProvider, config, path);
|
|
1589
|
+
const result = await runProviderEditor({
|
|
1590
|
+
config,
|
|
1591
|
+
existingProvider,
|
|
1592
|
+
io: context.io,
|
|
1593
|
+
mode: "update",
|
|
1594
|
+
source: providerUpdateSource(existingProvider.endpoint)
|
|
1595
|
+
});
|
|
1596
|
+
if (result.status !== "confirmed") return 0;
|
|
1597
|
+
const replaced = replaceSetupProvider(config, result.provider);
|
|
1598
|
+
await writeSetupConfig(path, result.defaultAliasTarget === void 0 ? replaced : applyDefaultAlias(replaced, result.defaultAliasTarget));
|
|
1599
|
+
return 0;
|
|
1600
|
+
}
|
|
1601
|
+
function scalarOption(value, flag) {
|
|
1602
|
+
if (Array.isArray(value)) throw new TypeError(`config providers update accepts ${flag} only once.`);
|
|
1603
|
+
return value;
|
|
1604
|
+
}
|
|
1605
|
+
function toArray$1(value) {
|
|
1606
|
+
if (value === void 0) return [];
|
|
1607
|
+
return Array.isArray(value) ? value : [value];
|
|
1608
|
+
}
|
|
1609
|
+
//#endregion
|
|
495
1610
|
//#region src/cli/commands/config/config.ts
|
|
496
1611
|
const config = defineCommand({
|
|
497
1612
|
children: [
|
|
@@ -500,23 +1615,25 @@ const config = defineCommand({
|
|
|
500
1615
|
defineCommand({
|
|
501
1616
|
children: [
|
|
502
1617
|
ls,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return 2;
|
|
509
|
-
}
|
|
510
|
-
context.io.stdout.write(formatProviderShow(provider));
|
|
511
|
-
return 0;
|
|
512
|
-
},
|
|
513
|
-
arguments: "<provider>",
|
|
514
|
-
description: "Show configured provider",
|
|
515
|
-
name: "show"
|
|
516
|
-
}),
|
|
517
|
-
probe
|
|
1618
|
+
show,
|
|
1619
|
+
probe,
|
|
1620
|
+
update,
|
|
1621
|
+
set,
|
|
1622
|
+
unset
|
|
518
1623
|
],
|
|
519
1624
|
description: "Manage configured providers",
|
|
1625
|
+
examples: [["# Probe and add newly reported models", "alint config providers update --provider openrouter"].join("\n"), [
|
|
1626
|
+
"# Set or unset one provider header",
|
|
1627
|
+
"alint config providers set --provider openrouter headers.Authorization \"Bearer $TOKEN\"",
|
|
1628
|
+
"alint config providers unset --provider openrouter headers.Authorization"
|
|
1629
|
+
].join("\n")],
|
|
1630
|
+
help: [
|
|
1631
|
+
"Inspect and edit providers in alint setup configuration.",
|
|
1632
|
+
"Provider update is additive by default. It does not automatically remove a configured model merely because the remote provider no longer reports it.",
|
|
1633
|
+
"Deselecting a configured model in the interactive TUI removes it when you confirm the update. Use the destructive config models prune command to remove configured models absent from provider responses.",
|
|
1634
|
+
"Writes use global scope by default. Pass --local to select the current project's setup configuration.",
|
|
1635
|
+
"Header values passed as command arguments may remain in shell history. Use environment-variable placeholders such as $TOKEN instead of literal secrets."
|
|
1636
|
+
].join("\n\n"),
|
|
520
1637
|
name: "providers"
|
|
521
1638
|
})
|
|
522
1639
|
],
|
|
@@ -538,9 +1655,21 @@ const config = defineCommand({
|
|
|
538
1655
|
"# Probe an OpenAI-compatible endpoint before saving it",
|
|
539
1656
|
"alint config providers probe --endpoint https://openrouter.ai/api/v1",
|
|
540
1657
|
"alint config models probe --endpoint https://openrouter.ai/api/v1"
|
|
1658
|
+
].join("\n"),
|
|
1659
|
+
["# Probe and add newly reported provider models", "alint config providers update --provider openrouter"].join("\n"),
|
|
1660
|
+
[
|
|
1661
|
+
"# Remove one model or prune models missing from a provider",
|
|
1662
|
+
"alint config models rm qwen --provider ollama",
|
|
1663
|
+
"alint config models prune --provider ollama -N --yes"
|
|
541
1664
|
].join("\n")
|
|
542
1665
|
],
|
|
543
|
-
help: [
|
|
1666
|
+
help: [
|
|
1667
|
+
"Inspect and update alint setup/configuration state.",
|
|
1668
|
+
"Use these commands to understand the effective config for a file, inspect saved provider/model setup, and probe OpenAI-compatible endpoints before using them in model-backed rules.",
|
|
1669
|
+
"Configuration writes use global scope by default. Pass --local to read and write the current project's .alint/config.toml instead.",
|
|
1670
|
+
"Provider updates are additive by default. They do not automatically remove configured models merely because the remote provider no longer reports them.",
|
|
1671
|
+
"Interactive TUI deselection can remove a configured model. Model pruning is destructive and requires confirmation."
|
|
1672
|
+
].join("\n\n"),
|
|
544
1673
|
name: "config"
|
|
545
1674
|
});
|
|
546
1675
|
//#endregion
|
|
@@ -582,7 +1711,7 @@ function formatStylish(input, options = {}) {
|
|
|
582
1711
|
function countCachedDiagnostics(diagnostics, severity) {
|
|
583
1712
|
return diagnostics.filter((diagnostic) => diagnostic.severity === severity && diagnostic.cached === true).length;
|
|
584
1713
|
}
|
|
585
|
-
function countDiagnostics$
|
|
1714
|
+
function countDiagnostics$1(diagnostics, severity) {
|
|
586
1715
|
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
587
1716
|
}
|
|
588
1717
|
function createStyle(color) {
|
|
@@ -611,8 +1740,8 @@ function formatExecutionSummary(execution) {
|
|
|
611
1740
|
return `${execution.cached}/${execution.planned} cached`;
|
|
612
1741
|
}
|
|
613
1742
|
function formatSummary(diagnostics, result, style) {
|
|
614
|
-
const warnCount = countDiagnostics$
|
|
615
|
-
const errorCount = countDiagnostics$
|
|
1743
|
+
const warnCount = countDiagnostics$1(diagnostics, "warn");
|
|
1744
|
+
const errorCount = countDiagnostics$1(diagnostics, "error");
|
|
616
1745
|
const cachedWarnCount = countCachedDiagnostics(diagnostics, "warn");
|
|
617
1746
|
const cachedErrorCount = countCachedDiagnostics(diagnostics, "error");
|
|
618
1747
|
const tokens = result === void 0 ? void 0 : formatTokenSummary(result);
|
|
@@ -650,19 +1779,18 @@ function createPlainProgressReporter(options) {
|
|
|
650
1779
|
const target = payload.job.target.name ? `${payload.job.target.kind} ${payload.job.target.name}` : payload.job.target.kind;
|
|
651
1780
|
writeLine(`scan ${payload.job.inputPath} > ${target} > ${payload.job.ruleId}`);
|
|
652
1781
|
},
|
|
1782
|
+
onPrepareEnd: ({ filesTotal }) => writeLine(`alint prepared: ${filesTotal} files`),
|
|
1783
|
+
onPrepareStart: () => writeLine("alint preparing"),
|
|
653
1784
|
onRunEnd: (payload) => {
|
|
654
|
-
const warnCount = countDiagnostics
|
|
655
|
-
const errorCount = countDiagnostics
|
|
1785
|
+
const warnCount = countDiagnostics(payload.diagnostics, "warn");
|
|
1786
|
+
const errorCount = countDiagnostics(payload.diagnostics, "error");
|
|
656
1787
|
const state = payload.execution.failed > 0 ? "failed" : "finished";
|
|
657
1788
|
const { cached, cancelled, completed, failed, skipped } = payload.execution;
|
|
658
1789
|
writeLine(`alint ${state}: ${warnCount} warn, ${errorCount} error, ${payload.usage.totalTokens} tokens, ${completed} completed, ${cached} cached, ${failed} failed, ${cancelled} cancelled, ${skipped} skipped`);
|
|
659
|
-
},
|
|
660
|
-
onRunStart: (payload) => {
|
|
661
|
-
writeLine(`alint started: ${payload.jobsTotal} queued jobs`);
|
|
662
1790
|
}
|
|
663
1791
|
};
|
|
664
1792
|
}
|
|
665
|
-
function countDiagnostics
|
|
1793
|
+
function countDiagnostics(diagnostics, severity) {
|
|
666
1794
|
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
667
1795
|
}
|
|
668
1796
|
//#endregion
|
|
@@ -686,9 +1814,9 @@ function formatMiniBar(options) {
|
|
|
686
1814
|
if (index <= frontierIndex) return "█";
|
|
687
1815
|
return "░";
|
|
688
1816
|
}
|
|
689
|
-
if (index < phase
|
|
690
|
-
if (index === phase - 1) return "░";
|
|
1817
|
+
if (index < phase) return "█";
|
|
691
1818
|
if (index === phase) return "▓";
|
|
1819
|
+
if (index === phase + 1 && index <= frontierIndex) return "░";
|
|
692
1820
|
if (index <= frontierIndex) return "█";
|
|
693
1821
|
return "░";
|
|
694
1822
|
}).join("")}]`;
|
|
@@ -708,35 +1836,48 @@ function createSummaryProgressReporter(options) {
|
|
|
708
1836
|
const state = createInitialState();
|
|
709
1837
|
return {
|
|
710
1838
|
getRows: () => createRows(state, options, now()),
|
|
711
|
-
onDiagnostic: ({ diagnostic }) => {
|
|
712
|
-
state
|
|
1839
|
+
onDiagnostic: ({ diagnostic, progress }) => {
|
|
1840
|
+
updateProgress(state, progress);
|
|
1841
|
+
if (diagnostic.severity === "warn") state.warnCount += 1;
|
|
1842
|
+
else state.errorCount += 1;
|
|
1843
|
+
},
|
|
1844
|
+
onExecuteEnd: ({ progress }) => {
|
|
1845
|
+
updateProgress(state, progress);
|
|
713
1846
|
},
|
|
1847
|
+
onExecuteStart: ({ progress, startedAt }) => {
|
|
1848
|
+
updateProgress(state, progress);
|
|
1849
|
+
state.runStartedAt = startedAt ?? state.runStartedAt ?? now();
|
|
1850
|
+
},
|
|
1851
|
+
onFileReady: ({ progress }) => updateProgress(state, progress),
|
|
714
1852
|
onJobEnd: (payload) => {
|
|
715
|
-
|
|
716
|
-
|
|
1853
|
+
updateProgress(state, payload.progress);
|
|
1854
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
1855
|
+
const group = state.ruleGroups.get(payload.job.ruleId);
|
|
1856
|
+
if (!group) throw new Error(`Cannot finish unqueued progress job "${payload.job.id}".`);
|
|
1857
|
+
group[payload.state] += 1;
|
|
1858
|
+
if (payload.endedAt !== void 0 && current?.startedAt !== void 0 && payload.state !== "cached") group.terminalDurationMs += Math.max(payload.endedAt - current.startedAt, 0);
|
|
1859
|
+
if (payload.state === "cached") {
|
|
717
1860
|
const cachedTokens = state.jobTokens.get(payload.job.id) ?? 0;
|
|
718
1861
|
state.totalTokens = Math.max(state.totalTokens - cachedTokens, 0);
|
|
719
1862
|
state.cachedTokens += cachedTokens;
|
|
720
1863
|
}
|
|
721
|
-
state.
|
|
722
|
-
|
|
723
|
-
job: payload.job,
|
|
724
|
-
retry: current?.retry,
|
|
725
|
-
startedAt: payload.startedAt ?? current?.startedAt,
|
|
726
|
-
state: payload.state
|
|
727
|
-
});
|
|
728
|
-
transition(state.execution, "running", payload.state);
|
|
1864
|
+
state.activeJobs.delete(payload.job.id);
|
|
1865
|
+
state.jobTokens.delete(payload.job.id);
|
|
729
1866
|
},
|
|
730
|
-
onJobQueued: ({ job }) => {
|
|
731
|
-
state
|
|
1867
|
+
onJobQueued: ({ job, progress }) => {
|
|
1868
|
+
updateProgress(state, progress);
|
|
1869
|
+
const group = state.ruleGroups.get(job.ruleId) ?? createRuleGroup(job);
|
|
1870
|
+
group.planned += 1;
|
|
1871
|
+
state.ruleGroups.set(job.ruleId, group);
|
|
1872
|
+
state.activeJobs.set(job.id, {
|
|
732
1873
|
job,
|
|
733
1874
|
state: "queued"
|
|
734
1875
|
});
|
|
735
|
-
state.execution.queued += 1;
|
|
736
1876
|
},
|
|
737
1877
|
onJobRetry: (payload) => {
|
|
738
|
-
|
|
739
|
-
state.
|
|
1878
|
+
updateProgress(state, payload.progress);
|
|
1879
|
+
const current = state.activeJobs.get(payload.job.id);
|
|
1880
|
+
state.activeJobs.set(payload.job.id, {
|
|
740
1881
|
job: payload.job,
|
|
741
1882
|
retry: {
|
|
742
1883
|
attempt: payload.attempt,
|
|
@@ -748,39 +1889,58 @@ function createSummaryProgressReporter(options) {
|
|
|
748
1889
|
});
|
|
749
1890
|
},
|
|
750
1891
|
onJobStart: (payload) => {
|
|
751
|
-
state
|
|
1892
|
+
updateProgress(state, payload.progress);
|
|
1893
|
+
state.activeJobs.set(payload.job.id, {
|
|
752
1894
|
job: payload.job,
|
|
753
1895
|
startedAt: payload.startedAt ?? now(),
|
|
754
1896
|
state: "running"
|
|
755
1897
|
});
|
|
756
|
-
transition(state.execution, "queued", "running");
|
|
757
|
-
},
|
|
758
|
-
onRunEnd: (payload) => {
|
|
759
|
-
state.diagnostics = [...payload.diagnostics];
|
|
760
|
-
state.execution = { ...payload.execution };
|
|
761
|
-
state.totalTokens = payload.usage.totalTokens;
|
|
762
|
-
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
763
|
-
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
764
1898
|
},
|
|
765
|
-
|
|
766
|
-
state.
|
|
767
|
-
state.
|
|
768
|
-
state.
|
|
1899
|
+
onPrepareStart: ({ startedAt }) => {
|
|
1900
|
+
state.activeJobs.clear();
|
|
1901
|
+
state.errorCount = 0;
|
|
1902
|
+
state.execution = createCounts();
|
|
1903
|
+
state.progress = createProgress();
|
|
1904
|
+
state.ruleGroups.clear();
|
|
769
1905
|
state.runStartedAt = startedAt ?? now();
|
|
770
|
-
state.
|
|
1906
|
+
state.animationTick = 0;
|
|
771
1907
|
state.cachedTokens = 0;
|
|
772
1908
|
state.jobTokens.clear();
|
|
773
1909
|
state.totalTokens = 0;
|
|
1910
|
+
state.warnCount = 0;
|
|
1911
|
+
},
|
|
1912
|
+
onRunEnd: (payload) => {
|
|
1913
|
+
state.warnCount = payload.diagnostics.filter((diagnostic) => diagnostic.severity === "warn").length;
|
|
1914
|
+
state.errorCount = payload.diagnostics.length - state.warnCount;
|
|
1915
|
+
updateProgress(state, payload.progress);
|
|
1916
|
+
state.totalTokens = payload.usage.totalTokens;
|
|
1917
|
+
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
1918
|
+
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
774
1919
|
},
|
|
775
|
-
onUsage: ({ job, record }) => {
|
|
1920
|
+
onUsage: ({ job, progress, record }) => {
|
|
1921
|
+
updateProgress(state, progress);
|
|
776
1922
|
if (record.totalTokens != null && Number.isFinite(record.totalTokens)) {
|
|
777
1923
|
state.jobTokens.set(job.id, (state.jobTokens.get(job.id) ?? 0) + record.totalTokens);
|
|
778
|
-
|
|
779
|
-
else state.totalTokens += record.totalTokens;
|
|
1924
|
+
state.totalTokens += record.totalTokens;
|
|
780
1925
|
}
|
|
781
1926
|
},
|
|
1927
|
+
/**
|
|
1928
|
+
* Advances the summary animation on each renderer interval.
|
|
1929
|
+
*
|
|
1930
|
+
* Triggering workflow:
|
|
1931
|
+
*
|
|
1932
|
+
* `createTtyProgressRenderer`
|
|
1933
|
+
* -> `createCliProgressReporter` `createInterval` callback
|
|
1934
|
+
* -> `SummaryProgressReporter.tick` (this handler)
|
|
1935
|
+
*
|
|
1936
|
+
* Upstream:
|
|
1937
|
+
* - `createCliProgressReporter` `createInterval` callback registered by `createTtyProgressRenderer`
|
|
1938
|
+
*
|
|
1939
|
+
* Downstream:
|
|
1940
|
+
* - Increments `state.animationTick`, consumed by {@link createRows}, {@link formatSpinnerFrame}, and {@link formatMiniBarSegment}.
|
|
1941
|
+
*/
|
|
782
1942
|
tick: () => {
|
|
783
|
-
state.
|
|
1943
|
+
state.animationTick += 1;
|
|
784
1944
|
}
|
|
785
1945
|
};
|
|
786
1946
|
}
|
|
@@ -806,9 +1966,6 @@ function completeGraphemeBoundary(input, maximumIndex) {
|
|
|
806
1966
|
}
|
|
807
1967
|
return boundary;
|
|
808
1968
|
}
|
|
809
|
-
function countDiagnostics(diagnostics, severity) {
|
|
810
|
-
return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
|
|
811
|
-
}
|
|
812
1969
|
function createCounts(planned = 0) {
|
|
813
1970
|
return {
|
|
814
1971
|
cached: 0,
|
|
@@ -823,19 +1980,30 @@ function createCounts(planned = 0) {
|
|
|
823
1980
|
}
|
|
824
1981
|
function createInitialState() {
|
|
825
1982
|
return {
|
|
1983
|
+
activeJobs: /* @__PURE__ */ new Map(),
|
|
1984
|
+
animationTick: 0,
|
|
826
1985
|
cachedTokens: 0,
|
|
827
|
-
|
|
1986
|
+
errorCount: 0,
|
|
828
1987
|
execution: createCounts(),
|
|
829
|
-
jobs: /* @__PURE__ */ new Map(),
|
|
830
1988
|
jobTokens: /* @__PURE__ */ new Map(),
|
|
831
|
-
|
|
832
|
-
|
|
1989
|
+
progress: createProgress(),
|
|
1990
|
+
ruleGroups: /* @__PURE__ */ new Map(),
|
|
1991
|
+
totalTokens: 0,
|
|
1992
|
+
warnCount: 0
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function createProgress() {
|
|
1996
|
+
return {
|
|
1997
|
+
execution: createCounts(),
|
|
1998
|
+
filesTotal: 0,
|
|
1999
|
+
final: false,
|
|
2000
|
+
jobsCompleted: 0,
|
|
2001
|
+
jobsStarted: 0,
|
|
2002
|
+
jobsTotal: 0
|
|
833
2003
|
};
|
|
834
2004
|
}
|
|
835
2005
|
function createRows(state, options, now) {
|
|
836
2006
|
if (options.rows !== void 0 && options.rows <= 0) return [];
|
|
837
|
-
const warnCount = countDiagnostics(state.diagnostics, "warn");
|
|
838
|
-
const errorCount = countDiagnostics(state.diagnostics, "error");
|
|
839
2007
|
const groups = createRuleGroups(state);
|
|
840
2008
|
const footerRows = options.rows === void 0 ? 3 : Math.min(options.rows, 3);
|
|
841
2009
|
const separatorRows = options.rows === void 0 || options.rows - footerRows > 1 ? 1 : 0;
|
|
@@ -845,34 +2013,27 @@ function createRows(state, options, now) {
|
|
|
845
2013
|
const visibleFooters = options.rows === void 0 ? footer : footer.slice(0, Math.max(options.rows - visible.length - actualSeparatorRows, 0));
|
|
846
2014
|
if (actualSeparatorRows === 1) visible.push("");
|
|
847
2015
|
visible.push(...visibleFooters);
|
|
848
|
-
return options.color ? visible.map((row) => styleRow(row, state, warnCount, errorCount, options)) : visible;
|
|
2016
|
+
return options.color ? visible.map((row) => styleRow(row, state, state.warnCount, state.errorCount, options)) : visible;
|
|
2017
|
+
}
|
|
2018
|
+
function createRuleGroup(job) {
|
|
2019
|
+
return {
|
|
2020
|
+
cached: 0,
|
|
2021
|
+
cancelled: 0,
|
|
2022
|
+
completed: 0,
|
|
2023
|
+
failed: 0,
|
|
2024
|
+
firstJobIndex: job.index,
|
|
2025
|
+
planned: 0,
|
|
2026
|
+
ruleId: job.ruleId,
|
|
2027
|
+
skipped: 0,
|
|
2028
|
+
terminalDurationMs: 0
|
|
2029
|
+
};
|
|
849
2030
|
}
|
|
850
2031
|
function createRuleGroups(state) {
|
|
851
|
-
const groups =
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
completed: 0,
|
|
857
|
-
failed: 0,
|
|
858
|
-
firstJobIndex: job.job.index,
|
|
859
|
-
jobs: [],
|
|
860
|
-
planned: 0,
|
|
861
|
-
ruleId: job.job.ruleId,
|
|
862
|
-
skipped: 0,
|
|
863
|
-
terminalDurationMs: 0
|
|
864
|
-
};
|
|
865
|
-
group.firstJobIndex = Math.min(group.firstJobIndex, job.job.index);
|
|
866
|
-
group.jobs.push(job);
|
|
867
|
-
group.planned = Math.max(group.planned, job.job.ruleTotal);
|
|
868
|
-
if (job.state === "cached") group.cached += 1;
|
|
869
|
-
if (job.state === "cancelled") group.cancelled += 1;
|
|
870
|
-
if (job.state === "completed") group.completed += 1;
|
|
871
|
-
if (job.state === "failed") group.failed += 1;
|
|
872
|
-
if (job.state === "skipped") group.skipped += 1;
|
|
873
|
-
if (job.endedAt !== void 0 && job.startedAt !== void 0 && job.state !== "cached") group.terminalDurationMs += Math.max(job.endedAt - job.startedAt, 0);
|
|
874
|
-
groups.set(group.ruleId, group);
|
|
875
|
-
}
|
|
2032
|
+
const groups = new Map([...state.ruleGroups].map(([ruleId, group]) => [ruleId, {
|
|
2033
|
+
...group,
|
|
2034
|
+
jobs: []
|
|
2035
|
+
}]));
|
|
2036
|
+
for (const job of state.activeJobs.values()) groups.get(job.job.ruleId)?.jobs.push(job);
|
|
876
2037
|
return [...groups.values()].sort(compareRuleGroups);
|
|
877
2038
|
}
|
|
878
2039
|
function estimateEta(durationMs, terminal, planned) {
|
|
@@ -937,11 +2098,12 @@ function formatDuration(ms) {
|
|
|
937
2098
|
function formatFooters(state, options, now) {
|
|
938
2099
|
const terminal = terminalCount(state.execution);
|
|
939
2100
|
const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
|
|
940
|
-
const progressBar = formatMiniBarSegment(terminal, state.execution.planned, state.
|
|
941
|
-
const eta = estimateEta(elapsed, terminal, state.execution.planned);
|
|
942
|
-
const projectedTokens = terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
2101
|
+
const progressBar = state.progress.final ? formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options) : "";
|
|
2102
|
+
const eta = state.progress.final ? estimateEta(elapsed, terminal, state.execution.planned) : "eta ?";
|
|
2103
|
+
const projectedTokens = state.progress.final && terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
2104
|
+
const discovering = state.progress.final ? "" : " jobs (discovering)";
|
|
943
2105
|
return [
|
|
944
|
-
fitRow(`${terminal}/${state.execution.planned}${progressBar} ${formatDuration(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
|
|
2106
|
+
fitRow(`${terminal}/${state.execution.planned}${discovering}${progressBar} ${formatDuration(elapsed)} -> ${eta === "eta ?" ? "~?" : eta.replace("eta ", "")}`, options.columns),
|
|
945
2107
|
fitRow(`${state.execution.running} concurrency / ${state.execution.queued} queued / ${state.execution.cached} cached / ${state.execution.failed} failed`, options.columns),
|
|
946
2108
|
fitRow(`${state.totalTokens.toLocaleString("en-US")} tokens (${state.cachedTokens.toLocaleString("en-US")} cached) -> ${projectedTokens === void 0 ? "~?" : `~${projectedTokens.toLocaleString("en-US")} tokens`}`, options.columns)
|
|
947
2109
|
];
|
|
@@ -956,15 +2118,18 @@ function formatMiniBarSegment(completed, planned, tick, options) {
|
|
|
956
2118
|
})}`;
|
|
957
2119
|
}
|
|
958
2120
|
function formatRuleRow(group, state, options) {
|
|
959
|
-
const spinner = options.spinnerFrames
|
|
2121
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
960
2122
|
const terminal = group.completed + group.cached + group.failed + group.cancelled + group.skipped;
|
|
961
2123
|
const percent = group.planned > 0 ? Math.floor(terminal / group.planned * 100) : 0;
|
|
962
|
-
const bar = formatMiniBarSegment(terminal, group.planned, state.
|
|
2124
|
+
const bar = formatMiniBarSegment(terminal, group.planned, state.animationTick, options);
|
|
963
2125
|
const running = group.jobs.filter((job) => job.state === "running").length;
|
|
964
2126
|
const failed = group.failed > 0 ? ` ${group.failed} failed` : "";
|
|
965
2127
|
const eta = estimateEta(group.terminalDurationMs, terminal, group.planned);
|
|
966
2128
|
return fitRow(`${spinner} ${group.ruleId} ${terminal}/${group.planned} ${percent}%${bar} ${eta} ${running} running${failed}`, options.columns);
|
|
967
2129
|
}
|
|
2130
|
+
function formatSpinnerFrame(animationTick, spinnerFrames) {
|
|
2131
|
+
return spinnerFrames[animationTick % Math.max(spinnerFrames.length, 1)] ?? "";
|
|
2132
|
+
}
|
|
968
2133
|
function formatTargetRow(jobState, options, now, last) {
|
|
969
2134
|
const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
|
|
970
2135
|
const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
|
|
@@ -973,16 +2138,19 @@ function formatTargetRow(jobState, options, now, last) {
|
|
|
973
2138
|
}
|
|
974
2139
|
function styleRow(row, state, warnCount, errorCount, options) {
|
|
975
2140
|
let styledRow = row;
|
|
976
|
-
const spinner = options.spinnerFrames
|
|
2141
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
977
2142
|
if (spinner) styledRow = styledRow.replace(spinner, colors$2.cyan(spinner));
|
|
978
2143
|
return styledRow.replace(`${state.execution.running} concurrency`, colors$2.cyan(`${state.execution.running} concurrency`)).replace(`${state.totalTokens.toLocaleString("en-US")} tokens`, colors$2.cyan(`${state.totalTokens.toLocaleString("en-US")} tokens`)).replace(`(${state.cachedTokens.toLocaleString("en-US")} cached)`, colors$2.dim(`(${state.cachedTokens.toLocaleString("en-US")} cached)`)).replace(` / ${state.execution.cached} cached / `, ` / ${colors$2.dim(`${state.execution.cached} cached`)} / `).replace(`${warnCount} warn`, colors$2.yellow(`${warnCount} warn`)).replace(`${errorCount} error`, (errorCount > 0 ? colors$2.red : colors$2.gray)(`${errorCount} error`)).replace(`${state.execution.failed} failed`, (state.execution.failed > 0 ? colors$2.red : colors$2.gray)(`${state.execution.failed} failed`)).replace(/\[([█▓░]*?)(░+)\]/g, (_, active, pending) => `[${active}${colors$2.gray(pending)}]`).replace(/\//g, colors$2.gray("/"));
|
|
979
2144
|
}
|
|
980
2145
|
function terminalCount(counts) {
|
|
981
2146
|
return counts.completed + counts.failed + counts.cached + counts.skipped + counts.cancelled;
|
|
982
2147
|
}
|
|
983
|
-
function
|
|
984
|
-
|
|
985
|
-
|
|
2148
|
+
function updateProgress(state, progress) {
|
|
2149
|
+
state.execution = { ...progress.execution };
|
|
2150
|
+
state.progress = {
|
|
2151
|
+
...progress,
|
|
2152
|
+
execution: { ...progress.execution }
|
|
2153
|
+
};
|
|
986
2154
|
}
|
|
987
2155
|
//#endregion
|
|
988
2156
|
//#region src/cli/reporters/progress/tty.ts
|
|
@@ -1072,6 +2240,18 @@ function createRenderingProgressReporter(summary, renderer) {
|
|
|
1072
2240
|
summary.onDiagnostic?.(payload);
|
|
1073
2241
|
renderer.render();
|
|
1074
2242
|
},
|
|
2243
|
+
onExecuteEnd: (payload) => {
|
|
2244
|
+
summary.onExecuteEnd?.(payload);
|
|
2245
|
+
renderer.render();
|
|
2246
|
+
},
|
|
2247
|
+
onExecuteStart: (payload) => {
|
|
2248
|
+
summary.onExecuteStart?.(payload);
|
|
2249
|
+
renderer.render();
|
|
2250
|
+
},
|
|
2251
|
+
onFileReady: (payload) => {
|
|
2252
|
+
summary.onFileReady?.(payload);
|
|
2253
|
+
renderer.render();
|
|
2254
|
+
},
|
|
1075
2255
|
onJobEnd: (payload) => {
|
|
1076
2256
|
summary.onJobEnd?.(payload);
|
|
1077
2257
|
renderer.render();
|
|
@@ -1088,14 +2268,18 @@ function createRenderingProgressReporter(summary, renderer) {
|
|
|
1088
2268
|
summary.onJobStart?.(payload);
|
|
1089
2269
|
renderer.render();
|
|
1090
2270
|
},
|
|
1091
|
-
|
|
1092
|
-
summary.
|
|
2271
|
+
onPrepareEnd: (payload) => {
|
|
2272
|
+
summary.onPrepareEnd?.(payload);
|
|
1093
2273
|
renderer.render();
|
|
1094
2274
|
},
|
|
1095
|
-
|
|
1096
|
-
summary.
|
|
2275
|
+
onPrepareStart: (payload) => {
|
|
2276
|
+
summary.onPrepareStart?.(payload);
|
|
1097
2277
|
renderer.start();
|
|
1098
2278
|
},
|
|
2279
|
+
onRunEnd: (payload) => {
|
|
2280
|
+
summary.onRunEnd?.(payload);
|
|
2281
|
+
renderer.render();
|
|
2282
|
+
},
|
|
1099
2283
|
onUsage: (payload) => {
|
|
1100
2284
|
summary.onUsage?.(payload);
|
|
1101
2285
|
renderer.render();
|
|
@@ -1283,13 +2467,21 @@ function formatCancelledError(error, color) {
|
|
|
1283
2467
|
}
|
|
1284
2468
|
function formatRunError(error, color) {
|
|
1285
2469
|
const label = color ? colors$1.red("error") : "error";
|
|
1286
|
-
const
|
|
2470
|
+
const fileFailures = error.failures.filter((failure) => "file" in failure);
|
|
2471
|
+
const groups = groupFailuresByRule(error.failures.filter((failure) => "job" in failure));
|
|
1287
2472
|
return [
|
|
1288
2473
|
`${label} ${error.message}`,
|
|
1289
2474
|
"",
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
2475
|
+
...fileFailures.length > 0 ? [
|
|
2476
|
+
`Failed Files ${fileFailures.length}`,
|
|
2477
|
+
"",
|
|
2478
|
+
...formatFileFailures(fileFailures, color)
|
|
2479
|
+
] : [],
|
|
2480
|
+
...groups.length > 0 ? [
|
|
2481
|
+
`Failed Rules ${groups.length}`,
|
|
2482
|
+
"",
|
|
2483
|
+
...formatFailureGroups(groups, color)
|
|
2484
|
+
] : []
|
|
1293
2485
|
].join("\n");
|
|
1294
2486
|
}
|
|
1295
2487
|
function formatFailureGroups(groups, color) {
|
|
@@ -1298,7 +2490,7 @@ function formatFailureGroups(groups, color) {
|
|
|
1298
2490
|
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
1299
2491
|
const targetLabel = group.length === 1 ? "target" : "targets";
|
|
1300
2492
|
lines.push(`${failLabel} ${ruleId} ${group.length} ${targetLabel}`);
|
|
1301
|
-
for (const failure of group
|
|
2493
|
+
for (const failure of group) {
|
|
1302
2494
|
lines.push(` ${formatTarget(failure)}`);
|
|
1303
2495
|
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
1304
2496
|
}
|
|
@@ -1306,6 +2498,16 @@ function formatFailureGroups(groups, color) {
|
|
|
1306
2498
|
}
|
|
1307
2499
|
return lines;
|
|
1308
2500
|
}
|
|
2501
|
+
function formatFileFailures(failures, color) {
|
|
2502
|
+
const lines = [];
|
|
2503
|
+
for (const failure of failures) {
|
|
2504
|
+
const failLabel = color ? colors$1.bgRed(colors$1.bold(" FAIL ")) : "FAIL";
|
|
2505
|
+
lines.push(`${failLabel} ${failure.file.path}`);
|
|
2506
|
+
lines.push(` [${failure.kind}] ${failure.message}`);
|
|
2507
|
+
lines.push("");
|
|
2508
|
+
}
|
|
2509
|
+
return lines;
|
|
2510
|
+
}
|
|
1309
2511
|
function formatTarget(failure) {
|
|
1310
2512
|
const target = failure.job.target.name ? `${failure.job.target.kind} ${failure.job.target.name}` : failure.job.target.kind;
|
|
1311
2513
|
return `${failure.job.inputPath} > ${target}`;
|
|
@@ -1741,6 +2943,9 @@ function createStatsCollector() {
|
|
|
1741
2943
|
counts,
|
|
1742
2944
|
durationMs: () => startedAt !== void 0 && endedAt !== void 0 ? endedAt - startedAt : void 0,
|
|
1743
2945
|
reporter: {
|
|
2946
|
+
onPrepareStart: (payload) => {
|
|
2947
|
+
startedAt = payload.startedAt;
|
|
2948
|
+
},
|
|
1744
2949
|
onRunEnd: (payload) => {
|
|
1745
2950
|
counts.cached = payload.execution.cached;
|
|
1746
2951
|
counts.cancelled = payload.execution.cancelled;
|
|
@@ -1748,9 +2953,6 @@ function createStatsCollector() {
|
|
|
1748
2953
|
counts.failed = payload.execution.failed;
|
|
1749
2954
|
counts.planned = payload.execution.planned;
|
|
1750
2955
|
endedAt = payload.endedAt;
|
|
1751
|
-
},
|
|
1752
|
-
onRunStart: (payload) => {
|
|
1753
|
-
startedAt = payload.startedAt;
|
|
1754
2956
|
}
|
|
1755
2957
|
}
|
|
1756
2958
|
};
|
|
@@ -1784,6 +2986,15 @@ function mergeProgressReporters(base, extra) {
|
|
|
1784
2986
|
onDiagnostic: (payload) => {
|
|
1785
2987
|
deliverBoth((reporter) => reporter.onDiagnostic?.(payload));
|
|
1786
2988
|
},
|
|
2989
|
+
onExecuteEnd: (payload) => {
|
|
2990
|
+
deliverBoth((reporter) => reporter.onExecuteEnd?.(payload));
|
|
2991
|
+
},
|
|
2992
|
+
onExecuteStart: (payload) => {
|
|
2993
|
+
deliverBoth((reporter) => reporter.onExecuteStart?.(payload));
|
|
2994
|
+
},
|
|
2995
|
+
onFileReady: (payload) => {
|
|
2996
|
+
deliverBoth((reporter) => reporter.onFileReady?.(payload));
|
|
2997
|
+
},
|
|
1787
2998
|
onJobEnd: (payload) => {
|
|
1788
2999
|
deliverBoth((reporter) => reporter.onJobEnd?.(payload));
|
|
1789
3000
|
},
|
|
@@ -1796,13 +3007,16 @@ function mergeProgressReporters(base, extra) {
|
|
|
1796
3007
|
onJobStart: (payload) => {
|
|
1797
3008
|
deliverBoth((reporter) => reporter.onJobStart?.(payload));
|
|
1798
3009
|
},
|
|
3010
|
+
onPrepareEnd: (payload) => {
|
|
3011
|
+
deliverBoth((reporter) => reporter.onPrepareEnd?.(payload));
|
|
3012
|
+
},
|
|
3013
|
+
onPrepareStart: (payload) => {
|
|
3014
|
+
deliverBoth((reporter) => reporter.onPrepareStart?.(payload));
|
|
3015
|
+
},
|
|
1799
3016
|
onRunEnd: (payload) => {
|
|
1800
3017
|
deliverBoth((reporter) => reporter.onRunEnd?.(payload));
|
|
1801
3018
|
if (firstFailure) throw firstFailure;
|
|
1802
3019
|
},
|
|
1803
|
-
onRunStart: (payload) => {
|
|
1804
|
-
deliverBoth((reporter) => reporter.onRunStart?.(payload));
|
|
1805
|
-
},
|
|
1806
3020
|
onUsage: (payload) => {
|
|
1807
3021
|
deliverBoth((reporter) => reporter.onUsage?.(payload));
|
|
1808
3022
|
}
|
|
@@ -1900,6 +3114,9 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1900
3114
|
const persistStats = async (runResult) => {
|
|
1901
3115
|
if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, runResult, io.cwd);
|
|
1902
3116
|
};
|
|
3117
|
+
const writeResult = (runResult) => {
|
|
3118
|
+
io.stdout.write(formatDiagnostics(options.format, runResult, { color: io.stdout.isTTY === true }));
|
|
3119
|
+
};
|
|
1903
3120
|
let result;
|
|
1904
3121
|
try {
|
|
1905
3122
|
result = await runAlint({
|
|
@@ -1919,11 +3136,13 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1919
3136
|
progress?.dispose();
|
|
1920
3137
|
if (error instanceof AlintRunError) {
|
|
1921
3138
|
await persistStats(error.result);
|
|
3139
|
+
writeResult(error.result);
|
|
1922
3140
|
io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
|
|
1923
3141
|
return 2;
|
|
1924
3142
|
}
|
|
1925
3143
|
if (error instanceof AlintRunCancelledError) {
|
|
1926
3144
|
await persistStats(error.result);
|
|
3145
|
+
writeResult(error.result);
|
|
1927
3146
|
io.stderr.write(formatCancelledError(error, io.stderr.isTTY === true));
|
|
1928
3147
|
return 2;
|
|
1929
3148
|
}
|
|
@@ -1932,7 +3151,7 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1932
3151
|
restoreProgressConsole?.();
|
|
1933
3152
|
progress?.dispose();
|
|
1934
3153
|
await persistStats(result);
|
|
1935
|
-
|
|
3154
|
+
writeResult(result);
|
|
1936
3155
|
return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
1937
3156
|
}
|
|
1938
3157
|
function shouldEnableProgress(options, io) {
|
|
@@ -2062,60 +3281,25 @@ const plugin = defineCommand({
|
|
|
2062
3281
|
//#region src/cli/commands/setup/interactive.ts
|
|
2063
3282
|
const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
|
|
2064
3283
|
const backValue = "__alint_back__";
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
function createDefaultModelCandidates(config, newProviderId, newModelIds) {
|
|
2085
|
-
const candidates = [];
|
|
2086
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2087
|
-
const newModelIdSet = new Set(newModelIds);
|
|
2088
|
-
const allModels = config.providers.flatMap((provider) => provider.models.map((model) => ({
|
|
2089
|
-
isCurrentDefault: (model.aliases ?? []).includes("default"),
|
|
2090
|
-
isNew: provider.id === newProviderId && newModelIdSet.has(model.id),
|
|
2091
|
-
model,
|
|
2092
|
-
provider,
|
|
2093
|
-
value: createDefaultModelCandidateValue(provider.id, model.id)
|
|
2094
|
-
})));
|
|
2095
|
-
const addCandidate = (candidate) => {
|
|
2096
|
-
if (candidate === void 0 || seen.has(candidate.value)) return;
|
|
2097
|
-
seen.add(candidate.value);
|
|
2098
|
-
candidates.push({
|
|
2099
|
-
isCurrentDefault: candidate.isCurrentDefault,
|
|
2100
|
-
isNew: candidate.isNew,
|
|
2101
|
-
label: `${candidate.provider.id} / ${candidate.model.id}`,
|
|
2102
|
-
modelId: candidate.model.id,
|
|
2103
|
-
providerId: candidate.provider.id,
|
|
2104
|
-
value: candidate.value
|
|
2105
|
-
});
|
|
2106
|
-
};
|
|
2107
|
-
addCandidate(allModels.find((candidate) => candidate.isCurrentDefault));
|
|
2108
|
-
for (const modelId of newModelIds) addCandidate(allModels.find((candidate) => candidate.provider.id === newProviderId && candidate.model.id === modelId));
|
|
2109
|
-
for (const candidate of allModels) addCandidate(candidate);
|
|
2110
|
-
return candidates;
|
|
2111
|
-
}
|
|
2112
|
-
function formatProbeModelsFailure(endpoint, error) {
|
|
2113
|
-
const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
|
|
2114
|
-
return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
|
|
2115
|
-
}
|
|
2116
|
-
function isBackInput(value) {
|
|
2117
|
-
return value.trim() === "..";
|
|
2118
|
-
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Handles setup-only scope/source navigation and persists a confirmed provider.
|
|
3286
|
+
*
|
|
3287
|
+
* Triggering workflow:
|
|
3288
|
+
*
|
|
3289
|
+
* {@link setup}
|
|
3290
|
+
* -> {@link runInteractiveSetup}
|
|
3291
|
+
* -> `provider-editor.confirmed`
|
|
3292
|
+
* -> {@link writeSetupConfig}
|
|
3293
|
+
*
|
|
3294
|
+
* Upstream:
|
|
3295
|
+
* - setup command action
|
|
3296
|
+
*
|
|
3297
|
+
* Downstream:
|
|
3298
|
+
* - {@link runProviderEditor} and {@link writeSetupConfig}
|
|
3299
|
+
*
|
|
3300
|
+
* The handler loads one selected scope once. Back returns to navigation,
|
|
3301
|
+
* cancellation writes nothing, and only a confirmed editor result is persisted.
|
|
3302
|
+
*/
|
|
2119
3303
|
async function runInteractiveSetup(io) {
|
|
2120
3304
|
if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
|
|
2121
3305
|
io.stderr.write(nonTtyMessage);
|
|
@@ -2127,11 +3311,11 @@ async function runInteractiveSetup(io) {
|
|
|
2127
3311
|
return 1;
|
|
2128
3312
|
};
|
|
2129
3313
|
prompts.intro("alint setup");
|
|
2130
|
-
|
|
3314
|
+
let scope = "global";
|
|
2131
3315
|
let step = "scope";
|
|
2132
3316
|
while (true) {
|
|
2133
3317
|
if (step === "scope") {
|
|
2134
|
-
const
|
|
3318
|
+
const selectedScope = await prompts.select({
|
|
2135
3319
|
message: "Where should alint write setup config?",
|
|
2136
3320
|
options: [{
|
|
2137
3321
|
label: "Global",
|
|
@@ -2141,263 +3325,50 @@ async function runInteractiveSetup(io) {
|
|
|
2141
3325
|
value: "local"
|
|
2142
3326
|
}]
|
|
2143
3327
|
});
|
|
2144
|
-
if (prompts.isCancel(
|
|
2145
|
-
|
|
3328
|
+
if (prompts.isCancel(selectedScope)) return cancelPrompt();
|
|
3329
|
+
scope = selectedScope;
|
|
2146
3330
|
step = "source";
|
|
2147
3331
|
continue;
|
|
2148
3332
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
step = "scope";
|
|
2160
|
-
continue;
|
|
2161
|
-
}
|
|
2162
|
-
draft.source = source;
|
|
2163
|
-
step = "endpoint";
|
|
2164
|
-
continue;
|
|
2165
|
-
}
|
|
2166
|
-
if (step === "endpoint") {
|
|
2167
|
-
const setupSource = findProviderSetupSource(draft.source ?? "custom");
|
|
2168
|
-
if (setupSource === void 0) return cancelPrompt();
|
|
2169
|
-
const endpoint = await promptEndpoint(prompts, setupSource);
|
|
2170
|
-
if (prompts.isCancel(endpoint)) return cancelPrompt();
|
|
2171
|
-
if (typeof endpoint !== "string") return cancelPrompt();
|
|
2172
|
-
if (isBackInput(endpoint)) {
|
|
2173
|
-
step = "source";
|
|
2174
|
-
continue;
|
|
2175
|
-
}
|
|
2176
|
-
draft.endpoint = endpoint;
|
|
2177
|
-
step = "providerId";
|
|
2178
|
-
continue;
|
|
2179
|
-
}
|
|
2180
|
-
if (step === "providerId") {
|
|
2181
|
-
const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
|
|
2182
|
-
const providerId = await prompts.text({
|
|
2183
|
-
initialValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
|
|
2184
|
-
message: "Provider id (type .. to go back)",
|
|
2185
|
-
validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
|
|
2186
|
-
});
|
|
2187
|
-
if (prompts.isCancel(providerId)) return cancelPrompt();
|
|
2188
|
-
if (typeof providerId !== "string") return cancelPrompt();
|
|
2189
|
-
if (isBackInput(providerId)) {
|
|
2190
|
-
step = "endpoint";
|
|
2191
|
-
continue;
|
|
2192
|
-
}
|
|
2193
|
-
draft.providerId = providerId;
|
|
2194
|
-
step = "headers";
|
|
2195
|
-
continue;
|
|
2196
|
-
}
|
|
2197
|
-
if (step === "headers") {
|
|
2198
|
-
const headerInput = await prompts.text({
|
|
2199
|
-
initialValue: draft.headerInput,
|
|
2200
|
-
message: "Headers (leave empty to skip; type .. to go back)",
|
|
2201
|
-
placeholder: "Authorization=Bearer token, X-Test=true",
|
|
2202
|
-
validate: (value) => {
|
|
2203
|
-
if (isBackInput(value ?? "")) return;
|
|
2204
|
-
try {
|
|
2205
|
-
parseHeaderList(splitHeaderInput(value ?? ""));
|
|
2206
|
-
return;
|
|
2207
|
-
} catch {
|
|
2208
|
-
return "Headers must be comma-separated Key=Value entries.";
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
});
|
|
2212
|
-
if (prompts.isCancel(headerInput)) return cancelPrompt();
|
|
2213
|
-
if (typeof headerInput !== "string") return cancelPrompt();
|
|
2214
|
-
if (isBackInput(headerInput)) {
|
|
2215
|
-
step = "providerId";
|
|
2216
|
-
continue;
|
|
2217
|
-
}
|
|
2218
|
-
draft.headerInput = headerInput;
|
|
2219
|
-
draft.headers = parseHeaderList(splitHeaderInput(headerInput));
|
|
2220
|
-
draft.discoveredModels = findProviderSetupSource(draft.source ?? "custom")?.probeModels === false ? [] : await probeModelsWithSpinner(prompts, draft.endpoint ?? "", draft.headers);
|
|
2221
|
-
step = "models";
|
|
2222
|
-
continue;
|
|
2223
|
-
}
|
|
2224
|
-
if (step === "models") {
|
|
2225
|
-
const selectedModels = await promptModels(prompts, draft.discoveredModels ?? []);
|
|
2226
|
-
if (prompts.isCancel(selectedModels)) return cancelPrompt();
|
|
2227
|
-
if (selectedModels === backValue) {
|
|
2228
|
-
step = "headers";
|
|
2229
|
-
continue;
|
|
2230
|
-
}
|
|
2231
|
-
if (!Array.isArray(selectedModels)) return cancelPrompt();
|
|
2232
|
-
draft.selectedModels = selectedModels;
|
|
2233
|
-
step = "defaultAlias";
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
if (step === "defaultAlias") {
|
|
2237
|
-
const defaultAliasAction = await prompts.select({
|
|
2238
|
-
message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
|
|
2239
|
-
options: withBackOption([
|
|
2240
|
-
{
|
|
2241
|
-
label: "Yes",
|
|
2242
|
-
value: "yes"
|
|
2243
|
-
},
|
|
2244
|
-
{
|
|
2245
|
-
label: "No",
|
|
2246
|
-
value: "no"
|
|
2247
|
-
},
|
|
2248
|
-
{
|
|
2249
|
-
label: "Select another",
|
|
2250
|
-
value: "selectAnother"
|
|
2251
|
-
}
|
|
2252
|
-
])
|
|
2253
|
-
});
|
|
2254
|
-
if (prompts.isCancel(defaultAliasAction)) return cancelPrompt();
|
|
2255
|
-
if (defaultAliasAction === backValue) {
|
|
2256
|
-
step = "models";
|
|
2257
|
-
continue;
|
|
2258
|
-
}
|
|
2259
|
-
if (defaultAliasAction === "selectAnother") {
|
|
2260
|
-
step = "selectDefaultModel";
|
|
2261
|
-
continue;
|
|
2262
|
-
}
|
|
2263
|
-
draft.defaultAliasTarget = defaultAliasAction === "yes" ? {
|
|
2264
|
-
modelId: draft.selectedModels?.[0] ?? "",
|
|
2265
|
-
providerId: (draft.providerId ?? "").trim()
|
|
2266
|
-
} : void 0;
|
|
2267
|
-
step = "confirm";
|
|
2268
|
-
continue;
|
|
2269
|
-
}
|
|
2270
|
-
if (step === "selectDefaultModel") {
|
|
2271
|
-
const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
|
|
2272
|
-
const draftProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? []);
|
|
2273
|
-
const candidates = createDefaultModelCandidates(mergeSetupConfigs(existingConfig, {
|
|
2274
|
-
providers: [draftProvider],
|
|
2275
|
-
version: 1
|
|
2276
|
-
}), draftProvider.id, draft.selectedModels ?? []);
|
|
2277
|
-
const defaultModel = await prompts.select({
|
|
2278
|
-
message: "Select default model",
|
|
2279
|
-
options: withBackOption(candidates.map((candidate) => ({
|
|
2280
|
-
hint: candidate.isCurrentDefault ? "current default" : candidate.isNew ? "new" : void 0,
|
|
2281
|
-
label: candidate.label,
|
|
2282
|
-
value: candidate.value
|
|
2283
|
-
})))
|
|
2284
|
-
});
|
|
2285
|
-
if (prompts.isCancel(defaultModel)) return cancelPrompt();
|
|
2286
|
-
if (defaultModel === backValue) {
|
|
2287
|
-
step = "defaultAlias";
|
|
2288
|
-
continue;
|
|
2289
|
-
}
|
|
2290
|
-
const candidate = candidates.find((item) => item.value === defaultModel);
|
|
2291
|
-
if (candidate === void 0) return cancelPrompt();
|
|
2292
|
-
draft.defaultAliasTarget = {
|
|
2293
|
-
modelId: candidate.modelId,
|
|
2294
|
-
providerId: candidate.providerId
|
|
2295
|
-
};
|
|
2296
|
-
step = "confirm";
|
|
3333
|
+
const selectedSource = await prompts.select({
|
|
3334
|
+
message: "Choose provider setup mode.",
|
|
3335
|
+
options: withBackOption(providerSetupSources.map(({ label, value }) => ({
|
|
3336
|
+
label,
|
|
3337
|
+
value
|
|
3338
|
+
})))
|
|
3339
|
+
});
|
|
3340
|
+
if (prompts.isCancel(selectedSource)) return cancelPrompt();
|
|
3341
|
+
if (selectedSource === backValue) {
|
|
3342
|
+
step = "scope";
|
|
2297
3343
|
continue;
|
|
2298
3344
|
}
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
label: "Yes",
|
|
2309
|
-
value: "yes"
|
|
2310
|
-
}, {
|
|
2311
|
-
label: "No",
|
|
2312
|
-
value: "no"
|
|
2313
|
-
}])
|
|
3345
|
+
const source = findProviderSetupSource(selectedSource);
|
|
3346
|
+
if (source === void 0) return cancelPrompt();
|
|
3347
|
+
const configPath = getConfigPath(io, scope);
|
|
3348
|
+
const existingConfig = await loadSetupConfig(configPath);
|
|
3349
|
+
const result = await runProviderEditor({
|
|
3350
|
+
config: existingConfig,
|
|
3351
|
+
io,
|
|
3352
|
+
mode: "create",
|
|
3353
|
+
source
|
|
2314
3354
|
});
|
|
2315
|
-
if (
|
|
2316
|
-
|
|
2317
|
-
step = "defaultAlias";
|
|
3355
|
+
if (result.status === "back") {
|
|
3356
|
+
step = "source";
|
|
2318
3357
|
continue;
|
|
2319
3358
|
}
|
|
2320
|
-
if (
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
providers: [nextProvider],
|
|
3359
|
+
if (result.status === "cancelled") return cancelPrompt();
|
|
3360
|
+
const merged = mergeSetupConfigs(existingConfig, {
|
|
3361
|
+
providers: [result.provider],
|
|
2324
3362
|
version: 1
|
|
2325
3363
|
});
|
|
2326
|
-
await writeSetupConfig(configPath,
|
|
3364
|
+
await writeSetupConfig(configPath, result.defaultAliasTarget === void 0 ? merged : applyDefaultAlias(merged, result.defaultAliasTarget));
|
|
2327
3365
|
prompts.outro(`Wrote ${configPath}`);
|
|
2328
3366
|
return 0;
|
|
2329
3367
|
}
|
|
2330
3368
|
}
|
|
2331
|
-
function withBackOption(options) {
|
|
2332
|
-
return [...options, {
|
|
2333
|
-
label: "Back",
|
|
2334
|
-
value: backValue
|
|
2335
|
-
}];
|
|
2336
|
-
}
|
|
2337
|
-
function createDefaultModelCandidateValue(providerId, modelId) {
|
|
2338
|
-
return `${providerId}\u0000${modelId}`;
|
|
2339
|
-
}
|
|
2340
|
-
function createProviderConfig(providerId, endpoint, headers, modelIds) {
|
|
2341
|
-
return {
|
|
2342
|
-
endpoint,
|
|
2343
|
-
headers,
|
|
2344
|
-
id: providerId,
|
|
2345
|
-
models: modelIds.map((modelId) => ({
|
|
2346
|
-
id: modelId,
|
|
2347
|
-
name: modelId
|
|
2348
|
-
})),
|
|
2349
|
-
type: "openai-compatible"
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
3369
|
function getConfigPath(io, scope) {
|
|
2353
3370
|
return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
|
|
2354
3371
|
}
|
|
2355
|
-
async function probeModelsWithSpinner(prompts, endpoint, headers) {
|
|
2356
|
-
const spinner = prompts.spinner();
|
|
2357
|
-
spinner.start("Probing models");
|
|
2358
|
-
try {
|
|
2359
|
-
const models = await probeModels(endpoint, headers ?? {});
|
|
2360
|
-
spinner.stop(models.length > 0 ? `Found ${models.length} models` : "No models discovered");
|
|
2361
|
-
return models;
|
|
2362
|
-
} catch (error) {
|
|
2363
|
-
spinner.stop(formatProbeModelsFailure(endpoint, error));
|
|
2364
|
-
return [];
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
async function promptEndpoint(prompts, source) {
|
|
2368
|
-
return prompts.text({
|
|
2369
|
-
initialValue: source.defaultEndpoint,
|
|
2370
|
-
message: "Provider endpoint (type .. to go back)",
|
|
2371
|
-
placeholder: source.defaultEndpoint ?? "https://example.test/v1",
|
|
2372
|
-
validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
|
|
2373
|
-
});
|
|
2374
|
-
}
|
|
2375
|
-
async function promptModels(prompts, discoveredModels) {
|
|
2376
|
-
if (discoveredModels.length > 0) {
|
|
2377
|
-
const selectedModels = await prompts.multiselect({
|
|
2378
|
-
message: "Select models",
|
|
2379
|
-
options: withBackOption(discoveredModels.map((model) => ({
|
|
2380
|
-
label: model,
|
|
2381
|
-
value: model
|
|
2382
|
-
}))),
|
|
2383
|
-
required: true
|
|
2384
|
-
});
|
|
2385
|
-
return Array.isArray(selectedModels) && selectedModels.includes(backValue) ? backValue : selectedModels;
|
|
2386
|
-
}
|
|
2387
|
-
const modelInput = await prompts.text({
|
|
2388
|
-
message: "Models",
|
|
2389
|
-
placeholder: "qwen:8b, qwen:32b; type .. to go back",
|
|
2390
|
-
validate: (value) => isBackInput(value ?? "") || splitModelInput(value ?? "").length > 0 ? void 0 : "At least one model is required."
|
|
2391
|
-
});
|
|
2392
|
-
if (prompts.isCancel(modelInput)) return modelInput;
|
|
2393
|
-
return isBackInput(modelInput) ? backValue : splitModelInput(modelInput);
|
|
2394
|
-
}
|
|
2395
|
-
function splitHeaderInput(value) {
|
|
2396
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2397
|
-
}
|
|
2398
|
-
function splitModelInput(value) {
|
|
2399
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2400
|
-
}
|
|
2401
3372
|
//#endregion
|
|
2402
3373
|
//#region src/cli/commands/setup/index.ts
|
|
2403
3374
|
const setup = defineCommand({
|