@alint-js/cli 0.0.21 → 0.0.23

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 CHANGED
@@ -274,6 +274,41 @@ Use `alint plugin install` after adding or changing plugin sources.
274
274
  - **Package:** `@scope/alint-plugin-example1@1.2.3` downloads that exact version into `.alint/plugins/store`, verifies its integrity, and loads its root export.
275
275
  - **Local:** `./plugins/my-plugin` or an absolute directory loads the current package root export in place. `alint` checks that the package and entry exist and that the entry stays inside the package directory. It does not build the plugin or install the plugin's dependencies.
276
276
 
277
+ #### Declarative local rule plugins
278
+
279
+ For simple prompt-backed rules, a local plugin directory can omit `package.json` and contain one or more `rule.alint.*` files:
280
+
281
+ ```toml
282
+ # rules/architecture/semantic/rule.alint.toml
283
+ name = "semantic-boundary"
284
+ builtInAgent = "basic-structured"
285
+ instruction = """
286
+ Find semantic boundary problems in the reviewed source.
287
+ Report concrete findings with line numbers and short suggestions.
288
+ """
289
+ includeFiles = [ "src/**/*.py" ]
290
+ excludeFiles = [
291
+ "**/*_test.py",
292
+ "**/vendor/**",
293
+ ]
294
+ ```
295
+
296
+ Reference the directory through the same static `plugins` table:
297
+
298
+ ```toml
299
+ [[config.group]]
300
+ files = [ "src/**/*.py" ]
301
+ language = "text/plain"
302
+
303
+ [config.group.plugins]
304
+ arch = "./rules/architecture"
305
+
306
+ [config.group.rules]
307
+ "arch/semantic-boundary" = "warn"
308
+ ```
309
+
310
+ `name` becomes the local rule id. `builtInAgent` can be `basic-structured` for a prompt-only structured-output rule or `basic-coding-agent` for a small built-in agent with filesystem tools. `includeFiles` and `excludeFiles` define where diagnostics may be reported; they do not limit what `basic-coding-agent` can inspect.
311
+
277
312
  Run the install command again after changing a source string, moving a local directory, or changing its symlink target. Changes inside the same local directory are loaded by the next CLI process without reinstalling.
278
313
 
279
314
  Local plugins execute as trusted Node.js code. Directory containment checks validate the installed source; they are not a sandbox.
@@ -386,11 +421,7 @@ import { defineRule } from '@alint-js/core'
386
421
 
387
422
  export const checkFunctionRule = defineRule({
388
423
  create: ctx => ({
389
- async onTarget(target) {
390
- if (target.kind !== 'function') {
391
- return
392
- }
393
-
424
+ async onTargetFunction(target) {
394
425
  const model = await ctx.model({ capabilities: ['tool-call'], size: 'small' })
395
426
 
396
427
  ctx.report({
@@ -432,7 +463,7 @@ Rule authors can opt out of caching when a rule depends on external state:
432
463
  defineRule({
433
464
  cache: false,
434
465
  create: ctx => ({
435
- onTarget(target) {
466
+ onTargetFile(target) {
436
467
  // Always reruns.
437
468
  },
438
469
  }),
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-Dey9TSc8.mjs";
2
+ import { t as executeCli } from "../cli-CwuIPM7m.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -1,12 +1,12 @@
1
1
  import process from "node:process";
2
2
  import { inspect } from "node:util";
3
3
  import { cac } from "cac";
4
+ import { appendFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
4
5
  import { getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, writeSetupConfig } from "@alint-js/config";
5
- import { AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForFile, runAlint } from "@alint-js/core";
6
+ import { AlintRunError, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
6
7
  import { isAbsolute, join, relative, resolve } from "pathe";
7
8
  import { getBorderCharacters, table } from "table";
8
9
  import { errorMessageFrom } from "@moeru/std/error";
9
- import { appendFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
10
10
  import c, { createColors } from "tinyrainbow";
11
11
  import cliSpinners from "cli-spinners";
12
12
  import { relative as relative$1 } from "node:path";
@@ -14,7 +14,7 @@ import Gitignore from "gitignore-fs";
14
14
  import { Minimatch, minimatch } from "minimatch";
15
15
  import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
16
16
  //#region package.json
17
- var version = "0.0.21";
17
+ var version = "0.0.23";
18
18
  //#endregion
19
19
  //#region src/cli/commands/command.ts
20
20
  function defineCommand(node) {
@@ -200,6 +200,7 @@ function shouldSkipOptionValue(arg) {
200
200
  "--rule-concurrency",
201
201
  "--since",
202
202
  "--timeout-ms",
203
+ "-c",
203
204
  "-f",
204
205
  "-l"
205
206
  ].includes(arg);
@@ -207,15 +208,17 @@ function shouldSkipOptionValue(arg) {
207
208
  //#endregion
208
209
  //#region src/cli/commands/config/inspect.ts
209
210
  const inspect$2 = defineCommand({
210
- action: (context, file, options) => runConfigInspectCommand(file, options.config, context.io),
211
- arguments: "<file>",
212
- description: "Inspect resolved config for a file",
211
+ action: (context, path, options) => runConfigInspectCommand(path, options.config, context.io),
212
+ arguments: "<path>",
213
+ description: "Inspect resolved config for a file or directory",
213
214
  name: "inspect"
214
215
  });
215
- async function runConfigInspectCommand(file, configPath, io) {
216
+ async function runConfigInspectCommand(path, configPath, io) {
216
217
  const config = await loadAlintConfig(io.cwd, configPath);
217
- const result = resolveConfigForFile(resolve(io.cwd, file), config, { cwd: io.cwd });
218
- io.stdout.write(`file: ${file}\n`);
218
+ const targetPath = resolve(io.cwd, path);
219
+ const isDirectory = await stat(targetPath).then((stats) => stats.isDirectory()).catch(() => false);
220
+ const result = isDirectory ? resolveConfigForDirectory(targetPath, config, { cwd: io.cwd }) : resolveConfigForFile(targetPath, config, { cwd: io.cwd });
221
+ io.stdout.write(`${isDirectory ? "directory" : "file"}: ${path}\n`);
219
222
  io.stdout.write(`ignored: ${result.ignored ? "yes" : "no"}\n`);
220
223
  io.stdout.write("matched:\n");
221
224
  for (const item of result.matched) io.stdout.write(` - ${item.name ?? "<anonymous>"}\n`);
@@ -258,7 +261,8 @@ function createProviderId(endpoint, existingIds) {
258
261
  let base = "provider";
259
262
  try {
260
263
  const url = new URL(endpoint);
261
- base = findProviderSetupSourceByEndpoint(url)?.defaultProviderId ?? (url.hostname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider");
264
+ const source = findProviderSetupSourceByEndpoint(url);
265
+ base = normalizeProviderIdBase(source?.defaultProviderId ?? source?.label ?? url.hostname);
262
266
  } catch {
263
267
  base = "provider";
264
268
  }
@@ -362,6 +366,9 @@ function formatTable(rows) {
362
366
  drawHorizontalLine: () => false
363
367
  });
364
368
  }
369
+ function normalizeProviderIdBase(value) {
370
+ return value.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "provider";
371
+ }
365
372
  //#endregion
366
373
  //#region src/cli/commands/config/setup-config.ts
367
374
  async function loadMergedSetupConfig(io) {
@@ -541,8 +548,9 @@ function formatJson(result) {
541
548
  const colors$2 = createColors({ force: true });
542
549
  function formatStylish(input, options = {}) {
543
550
  const diagnostics = Array.isArray(input) ? input : input.diagnostics;
544
- const totalTokens = Array.isArray(input) ? void 0 : input.usage.totalTokens;
545
- if (diagnostics.length === 0) return "";
551
+ const result = Array.isArray(input) ? void 0 : input;
552
+ const style = createStyle(options.color === true);
553
+ if (diagnostics.length === 0) return result?.execution?.cached ? `${formatSummary(diagnostics, result, style)}\n` : "";
546
554
  const diagnosticsByFile = /* @__PURE__ */ new Map();
547
555
  for (const diagnostic of diagnostics) {
548
556
  const fileDiagnostics = diagnosticsByFile.get(diagnostic.filePath);
@@ -553,7 +561,6 @@ function formatStylish(input, options = {}) {
553
561
  diagnosticsByFile.set(diagnostic.filePath, [diagnostic]);
554
562
  }
555
563
  const lines = [];
556
- const style = createStyle(options.color === true);
557
564
  for (const [filePath, fileDiagnostics] of diagnosticsByFile) {
558
565
  lines.push(style.file(filePath));
559
566
  for (const diagnostic of fileDiagnostics) {
@@ -564,9 +571,12 @@ function formatStylish(input, options = {}) {
564
571
  }
565
572
  lines.push("");
566
573
  }
567
- lines.push("", formatSummary(diagnostics, totalTokens, style));
574
+ lines.push("", formatSummary(diagnostics, result, style));
568
575
  return `${lines.join("\n")}\n`;
569
576
  }
577
+ function countCachedDiagnostics(diagnostics, severity) {
578
+ return diagnostics.filter((diagnostic) => diagnostic.severity === severity && diagnostic.cached === true).length;
579
+ }
570
580
  function countDiagnostics$2(diagnostics, severity) {
571
581
  return diagnostics.filter((diagnostic) => diagnostic.severity === severity).length;
572
582
  }
@@ -588,13 +598,32 @@ function createStyle(color) {
588
598
  warning: colors$2.yellow
589
599
  };
590
600
  }
591
- function formatSummary(diagnostics, totalTokens, style) {
601
+ function formatDiagnosticCount(total, cached, label) {
602
+ return `${total} ${label}${cached > 0 ? ` (${cached} cached)` : ""}`;
603
+ }
604
+ function formatExecutionSummary(execution) {
605
+ if (!execution || execution.cached === 0) return;
606
+ return `${execution.cached}/${execution.planned} cached`;
607
+ }
608
+ function formatSummary(diagnostics, result, style) {
592
609
  const warnCount = countDiagnostics$2(diagnostics, "warn");
593
610
  const errorCount = countDiagnostics$2(diagnostics, "error");
594
- const tokens = totalTokens === void 0 ? void 0 : `${totalTokens.toLocaleString("en-US")} tokens`;
595
- const problemSummary = [style.warning(`${warnCount} warn`), style.error(`${errorCount} error`)].join(" / ");
611
+ const cachedWarnCount = countCachedDiagnostics(diagnostics, "warn");
612
+ const cachedErrorCount = countCachedDiagnostics(diagnostics, "error");
613
+ const tokens = result === void 0 ? void 0 : formatTokenSummary(result);
614
+ const problemSummary = [style.warning(formatDiagnosticCount(warnCount, cachedWarnCount, "warn")), style.error(formatDiagnosticCount(errorCount, cachedErrorCount, "error"))].join(" / ");
596
615
  if (tokens === void 0) return problemSummary;
597
- return `${problemSummary} | ${style.summaryToken(tokens)}`;
616
+ const execution = formatExecutionSummary(result?.execution);
617
+ return [
618
+ problemSummary,
619
+ style.summaryToken(tokens),
620
+ execution
621
+ ].filter((segment) => segment !== void 0).join(" | ");
622
+ }
623
+ function formatTokenSummary(result) {
624
+ const liveTokens = result.usage.totalTokens.toLocaleString("en-US");
625
+ const cachedTokens = result.usage.cached?.totalTokens ?? 0;
626
+ return `${liveTokens} tokens${cachedTokens > 0 ? ` (${cachedTokens.toLocaleString("en-US")} cached)` : ""}`;
598
627
  }
599
628
  function identity(value) {
600
629
  return value;
@@ -635,7 +664,7 @@ function countDiagnostics$1(diagnostics, severity) {
635
664
  //#region src/cli/reporters/progress/summary.ts
636
665
  const colors$1 = createColors({ force: true });
637
666
  function createSummaryProgressReporter(options) {
638
- const now = () => options.clock?.() ?? Date.now();
667
+ const now = Date.now;
639
668
  const state = {
640
669
  cached: 0,
641
670
  completed: 0,
@@ -983,10 +1012,10 @@ var NoFilesFoundError = class extends Error {
983
1012
  this.pattern = pattern;
984
1013
  }
985
1014
  };
986
- async function findFiles(options) {
1015
+ async function findLintTargets(options) {
987
1016
  const { config, cwd, errorOnUnmatchedPattern = true, globInputPaths = true, inputs } = options;
988
1017
  const gitignore = shouldFilterGitignoredFiles(config) ? new Gitignore() : void 0;
989
- const candidates = await resolveInputFiles({
1018
+ const targets = await resolveInputFiles({
990
1019
  config,
991
1020
  cwd,
992
1021
  errorOnUnmatchedPattern,
@@ -994,13 +1023,16 @@ async function findFiles(options) {
994
1023
  globInputPaths,
995
1024
  inputs: inputs.length > 0 ? inputs : ["."]
996
1025
  });
997
- if (!gitignore || candidates.length === 0) return candidates;
1026
+ if (!gitignore || targets.files.length === 0) return targets;
998
1027
  const lintFiles = [];
999
- for (const file of candidates) {
1028
+ for (const file of targets.files) {
1000
1029
  if (await gitignore.ignores(resolve(cwd, file))) continue;
1001
1030
  lintFiles.push(file);
1002
1031
  }
1003
- return lintFiles;
1032
+ return {
1033
+ directories: targets.directories,
1034
+ files: lintFiles
1035
+ };
1004
1036
  }
1005
1037
  function collectGlobalIgnorePatterns(config) {
1006
1038
  return normalizeConfig(config).flatMap((item) => isGlobalIgnoreItem(item) ? [...item.ignores] : []);
@@ -1041,6 +1073,7 @@ async function resolveInputFiles(options) {
1041
1073
  const hasFilePatterns = hasDiscoveryFilePatterns(options.config);
1042
1074
  const ignoredPatterns = collectGlobalIgnorePatterns(options.config);
1043
1075
  const candidates = [];
1076
+ const directories = [];
1044
1077
  for (const input of options.inputs) {
1045
1078
  const path = resolve(options.cwd, input);
1046
1079
  const stats = await statPath(path);
@@ -1049,12 +1082,13 @@ async function resolveInputFiles(options) {
1049
1082
  continue;
1050
1083
  }
1051
1084
  if (stats?.isDirectory()) {
1052
- if (!hasFilePatterns) continue;
1053
1085
  if (await shouldPruneDirectory(path, {
1054
1086
  cwd: options.cwd,
1055
1087
  gitignore: options.gitignore,
1056
1088
  ignoredPatterns
1057
1089
  })) continue;
1090
+ directories.push(input);
1091
+ if (!hasFilePatterns) continue;
1058
1092
  const directoryFiles = (await walkFiles(path, {
1059
1093
  cwd: options.cwd,
1060
1094
  gitignore: options.gitignore,
@@ -1081,7 +1115,10 @@ async function resolveInputFiles(options) {
1081
1115
  }
1082
1116
  if (options.errorOnUnmatchedPattern) throw new NoFilesFoundError(input, { globInputPaths: options.globInputPaths });
1083
1117
  }
1084
- return [...new Set(candidates)];
1118
+ return {
1119
+ directories: [...new Set(directories)],
1120
+ files: [...new Set(candidates)]
1121
+ };
1085
1122
  }
1086
1123
  async function searchGlob(options) {
1087
1124
  const root = resolve(options.cwd, getGlobParent(options.pattern));
@@ -1251,11 +1288,10 @@ function parseSince(time, now) {
1251
1288
  const DEFAULT_RETENTION_MONTHS = 12;
1252
1289
  const STATS_FILE_PATTERN = /^stats-(\d{4})-(\d{2})\.jsonl$/u;
1253
1290
  function createJsonlStatsStore(options) {
1254
- const now = options.clock ?? Date.now;
1255
1291
  const retentionMonths = options.retentionMonths ?? DEFAULT_RETENTION_MONTHS;
1256
1292
  return {
1257
- query: (filter) => query(options.dir, filter ?? {}, now),
1258
- record: (input) => record(options.dir, input, now(), retentionMonths)
1293
+ query: (filter) => query(options.dir, filter ?? {}, Date.now),
1294
+ record: (input) => record(options.dir, input, Date.now(), retentionMonths)
1259
1295
  };
1260
1296
  }
1261
1297
  function accumulate(rows, key, inTok, outTok, totalTok, countRun) {
@@ -1534,9 +1570,9 @@ function isNodeError(error) {
1534
1570
  async function runLintCommand(files, options, io, interceptConsoleOutput) {
1535
1571
  if (options.config) await assertConfigExists(io.cwd, options.config);
1536
1572
  const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
1537
- let lintFiles;
1573
+ let lintTargets;
1538
1574
  try {
1539
- lintFiles = await findFiles({
1575
+ lintTargets = await findLintTargets({
1540
1576
  config,
1541
1577
  cwd: io.cwd,
1542
1578
  errorOnUnmatchedPattern: true,
@@ -1566,7 +1602,8 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1566
1602
  result = await runAlint({
1567
1603
  config,
1568
1604
  cwd: io.cwd,
1569
- files: lintFiles,
1605
+ directories: lintTargets.directories,
1606
+ files: lintTargets.files,
1570
1607
  modelOverride: options.model,
1571
1608
  outputLanguage: options.outputLanguage,
1572
1609
  progress: mergeProgressReporters(progress?.reporter, statsCollector?.reporter),
@@ -1586,7 +1623,7 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
1586
1623
  progress?.dispose();
1587
1624
  if (statsTarget && statsCollector) await writeRunStats(statsTarget, statsCollector, result, io.cwd);
1588
1625
  io.stdout.write(formatDiagnostics(options.format, result, { color: io.stdout.isTTY === true }));
1589
- return result.diagnostics.length > 0 ? 1 : 0;
1626
+ return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
1590
1627
  }
1591
1628
  function shouldEnableProgress(options, io) {
1592
1629
  if (options.progress !== void 0) return options.progress;
@@ -1641,7 +1678,7 @@ async function inspectOutputFile(io, file, options) {
1641
1678
  io.stderr.write(`${errorMessageFrom$1(error)}\n`);
1642
1679
  return 2;
1643
1680
  }
1644
- return parsed.diagnostics.length > 0 ? 1 : 0;
1681
+ return parsed.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
1645
1682
  }
1646
1683
  function isDiagnostic(value) {
1647
1684
  if (!isRecord(value)) return false;
@@ -1715,6 +1752,53 @@ const plugin = defineCommand({
1715
1752
  //#region src/cli/commands/setup/interactive.ts
1716
1753
  const nonTtyMessage = "interactive setup requires a TTY. Use -N/--no-interactive with --provider-id and --provider-endpoint.\n";
1717
1754
  const backValue = "__alint_back__";
1755
+ function applyDefaultAlias(config, target) {
1756
+ return {
1757
+ ...config,
1758
+ providers: config.providers.map((provider) => ({
1759
+ ...provider,
1760
+ headers: provider.headers === void 0 ? void 0 : { ...provider.headers },
1761
+ models: provider.models.map((model) => {
1762
+ const aliasesWithoutDefault = (model.aliases ?? []).filter((alias) => alias !== "default");
1763
+ const aliases = provider.id === target.providerId && model.id === target.modelId ? [...aliasesWithoutDefault, "default"] : aliasesWithoutDefault;
1764
+ return {
1765
+ ...model,
1766
+ aliases: aliases.length > 0 ? aliases : void 0,
1767
+ capabilities: model.capabilities === void 0 ? void 0 : [...model.capabilities],
1768
+ defaultParams: model.defaultParams === void 0 ? void 0 : { ...model.defaultParams }
1769
+ };
1770
+ })
1771
+ }))
1772
+ };
1773
+ }
1774
+ function createDefaultModelCandidates(config, newProviderId, newModelIds) {
1775
+ const candidates = [];
1776
+ const seen = /* @__PURE__ */ new Set();
1777
+ const newModelIdSet = new Set(newModelIds);
1778
+ const allModels = config.providers.flatMap((provider) => provider.models.map((model) => ({
1779
+ isCurrentDefault: (model.aliases ?? []).includes("default"),
1780
+ isNew: provider.id === newProviderId && newModelIdSet.has(model.id),
1781
+ model,
1782
+ provider,
1783
+ value: createDefaultModelCandidateValue(provider.id, model.id)
1784
+ })));
1785
+ const addCandidate = (candidate) => {
1786
+ if (candidate === void 0 || seen.has(candidate.value)) return;
1787
+ seen.add(candidate.value);
1788
+ candidates.push({
1789
+ isCurrentDefault: candidate.isCurrentDefault,
1790
+ isNew: candidate.isNew,
1791
+ label: `${candidate.provider.id} / ${candidate.model.id}`,
1792
+ modelId: candidate.model.id,
1793
+ providerId: candidate.provider.id,
1794
+ value: candidate.value
1795
+ });
1796
+ };
1797
+ addCandidate(allModels.find((candidate) => candidate.isCurrentDefault));
1798
+ for (const modelId of newModelIds) addCandidate(allModels.find((candidate) => candidate.provider.id === newProviderId && candidate.model.id === modelId));
1799
+ for (const candidate of allModels) addCandidate(candidate);
1800
+ return candidates;
1801
+ }
1718
1802
  function formatProbeModelsFailure(endpoint, error) {
1719
1803
  const hint = endpoint.startsWith("https://localhost:11434") ? " Ollama usually uses http://localhost:11434/v1." : "";
1720
1804
  return `Could not probe models: ${errorMessageFrom(error)}.${hint}`;
@@ -1786,9 +1870,8 @@ async function runInteractiveSetup(io) {
1786
1870
  if (step === "providerId") {
1787
1871
  const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
1788
1872
  const providerId = await prompts.text({
1789
- defaultValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
1790
- message: "Provider id",
1791
- placeholder: "Type .. to go back",
1873
+ initialValue: draft.providerId ?? createProviderId(draft.endpoint ?? "", new Set(existingConfig.providers.map((provider) => provider.id))),
1874
+ message: "Provider id (type .. to go back)",
1792
1875
  validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider id is required."
1793
1876
  });
1794
1877
  if (prompts.isCancel(providerId)) return cancelPrompt();
@@ -1803,9 +1886,9 @@ async function runInteractiveSetup(io) {
1803
1886
  }
1804
1887
  if (step === "headers") {
1805
1888
  const headerInput = await prompts.text({
1806
- defaultValue: draft.headerInput ?? "",
1807
- message: "Headers",
1808
- placeholder: "Authorization=Bearer token, X-Test=true; type .. to go back",
1889
+ initialValue: draft.headerInput,
1890
+ message: "Headers (leave empty to skip; type .. to go back)",
1891
+ placeholder: "Authorization=Bearer token, X-Test=true",
1809
1892
  validate: (value) => {
1810
1893
  if (isBackInput(value ?? "")) return;
1811
1894
  try {
@@ -1841,26 +1924,69 @@ async function runInteractiveSetup(io) {
1841
1924
  continue;
1842
1925
  }
1843
1926
  if (step === "defaultAlias") {
1844
- const addDefaultAlias = await prompts.select({
1927
+ const defaultAliasAction = await prompts.select({
1845
1928
  message: `Add alias "default" to ${draft.selectedModels?.[0]}?`,
1846
- options: withBackOption([{
1847
- label: "Yes",
1848
- value: "yes"
1849
- }, {
1850
- label: "No",
1851
- value: "no"
1852
- }])
1929
+ options: withBackOption([
1930
+ {
1931
+ label: "Yes",
1932
+ value: "yes"
1933
+ },
1934
+ {
1935
+ label: "No",
1936
+ value: "no"
1937
+ },
1938
+ {
1939
+ label: "Select another",
1940
+ value: "selectAnother"
1941
+ }
1942
+ ])
1853
1943
  });
1854
- if (prompts.isCancel(addDefaultAlias)) return cancelPrompt();
1855
- if (addDefaultAlias === backValue) {
1944
+ if (prompts.isCancel(defaultAliasAction)) return cancelPrompt();
1945
+ if (defaultAliasAction === backValue) {
1856
1946
  step = "models";
1857
1947
  continue;
1858
1948
  }
1859
- draft.addDefaultAlias = addDefaultAlias === "yes";
1949
+ if (defaultAliasAction === "selectAnother") {
1950
+ step = "selectDefaultModel";
1951
+ continue;
1952
+ }
1953
+ draft.defaultAliasTarget = defaultAliasAction === "yes" ? {
1954
+ modelId: draft.selectedModels?.[0] ?? "",
1955
+ providerId: (draft.providerId ?? "").trim()
1956
+ } : void 0;
1957
+ step = "confirm";
1958
+ continue;
1959
+ }
1960
+ if (step === "selectDefaultModel") {
1961
+ const existingConfig = await loadSetupConfig(getConfigPath(io, draft.scope ?? "global"));
1962
+ const draftProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? []);
1963
+ const candidates = createDefaultModelCandidates(mergeSetupConfigs(existingConfig, {
1964
+ providers: [draftProvider],
1965
+ version: 1
1966
+ }), draftProvider.id, draft.selectedModels ?? []);
1967
+ const defaultModel = await prompts.select({
1968
+ message: "Select default model",
1969
+ options: withBackOption(candidates.map((candidate) => ({
1970
+ hint: candidate.isCurrentDefault ? "current default" : candidate.isNew ? "new" : void 0,
1971
+ label: candidate.label,
1972
+ value: candidate.value
1973
+ })))
1974
+ });
1975
+ if (prompts.isCancel(defaultModel)) return cancelPrompt();
1976
+ if (defaultModel === backValue) {
1977
+ step = "defaultAlias";
1978
+ continue;
1979
+ }
1980
+ const candidate = candidates.find((item) => item.value === defaultModel);
1981
+ if (candidate === void 0) return cancelPrompt();
1982
+ draft.defaultAliasTarget = {
1983
+ modelId: candidate.modelId,
1984
+ providerId: candidate.providerId
1985
+ };
1860
1986
  step = "confirm";
1861
1987
  continue;
1862
1988
  }
1863
- const nextProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? [], draft.addDefaultAlias ?? true);
1989
+ const nextProvider = createProviderConfig((draft.providerId ?? "").trim(), (draft.endpoint ?? "").trim(), draft.headers, draft.selectedModels ?? []);
1864
1990
  const confirmed = await prompts.select({
1865
1991
  message: [
1866
1992
  `Write ${draft.scope} setup config?`,
@@ -1883,10 +2009,11 @@ async function runInteractiveSetup(io) {
1883
2009
  }
1884
2010
  if (confirmed === "no") return cancelPrompt();
1885
2011
  const configPath = getConfigPath(io, draft.scope ?? "global");
1886
- await writeSetupConfig(configPath, mergeSetupConfigs(await loadSetupConfig(configPath), {
2012
+ const mergedConfig = mergeSetupConfigs(await loadSetupConfig(configPath), {
1887
2013
  providers: [nextProvider],
1888
2014
  version: 1
1889
- }));
2015
+ });
2016
+ await writeSetupConfig(configPath, draft.defaultAliasTarget === void 0 ? mergedConfig : applyDefaultAlias(mergedConfig, draft.defaultAliasTarget));
1890
2017
  prompts.outro(`Wrote ${configPath}`);
1891
2018
  return 0;
1892
2019
  }
@@ -1897,13 +2024,15 @@ function withBackOption(options) {
1897
2024
  value: backValue
1898
2025
  }];
1899
2026
  }
1900
- function createProviderConfig(providerId, endpoint, headers, modelIds, addDefaultAlias) {
2027
+ function createDefaultModelCandidateValue(providerId, modelId) {
2028
+ return `${providerId}\u0000${modelId}`;
2029
+ }
2030
+ function createProviderConfig(providerId, endpoint, headers, modelIds) {
1901
2031
  return {
1902
2032
  endpoint,
1903
2033
  headers,
1904
2034
  id: providerId,
1905
- models: modelIds.map((modelId, index) => ({
1906
- aliases: index === 0 && addDefaultAlias ? ["default"] : void 0,
2035
+ models: modelIds.map((modelId) => ({
1907
2036
  id: modelId,
1908
2037
  name: modelId
1909
2038
  })),
@@ -1927,9 +2056,9 @@ async function probeModelsWithSpinner(prompts, endpoint, headers) {
1927
2056
  }
1928
2057
  async function promptEndpoint(prompts, source) {
1929
2058
  return prompts.text({
1930
- defaultValue: source.defaultEndpoint,
1931
- message: "Provider endpoint",
1932
- placeholder: `${source.defaultEndpoint ?? "https://example.test/v1"}; type .. to go back`,
2059
+ initialValue: source.defaultEndpoint,
2060
+ message: "Provider endpoint (type .. to go back)",
2061
+ placeholder: source.defaultEndpoint ?? "https://example.test/v1",
1933
2062
  validate: (value) => isBackInput(value ?? "") || (value ?? "").trim().length > 0 ? void 0 : "Provider endpoint is required."
1934
2063
  });
1935
2064
  }
@@ -2163,7 +2292,7 @@ async function executeCli(argv, io) {
2163
2292
  pendingResult = result;
2164
2293
  return result;
2165
2294
  };
2166
- cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("-l, --lang <language>", "Ask model-backed rules to write diagnostics in this language").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--no-stats", "Do not record run stats for this run").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").version(version).help();
2295
+ cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("-c, --config <path>", "Path to alint config file").option("--file-concurrency <count>", "Number of files to lint concurrently").option("--format <format>", "Reporter format", { default: "stylish" }).option("--model <model>", "Force a model override").option("-l, --lang <language>", "Ask model-backed rules to write diagnostics in this language").option("--progress", "Show run progress").option("--rule-concurrency <count>", "Number of rules to run concurrently within a file").option("--no-stats", "Do not record run stats for this run").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").version(version).help();
2167
2296
  registerCommandTree(cli, commandTree, {
2168
2297
  globalOptions,
2169
2298
  interceptConsoleOutput,
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-Dey9TSc8.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-CwuIPM7m.mjs";
2
2
  import { ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated } from "@alint-js/config";
3
3
  import { defineConfig } from "@alint-js/core";
4
4
  export { defineConfig, executeCli, formatDiagnostics, formatJson, formatStylish, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -25,18 +25,14 @@
25
25
  "pathe": "^2.0.3",
26
26
  "table": "^6.9.0",
27
27
  "tinyrainbow": "^3.1.0",
28
- "@alint-js/config": "0.0.21",
29
- "@alint-js/core": "0.0.21"
28
+ "@alint-js/config": "0.0.23",
29
+ "@alint-js/core": "0.0.23"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@pnpm/find-workspace-dir": "^1000.1.5",
33
- "@types/node": "^26.0.1",
34
33
  "local-pkg": "^1.2.1",
35
34
  "mlly": "^1.8.2",
36
- "tinyexec": "^1.2.4",
37
- "tsdown": "^0.22.3",
38
- "typescript": "^6.0.3",
39
- "vitest": "^4.1.9"
35
+ "tinyexec": "^1.2.4"
40
36
  },
41
37
  "scripts": {
42
38
  "alint": "node ./dist/bin/index.mjs",