@alint-js/cli 0.0.37 → 0.1.0
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-Dm3rTcjL.mjs → cli-5-mYfGS0.mjs} +1355 -435
- 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.0
|
|
19
|
+
var version = "0.1.0";
|
|
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,127 +440,1173 @@ 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
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/cli/commands/config/models/ls.ts
|
|
475
|
+
const ls$1 = defineCommand({
|
|
476
|
+
async action(context) {
|
|
477
|
+
context.io.stdout.write(formatModelList(await loadMergedSetupConfig(context.io)));
|
|
478
|
+
return 0;
|
|
479
|
+
},
|
|
480
|
+
alias: ["ls"],
|
|
481
|
+
description: "List configured models",
|
|
482
|
+
name: "list"
|
|
483
|
+
});
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/cli/commands/config/probe.ts
|
|
486
|
+
function providerHeadersFromOptions(options) {
|
|
487
|
+
return parseHeaderList(toArray$2(options.providerHeader)) ?? {};
|
|
488
|
+
}
|
|
489
|
+
function toArray$2(value) {
|
|
490
|
+
if (value === void 0) return [];
|
|
491
|
+
return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
|
|
492
|
+
}
|
|
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
|
|
784
|
+
//#region src/cli/commands/config/models/models.ts
|
|
785
|
+
const models = defineCommand({
|
|
786
|
+
children: [
|
|
787
|
+
probe$1,
|
|
788
|
+
prune$1,
|
|
789
|
+
ls$1,
|
|
790
|
+
defineCommand({
|
|
791
|
+
async action(context, model) {
|
|
792
|
+
const candidates = findModels(await loadMergedSetupConfig(context.io), model);
|
|
793
|
+
if (candidates.length === 0) {
|
|
794
|
+
context.io.stderr.write(`unknown model "${model}".\n`);
|
|
795
|
+
return 2;
|
|
796
|
+
}
|
|
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]));
|
|
802
|
+
return 0;
|
|
803
|
+
},
|
|
804
|
+
arguments: "<model>",
|
|
805
|
+
description: "Show configured model",
|
|
806
|
+
name: "show"
|
|
807
|
+
}),
|
|
808
|
+
rm$1
|
|
809
|
+
],
|
|
810
|
+
description: "Manage configured models",
|
|
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"),
|
|
819
|
+
name: "models"
|
|
820
|
+
});
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/cli/commands/config/providers/ls.ts
|
|
823
|
+
const ls = defineCommand({
|
|
824
|
+
async action(context) {
|
|
825
|
+
context.io.stdout.write(formatProviderList(await loadMergedSetupConfig(context.io)));
|
|
826
|
+
return 0;
|
|
827
|
+
},
|
|
828
|
+
alias: ["ls"],
|
|
829
|
+
description: "List configured providers",
|
|
830
|
+
name: "list"
|
|
831
|
+
});
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/cli/commands/config/providers/probe.ts
|
|
834
|
+
const probe = defineCommand({
|
|
835
|
+
async action(context, options) {
|
|
836
|
+
if (!options.endpoint) {
|
|
837
|
+
context.io.stderr.write("config providers probe requires --endpoint.\n");
|
|
838
|
+
return 2;
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
|
|
842
|
+
context.io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
|
|
843
|
+
return 0;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
846
|
+
return 2;
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
description: "Probe provider reachability",
|
|
850
|
+
name: "probe",
|
|
851
|
+
options: [{
|
|
852
|
+
description: "Provider endpoint",
|
|
853
|
+
flags: "--endpoint <url>"
|
|
854
|
+
}, {
|
|
855
|
+
description: "Provider header",
|
|
856
|
+
flags: "--provider-header <Key=Value>"
|
|
857
|
+
}]
|
|
858
|
+
});
|
|
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;
|
|
400
1604
|
}
|
|
401
1605
|
function toArray$1(value) {
|
|
402
1606
|
if (value === void 0) return [];
|
|
403
|
-
return
|
|
1607
|
+
return Array.isArray(value) ? value : [value];
|
|
404
1608
|
}
|
|
405
1609
|
//#endregion
|
|
406
|
-
//#region src/cli/commands/config/models/models.ts
|
|
407
|
-
const models = defineCommand({
|
|
408
|
-
children: [
|
|
409
|
-
defineCommand({
|
|
410
|
-
async action(context, options) {
|
|
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
|
-
}),
|
|
436
|
-
ls$1,
|
|
437
|
-
defineCommand({
|
|
438
|
-
async action(context, model) {
|
|
439
|
-
const candidate = findModel(await loadMergedSetupConfig(context.io), model);
|
|
440
|
-
if (candidate === void 0) {
|
|
441
|
-
context.io.stderr.write(`unknown model "${model}".\n`);
|
|
442
|
-
return 2;
|
|
443
|
-
}
|
|
444
|
-
context.io.stdout.write(formatModelShow(candidate));
|
|
445
|
-
return 0;
|
|
446
|
-
},
|
|
447
|
-
arguments: "<model>",
|
|
448
|
-
description: "Show configured model",
|
|
449
|
-
name: "show"
|
|
450
|
-
})
|
|
451
|
-
],
|
|
452
|
-
description: "Manage configured models",
|
|
453
|
-
help: ["Inspect, list, and probe model entries from alint setup configuration.", "Model entries describe the provider/model ids alint can use for model-backed rules. Probe endpoints before saving them when you want to verify what model ids a provider exposes."].join("\n\n"),
|
|
454
|
-
name: "models"
|
|
455
|
-
});
|
|
456
|
-
//#endregion
|
|
457
|
-
//#region src/cli/commands/config/providers/ls.ts
|
|
458
|
-
const ls = defineCommand({
|
|
459
|
-
async action(context) {
|
|
460
|
-
context.io.stdout.write(formatProviderList(await loadMergedSetupConfig(context.io)));
|
|
461
|
-
return 0;
|
|
462
|
-
},
|
|
463
|
-
alias: ["ls"],
|
|
464
|
-
description: "List configured providers",
|
|
465
|
-
name: "list"
|
|
466
|
-
});
|
|
467
|
-
//#endregion
|
|
468
|
-
//#region src/cli/commands/config/providers/probe.ts
|
|
469
|
-
const probe = defineCommand({
|
|
470
|
-
async action(context, options) {
|
|
471
|
-
if (!options.endpoint) {
|
|
472
|
-
context.io.stderr.write("config providers probe requires --endpoint.\n");
|
|
473
|
-
return 2;
|
|
474
|
-
}
|
|
475
|
-
try {
|
|
476
|
-
const models = await probeModels(options.endpoint, providerHeadersFromOptions(options));
|
|
477
|
-
context.io.stdout.write(`endpoint: ${options.endpoint}\nmodels: ${models.length}\n`);
|
|
478
|
-
return 0;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? String(error)}\n`);
|
|
481
|
-
return 2;
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
description: "Probe provider reachability",
|
|
485
|
-
name: "probe",
|
|
486
|
-
options: [{
|
|
487
|
-
description: "Provider endpoint",
|
|
488
|
-
flags: "--endpoint <url>"
|
|
489
|
-
}, {
|
|
490
|
-
description: "Provider header",
|
|
491
|
-
flags: "--provider-header <Key=Value>"
|
|
492
|
-
}]
|
|
493
|
-
});
|
|
494
|
-
//#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
|
|
@@ -686,9 +1815,9 @@ function formatMiniBar(options) {
|
|
|
686
1815
|
if (index <= frontierIndex) return "█";
|
|
687
1816
|
return "░";
|
|
688
1817
|
}
|
|
689
|
-
if (index < phase
|
|
690
|
-
if (index === phase - 1) return "░";
|
|
1818
|
+
if (index < phase) return "█";
|
|
691
1819
|
if (index === phase) return "▓";
|
|
1820
|
+
if (index === phase + 1 && index <= frontierIndex) return "░";
|
|
692
1821
|
if (index <= frontierIndex) return "█";
|
|
693
1822
|
return "░";
|
|
694
1823
|
}).join("")}]`;
|
|
@@ -762,12 +1891,28 @@ function createSummaryProgressReporter(options) {
|
|
|
762
1891
|
state.cachedTokens = payload.usage.cached?.totalTokens ?? 0;
|
|
763
1892
|
state.runStartedAt = payload.startedAt ?? state.runStartedAt;
|
|
764
1893
|
},
|
|
1894
|
+
/**
|
|
1895
|
+
* Resets summary state when the core run starts.
|
|
1896
|
+
*
|
|
1897
|
+
* Triggering workflow:
|
|
1898
|
+
*
|
|
1899
|
+
* `@alint-js/core run`
|
|
1900
|
+
* -> `ProgressReporter.onRunStart`
|
|
1901
|
+
* -> `createRenderingProgressReporter(...).onRunStart`
|
|
1902
|
+
* -> `SummaryProgressReporter.onRunStart` (this handler)
|
|
1903
|
+
*
|
|
1904
|
+
* Upstream:
|
|
1905
|
+
* - `createRenderingProgressReporter(...).onRunStart`
|
|
1906
|
+
*
|
|
1907
|
+
* Downstream:
|
|
1908
|
+
* - Resets `state.animationTick` and run data consumed by {@link createRows}.
|
|
1909
|
+
*/
|
|
765
1910
|
onRunStart: ({ jobsTotal, startedAt }) => {
|
|
766
1911
|
state.diagnostics = [];
|
|
767
1912
|
state.execution = createCounts(jobsTotal);
|
|
768
1913
|
state.jobs.clear();
|
|
769
1914
|
state.runStartedAt = startedAt ?? now();
|
|
770
|
-
state.
|
|
1915
|
+
state.animationTick = 0;
|
|
771
1916
|
state.cachedTokens = 0;
|
|
772
1917
|
state.jobTokens.clear();
|
|
773
1918
|
state.totalTokens = 0;
|
|
@@ -779,8 +1924,23 @@ function createSummaryProgressReporter(options) {
|
|
|
779
1924
|
else 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
|
}
|
|
@@ -823,12 +1983,12 @@ function createCounts(planned = 0) {
|
|
|
823
1983
|
}
|
|
824
1984
|
function createInitialState() {
|
|
825
1985
|
return {
|
|
1986
|
+
animationTick: 0,
|
|
826
1987
|
cachedTokens: 0,
|
|
827
1988
|
diagnostics: [],
|
|
828
1989
|
execution: createCounts(),
|
|
829
1990
|
jobs: /* @__PURE__ */ new Map(),
|
|
830
1991
|
jobTokens: /* @__PURE__ */ new Map(),
|
|
831
|
-
spinnerIndex: 0,
|
|
832
1992
|
totalTokens: 0
|
|
833
1993
|
};
|
|
834
1994
|
}
|
|
@@ -937,7 +2097,7 @@ function formatDuration(ms) {
|
|
|
937
2097
|
function formatFooters(state, options, now) {
|
|
938
2098
|
const terminal = terminalCount(state.execution);
|
|
939
2099
|
const elapsed = state.runStartedAt === void 0 ? 0 : now - state.runStartedAt;
|
|
940
|
-
const progressBar = formatMiniBarSegment(terminal, state.execution.planned, state.
|
|
2100
|
+
const progressBar = formatMiniBarSegment(terminal, state.execution.planned, state.animationTick, options);
|
|
941
2101
|
const eta = estimateEta(elapsed, terminal, state.execution.planned);
|
|
942
2102
|
const projectedTokens = terminal > 0 && state.execution.planned > 0 ? Math.ceil((state.totalTokens + state.cachedTokens) * state.execution.planned / terminal) : void 0;
|
|
943
2103
|
return [
|
|
@@ -956,15 +2116,18 @@ function formatMiniBarSegment(completed, planned, tick, options) {
|
|
|
956
2116
|
})}`;
|
|
957
2117
|
}
|
|
958
2118
|
function formatRuleRow(group, state, options) {
|
|
959
|
-
const spinner = options.spinnerFrames
|
|
2119
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
960
2120
|
const terminal = group.completed + group.cached + group.failed + group.cancelled + group.skipped;
|
|
961
2121
|
const percent = group.planned > 0 ? Math.floor(terminal / group.planned * 100) : 0;
|
|
962
|
-
const bar = formatMiniBarSegment(terminal, group.planned, state.
|
|
2122
|
+
const bar = formatMiniBarSegment(terminal, group.planned, state.animationTick, options);
|
|
963
2123
|
const running = group.jobs.filter((job) => job.state === "running").length;
|
|
964
2124
|
const failed = group.failed > 0 ? ` ${group.failed} failed` : "";
|
|
965
2125
|
const eta = estimateEta(group.terminalDurationMs, terminal, group.planned);
|
|
966
2126
|
return fitRow(`${spinner} ${group.ruleId} ${terminal}/${group.planned} ${percent}%${bar} ${eta} ${running} running${failed}`, options.columns);
|
|
967
2127
|
}
|
|
2128
|
+
function formatSpinnerFrame(animationTick, spinnerFrames) {
|
|
2129
|
+
return spinnerFrames[animationTick % Math.max(spinnerFrames.length, 1)] ?? "";
|
|
2130
|
+
}
|
|
968
2131
|
function formatTargetRow(jobState, options, now, last) {
|
|
969
2132
|
const inputPath = options.cwd ? relative$1(options.cwd, jobState.job.inputPath) || "." : jobState.job.inputPath;
|
|
970
2133
|
const target = jobState.job.target.name ? `${jobState.job.target.kind} ${jobState.job.target.name}` : jobState.job.target.kind;
|
|
@@ -973,7 +2136,7 @@ function formatTargetRow(jobState, options, now, last) {
|
|
|
973
2136
|
}
|
|
974
2137
|
function styleRow(row, state, warnCount, errorCount, options) {
|
|
975
2138
|
let styledRow = row;
|
|
976
|
-
const spinner = options.spinnerFrames
|
|
2139
|
+
const spinner = formatSpinnerFrame(state.animationTick, options.spinnerFrames);
|
|
977
2140
|
if (spinner) styledRow = styledRow.replace(spinner, colors$2.cyan(spinner));
|
|
978
2141
|
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
2142
|
}
|
|
@@ -1900,6 +3063,9 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1900
3063
|
const persistStats = async (runResult) => {
|
|
1901
3064
|
if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, runResult, io.cwd);
|
|
1902
3065
|
};
|
|
3066
|
+
const writeResult = (runResult) => {
|
|
3067
|
+
io.stdout.write(formatDiagnostics(options.format, runResult, { color: io.stdout.isTTY === true }));
|
|
3068
|
+
};
|
|
1903
3069
|
let result;
|
|
1904
3070
|
try {
|
|
1905
3071
|
result = await runAlint({
|
|
@@ -1919,11 +3085,13 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1919
3085
|
progress?.dispose();
|
|
1920
3086
|
if (error instanceof AlintRunError) {
|
|
1921
3087
|
await persistStats(error.result);
|
|
3088
|
+
writeResult(error.result);
|
|
1922
3089
|
io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
|
|
1923
3090
|
return 2;
|
|
1924
3091
|
}
|
|
1925
3092
|
if (error instanceof AlintRunCancelledError) {
|
|
1926
3093
|
await persistStats(error.result);
|
|
3094
|
+
writeResult(error.result);
|
|
1927
3095
|
io.stderr.write(formatCancelledError(error, io.stderr.isTTY === true));
|
|
1928
3096
|
return 2;
|
|
1929
3097
|
}
|
|
@@ -1932,7 +3100,7 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
|
|
|
1932
3100
|
restoreProgressConsole?.();
|
|
1933
3101
|
progress?.dispose();
|
|
1934
3102
|
await persistStats(result);
|
|
1935
|
-
|
|
3103
|
+
writeResult(result);
|
|
1936
3104
|
return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
1937
3105
|
}
|
|
1938
3106
|
function shouldEnableProgress(options, io) {
|
|
@@ -2062,60 +3230,25 @@ const plugin = defineCommand({
|
|
|
2062
3230
|
//#region src/cli/commands/setup/interactive.ts
|
|
2063
3231
|
const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
|
|
2064
3232
|
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
|
-
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Handles setup-only scope/source navigation and persists a confirmed provider.
|
|
3235
|
+
*
|
|
3236
|
+
* Triggering workflow:
|
|
3237
|
+
*
|
|
3238
|
+
* {@link setup}
|
|
3239
|
+
* -> {@link runInteractiveSetup}
|
|
3240
|
+
* -> `provider-editor.confirmed`
|
|
3241
|
+
* -> {@link writeSetupConfig}
|
|
3242
|
+
*
|
|
3243
|
+
* Upstream:
|
|
3244
|
+
* - setup command action
|
|
3245
|
+
*
|
|
3246
|
+
* Downstream:
|
|
3247
|
+
* - {@link runProviderEditor} and {@link writeSetupConfig}
|
|
3248
|
+
*
|
|
3249
|
+
* The handler loads one selected scope once. Back returns to navigation,
|
|
3250
|
+
* cancellation writes nothing, and only a confirmed editor result is persisted.
|
|
3251
|
+
*/
|
|
2119
3252
|
async function runInteractiveSetup(io) {
|
|
2120
3253
|
if (io.stdin?.isTTY !== true || io.stdout.isTTY !== true) {
|
|
2121
3254
|
io.stderr.write(nonTtyMessage);
|
|
@@ -2127,11 +3260,11 @@ async function runInteractiveSetup(io) {
|
|
|
2127
3260
|
return 1;
|
|
2128
3261
|
};
|
|
2129
3262
|
prompts.intro("alint setup");
|
|
2130
|
-
|
|
3263
|
+
let scope = "global";
|
|
2131
3264
|
let step = "scope";
|
|
2132
3265
|
while (true) {
|
|
2133
3266
|
if (step === "scope") {
|
|
2134
|
-
const
|
|
3267
|
+
const selectedScope = await prompts.select({
|
|
2135
3268
|
message: "Where should alint write setup config?",
|
|
2136
3269
|
options: [{
|
|
2137
3270
|
label: "Global",
|
|
@@ -2141,263 +3274,50 @@ async function runInteractiveSetup(io) {
|
|
|
2141
3274
|
value: "local"
|
|
2142
3275
|
}]
|
|
2143
3276
|
});
|
|
2144
|
-
if (prompts.isCancel(
|
|
2145
|
-
|
|
3277
|
+
if (prompts.isCancel(selectedScope)) return cancelPrompt();
|
|
3278
|
+
scope = selectedScope;
|
|
2146
3279
|
step = "source";
|
|
2147
3280
|
continue;
|
|
2148
3281
|
}
|
|
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";
|
|
3282
|
+
const selectedSource = await prompts.select({
|
|
3283
|
+
message: "Choose provider setup mode.",
|
|
3284
|
+
options: withBackOption(providerSetupSources.map(({ label, value }) => ({
|
|
3285
|
+
label,
|
|
3286
|
+
value
|
|
3287
|
+
})))
|
|
3288
|
+
});
|
|
3289
|
+
if (prompts.isCancel(selectedSource)) return cancelPrompt();
|
|
3290
|
+
if (selectedSource === backValue) {
|
|
3291
|
+
step = "scope";
|
|
2297
3292
|
continue;
|
|
2298
3293
|
}
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
label: "Yes",
|
|
2309
|
-
value: "yes"
|
|
2310
|
-
}, {
|
|
2311
|
-
label: "No",
|
|
2312
|
-
value: "no"
|
|
2313
|
-
}])
|
|
3294
|
+
const source = findProviderSetupSource(selectedSource);
|
|
3295
|
+
if (source === void 0) return cancelPrompt();
|
|
3296
|
+
const configPath = getConfigPath(io, scope);
|
|
3297
|
+
const existingConfig = await loadSetupConfig(configPath);
|
|
3298
|
+
const result = await runProviderEditor({
|
|
3299
|
+
config: existingConfig,
|
|
3300
|
+
io,
|
|
3301
|
+
mode: "create",
|
|
3302
|
+
source
|
|
2314
3303
|
});
|
|
2315
|
-
if (
|
|
2316
|
-
|
|
2317
|
-
step = "defaultAlias";
|
|
3304
|
+
if (result.status === "back") {
|
|
3305
|
+
step = "source";
|
|
2318
3306
|
continue;
|
|
2319
3307
|
}
|
|
2320
|
-
if (
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
providers: [nextProvider],
|
|
3308
|
+
if (result.status === "cancelled") return cancelPrompt();
|
|
3309
|
+
const merged = mergeSetupConfigs(existingConfig, {
|
|
3310
|
+
providers: [result.provider],
|
|
2324
3311
|
version: 1
|
|
2325
3312
|
});
|
|
2326
|
-
await writeSetupConfig(configPath,
|
|
3313
|
+
await writeSetupConfig(configPath, result.defaultAliasTarget === void 0 ? merged : applyDefaultAlias(merged, result.defaultAliasTarget));
|
|
2327
3314
|
prompts.outro(`Wrote ${configPath}`);
|
|
2328
3315
|
return 0;
|
|
2329
3316
|
}
|
|
2330
3317
|
}
|
|
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
3318
|
function getConfigPath(io, scope) {
|
|
2353
3319
|
return scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
|
|
2354
3320
|
}
|
|
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
3321
|
//#endregion
|
|
2402
3322
|
//#region src/cli/commands/setup/index.ts
|
|
2403
3323
|
const setup = defineCommand({
|