@alint-js/cli 0.3.1 → 0.4.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 CHANGED
@@ -167,6 +167,8 @@ Override the matched model for a one-off run:
167
167
  alint --model qwen:8b demo.ts
168
168
  ```
169
169
 
170
+ When the project-local setup does not configure any models, model calls without a rule-level or call-level selector use the `default` alias from the global setup. Rule selectors continue to use normal model matching. Configuring at least one model in `.alint/config.toml` restores project-first matching for unselected calls. An explicit `--model` override always takes precedence.
171
+
170
172
  Ask model-backed rules to write diagnostics in a specific language:
171
173
 
172
174
  ```bash
@@ -213,18 +215,12 @@ alint output inspect alint-output.json
213
215
 
214
216
  #### Configurations
215
217
 
216
- In order to provision models and LLMs for `alint` while keeping it clean for contributors of your project without requiring them to set up their own LLMs, `alint` offers layers of configuration covering **Project Local**, **Global**, project config, and environment or CLI overrides.
217
-
218
- The priorities follow:
219
-
220
- ```text
221
- ~/.config/alint/config.toml < .alint/config.toml < alint.config.* < environment and CLI overrides
222
- ```
218
+ `alint` uses two configuration systems with separate schemas and responsibilities:
223
219
 
224
- - `~/.config/alint/config.toml` stores user-level provider setup.
225
- - `.alint/config.toml` stores optional project-local provider setup.
226
- - `alint.config.*` stores project lint config, plugins, files, ignores, and rule settings.
227
- - Environment variables and CLI flags are the highest-priority overrides.
220
+ | Configuration | Priority (high → low) | Content |
221
+ | --- |--------------------------------------------------------------------------------------|----------------------------------------------------------------------|
222
+ | Setup | Environment and CLI overrides > `.alint/config.toml` > `~/.config/alint/config.toml` | Providers, models, and runner defaults. |
223
+ | Lint | Environment and CLI overrides > `alint.config.*` | Files, ignores, plugins, rules, and other project-level lint policy. |
228
224
 
229
225
  Use setup TOML for machine or project provider definitions:
230
226
 
@@ -241,8 +237,44 @@ id = "qwen:8b"
241
237
  name = "qwen:8b"
242
238
  size = "small"
243
239
  capabilities = [ "tool-call" ]
240
+
241
+ [providers.models.default_params]
242
+ thinking = { type = "disabled" }
244
243
  ```
245
244
 
245
+ `default_params` is merged into every chat request for that model. Use it for provider-specific request fields that are not covered by the rest of the model entry.
246
+
247
+ The CLI can also launch an ACP coding-agent command and expose it to rules as an ordinary model. Interactive `alint setup` provides presets for Claude Code, Codex, Gemini CLI, Kimi Code CLI, and OpenCode. Put a custom machine-specific command in the global setup config, or in `.alint/config.toml` when the repository standardizes the same command for every contributor:
248
+
249
+ ```toml
250
+ version = 1
251
+
252
+ [[providers]]
253
+ id = "acp"
254
+
255
+ [[providers.models]]
256
+ id = "codex"
257
+ name = "Codex ACP"
258
+ driver = "acp"
259
+ aliases = [ "default" ]
260
+ capabilities = [ "tool-call" ]
261
+ command = "codex-acp"
262
+ args = []
263
+ cwd = "."
264
+ ```
265
+
266
+ Then select it like any other model:
267
+
268
+ ```bash
269
+ alint --model acp/codex src
270
+ ```
271
+
272
+ `alint.config.ts` remains the shareable lint policy: it selects models, plugins, and rules but does not launch processes. The CLI merges global setup with `.alint/config.toml`, starts a loopback OpenAI-compatible adapter for the run, and shuts it down afterward. Each concurrent model request gets its own ACP process, so `--rule-concurrency` also bounds how many coding-agent processes a run can require.
273
+
274
+ The command inherits the environment that launched `alint`. Optional `[providers.models.env]` entries override individual variables, but setup TOML does not interpolate variables. Do not commit credentials into the table. Request tools require the ACP agent to support MCP over HTTP.
275
+
276
+ `alint` structured output forces a tool call (`tool_choice`). Some models, such as DeepSeek V4, reject that combination while thinking/reasoning is enabled. For those models, set `thinking = { type = "disabled" }` as above so structured output can run.
277
+
246
278
  Note that `-N` stands for `--no-interactive`, which means this is not an interactive setup. TUI is not required, so you can ask Codex or Claude Code to run this command for you.
247
279
 
248
280
  You can also use `--local` to write the config in the current project:
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-CL433bER.mjs";
2
+ import { t as executeCli } from "../cli-DFWnPI6m.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -2,12 +2,13 @@ import process from "node:process";
2
2
  import { inspect } from "node:util";
3
3
  import { cac } from "cac";
4
4
  import { appendFile, mkdir, opendir, readFile, readdir, rm, stat } from "node:fs/promises";
5
- import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
5
+ import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, isAcpModel, loadAlintConfig, loadSetupConfig, mergeSetupConfigs, normalizeSetupConfig, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
6
6
  import { AlintRunCancelledError, AlintRunError, benchmarkModels, hasDiscoveryFilePatterns, matchesDiscoveryFile, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, runAlint } from "@alint-js/core";
7
7
  import { isAbsolute, join, relative, resolve } from "pathe";
8
8
  import { errorMessageFrom } from "@moeru/std/error";
9
9
  import { getBorderCharacters, table } from "table";
10
10
  import { createColors } from "tinyrainbow";
11
+ import { createCommandModel, startGateway } from "@alint-js/model-adapter-acp";
11
12
  import cliSpinners from "cli-spinners";
12
13
  import fastStringWidth from "fast-string-width";
13
14
  import { relative as relative$1 } from "node:path";
@@ -17,7 +18,7 @@ import Gitignore from "gitignore-fs";
17
18
  import { Minimatch, minimatch } from "minimatch";
18
19
  import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std";
19
20
  //#region package.json
20
- var version = "0.3.1";
21
+ var version = "0.4.0";
21
22
  //#endregion
22
23
  //#region src/cli/output.ts
23
24
  function escapeLineValue(value) {
@@ -296,12 +297,14 @@ const providerSetupSources = [
296
297
  benchmarkConcurrency: 2,
297
298
  defaultEndpoint: "http://127.0.0.1:8317/v1",
298
299
  defaultProviderId: "CLIProxyAPI",
300
+ kind: "openai-compatible",
299
301
  label: "CLIProxyAPI",
300
302
  probeModels: true,
301
303
  value: "cliProxyApi"
302
304
  },
303
305
  {
304
306
  benchmarkConcurrency: 2,
307
+ kind: "openai-compatible",
305
308
  label: "Custom OpenAI-compatible provider",
306
309
  probeModels: true,
307
310
  value: "custom"
@@ -310,6 +313,7 @@ const providerSetupSources = [
310
313
  benchmarkConcurrency: 20,
311
314
  defaultEndpoint: "https://openrouter.ai/api/v1",
312
315
  defaultProviderId: "openrouter",
316
+ kind: "openai-compatible",
313
317
  label: "OpenRouter",
314
318
  probeModels: true,
315
319
  value: "openrouter"
@@ -318,6 +322,7 @@ const providerSetupSources = [
318
322
  benchmarkConcurrency: 2,
319
323
  defaultEndpoint: "https://api.cerebras.ai/v1",
320
324
  defaultProviderId: "cerebras",
325
+ kind: "openai-compatible",
321
326
  label: "Cerebras",
322
327
  probeModels: true,
323
328
  value: "cerebras"
@@ -326,6 +331,7 @@ const providerSetupSources = [
326
331
  benchmarkConcurrency: 2,
327
332
  defaultEndpoint: "https://api.groq.com/openai/v1",
328
333
  defaultProviderId: "groq",
334
+ kind: "openai-compatible",
329
335
  label: "Groq",
330
336
  probeModels: true,
331
337
  value: "groq"
@@ -333,15 +339,81 @@ const providerSetupSources = [
333
339
  {
334
340
  benchmarkConcurrency: 2,
335
341
  defaultEndpoint: "http://localhost:11434/v1",
342
+ kind: "openai-compatible",
336
343
  label: "Ollama",
337
344
  probeModels: true,
338
345
  value: "ollama"
339
346
  },
340
347
  {
341
348
  benchmarkConcurrency: 2,
349
+ kind: "openai-compatible",
342
350
  label: "Manual model entry",
343
351
  probeModels: false,
344
352
  value: "manual"
353
+ },
354
+ {
355
+ args: ["-y", "@agentclientprotocol/claude-agent-acp"],
356
+ command: "npx",
357
+ defaultProviderId: "claude-code",
358
+ kind: "acp",
359
+ label: "Claude Code (ACP)",
360
+ model: {
361
+ id: "claude-code",
362
+ name: "Claude Code"
363
+ },
364
+ value: "claudeCodeAcp"
365
+ },
366
+ {
367
+ args: ["-y", "@agentclientprotocol/codex-acp"],
368
+ command: "npx",
369
+ defaultProviderId: "codex",
370
+ kind: "acp",
371
+ label: "Codex (ACP)",
372
+ model: {
373
+ id: "codex",
374
+ name: "Codex"
375
+ },
376
+ value: "codexAcp"
377
+ },
378
+ {
379
+ args: [
380
+ "-y",
381
+ "@google/gemini-cli",
382
+ "--acp"
383
+ ],
384
+ command: "npx",
385
+ defaultProviderId: "gemini-cli",
386
+ kind: "acp",
387
+ label: "Gemini CLI (ACP)",
388
+ model: {
389
+ id: "gemini-cli",
390
+ name: "Gemini CLI"
391
+ },
392
+ value: "geminiCliAcp"
393
+ },
394
+ {
395
+ args: ["acp"],
396
+ command: "kimi",
397
+ defaultProviderId: "kimi-code",
398
+ kind: "acp",
399
+ label: "Kimi Code CLI (ACP)",
400
+ model: {
401
+ id: "kimi-code",
402
+ name: "Kimi Code CLI"
403
+ },
404
+ value: "kimiCliAcp"
405
+ },
406
+ {
407
+ args: ["acp"],
408
+ command: "opencode",
409
+ defaultProviderId: "opencode",
410
+ kind: "acp",
411
+ label: "OpenCode (ACP)",
412
+ model: {
413
+ id: "opencode",
414
+ name: "OpenCode"
415
+ },
416
+ value: "openCodeAcp"
345
417
  }
346
418
  ];
347
419
  function buildModelsUrl(endpoint) {
@@ -379,7 +451,7 @@ function findProviderSetupSource(value) {
379
451
  }
380
452
  function findProviderSetupSourceByEndpoint(endpoint) {
381
453
  const normalizedEndpoint = endpoint instanceof URL ? endpoint.toString() : new URL(endpoint).toString();
382
- return providerSetupSources.find((source) => source.defaultEndpoint !== void 0 && new URL(source.defaultEndpoint).toString() === normalizedEndpoint);
454
+ return providerSetupSources.find((source) => source.kind === "openai-compatible" && source.defaultEndpoint !== void 0 && new URL(source.defaultEndpoint).toString() === normalizedEndpoint);
383
455
  }
384
456
  function flattenModels(config) {
385
457
  return config.providers.flatMap((provider) => provider.models.map((model) => ({
@@ -520,16 +592,16 @@ function formatProviderList(config) {
520
592
  "models"
521
593
  ], ...config.providers.map((provider) => [
522
594
  provider.id,
523
- provider.type,
524
- provider.endpoint,
595
+ provider.type ?? "model-driven",
596
+ provider.endpoint ?? "-",
525
597
  String(provider.models.length)
526
598
  ])]);
527
599
  }
528
600
  function formatProviderShow(provider) {
529
601
  const lines = [
530
602
  `id: ${provider.id}`,
531
- `type: ${provider.type}`,
532
- `endpoint: ${provider.endpoint}`,
603
+ `type: ${provider.type ?? "model-driven"}`,
604
+ `endpoint: ${provider.endpoint ?? "-"}`,
533
605
  `models: ${provider.models.map((model) => model.id).join(", ")}`
534
606
  ];
535
607
  const headerKeys = Object.keys(provider.headers ?? {});
@@ -560,7 +632,7 @@ async function probeModels(endpoint, headers = {}) {
560
632
  return body.data.map((model) => model.id);
561
633
  }
562
634
  function resolveProviderBenchmarkConcurrency(config, overrides = []) {
563
- const concurrency = Object.fromEntries(config.providers.map((provider) => [provider.id, findProviderSetupSourceByEndpoint(provider.endpoint)?.benchmarkConcurrency ?? defaultBenchmarkConcurrency]));
635
+ const concurrency = Object.fromEntries(config.providers.map((provider) => [provider.id, provider.endpoint === void 0 ? defaultBenchmarkConcurrency : findProviderSetupSourceByEndpoint(provider.endpoint)?.benchmarkConcurrency ?? defaultBenchmarkConcurrency]));
564
636
  for (const override of overrides) {
565
637
  const separatorIndex = override.indexOf("=");
566
638
  if (separatorIndex <= 0 || separatorIndex === override.length - 1) throw new Error("Invalid benchmark concurrency override. Expected <provider-id>=<limit>.");
@@ -645,17 +717,105 @@ function stripTrailingZeros(value) {
645
717
  return String(Number(value));
646
718
  }
647
719
  //#endregion
720
+ //#region src/cli/runtime/model-adapter.ts
721
+ /**
722
+ * Normalizes file configuration and starts its model adapters.
723
+ *
724
+ * Triggering workflow:
725
+ *
726
+ * `runLintCommand | runListModelsCommand`
727
+ * -> {@link startModelAdapters}
728
+ * -> {@link createAcpAdapter}
729
+ * -> {@link normalizeSetupConfig}
730
+ * -> `AcpAdapter.provider`
731
+ * -> {@link startGateway}
732
+ *
733
+ * Upstream:
734
+ * - `runLintCommand`
735
+ * - `runListModelsCommand`
736
+ *
737
+ * Downstream:
738
+ * - {@link normalizeSetupConfig}
739
+ * - {@link startGateway}
740
+ */
741
+ async function startModelAdapters(config, io) {
742
+ const acp = createAcpAdapter(io);
743
+ try {
744
+ return {
745
+ setupConfig: await normalizeSetupConfig(config, { acp: acp.provider }),
746
+ shutdown: acp.shutdown
747
+ };
748
+ } catch (error) {
749
+ await acp.shutdown();
750
+ throw error;
751
+ }
752
+ }
753
+ /** Owns the ACP processes and loopback gateways used by one CLI operation. */
754
+ function createAcpAdapter(io) {
755
+ const shutdowns = [];
756
+ return {
757
+ provider: async (provider) => {
758
+ const gateway = await startGateway({
759
+ cwd: io.cwd,
760
+ models: provider.models.map((model) => createCommandModel({
761
+ args: model.args,
762
+ command: model.command,
763
+ cwd: resolve(io.cwd, model.cwd ?? "."),
764
+ env: {
765
+ ...io.env ?? process.env,
766
+ ...model.env
767
+ },
768
+ id: model.id,
769
+ name: model.name ?? model.id,
770
+ onStderr: createStderrWriter(io, model.id)
771
+ }))
772
+ });
773
+ shutdowns.push(gateway.shutdown);
774
+ return {
775
+ endpoint: gateway.endpoint,
776
+ type: "openai-compatible"
777
+ };
778
+ },
779
+ shutdown: async () => {
780
+ await Promise.all(shutdowns.map((shutdown) => shutdown()));
781
+ }
782
+ };
783
+ }
784
+ /**
785
+ * Creates the CLI stderr sink for non-protocol output emitted by an ACP command.
786
+ *
787
+ * Triggering workflow:
788
+ *
789
+ * {@link createCommandModel}
790
+ * -> `ChildProcess.stderr:data`
791
+ * -> {@link createStderrWriter}
792
+ *
793
+ * Upstream:
794
+ * - {@link createAcpAdapter}
795
+ *
796
+ * Downstream:
797
+ * - {@link CliIo.stderr}
798
+ */
799
+ function createStderrWriter(io, modelId) {
800
+ return (text) => io.stderr.write(`[acp:${modelId}] ${text}`);
801
+ }
802
+ //#endregion
648
803
  //#region src/cli/commands/config/setup-config.ts
649
804
  function formatUnknownProvider(providerId, scope) {
650
805
  const hint = scope === "local" ? " Remove --local to inspect global configuration." : " Add --local to inspect project-local configuration.";
651
806
  return `unknown provider "${escapeLineValue(providerId)}" in ${scope} setup config.${hint}\n`;
652
807
  }
653
808
  async function loadMergedSetupConfig(io) {
654
- const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
655
- const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
656
- const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
809
+ const { globalSetupConfig, projectSetupConfig } = await loadSetupConfigs(io);
657
810
  return mergeSetupConfigs(globalSetupConfig, projectSetupConfig);
658
811
  }
812
+ async function loadRunSetupConfig(io) {
813
+ const { globalSetupConfig, projectSetupConfig } = await loadSetupConfigs(io);
814
+ return {
815
+ defaultModel: projectSetupConfig.providers.some((provider) => provider.models.length > 0) ? void 0 : "default",
816
+ setupConfig: mergeSetupConfigs(globalSetupConfig, projectSetupConfig)
817
+ };
818
+ }
659
819
  async function loadScopedSetupConfig(io, local) {
660
820
  const scope = local === true ? "local" : "global";
661
821
  const path = scope === "local" ? getProjectSetupConfigPath(io.cwd) : getGlobalSetupConfigPath(io.env ?? process.env);
@@ -665,6 +825,15 @@ async function loadScopedSetupConfig(io, local) {
665
825
  scope
666
826
  };
667
827
  }
828
+ async function loadSetupConfigs(io) {
829
+ const globalSetupConfigPath = getGlobalSetupConfigPath(io.env ?? process.env);
830
+ const projectSetupConfigPath = getProjectSetupConfigPath(io.cwd);
831
+ const [globalSetupConfig, projectSetupConfig] = await Promise.all([loadSetupConfig(globalSetupConfigPath), loadSetupConfig(projectSetupConfigPath)]);
832
+ return {
833
+ globalSetupConfig,
834
+ projectSetupConfig
835
+ };
836
+ }
668
837
  //#endregion
669
838
  //#region src/cli/commands/config/models/benchmark-progress.ts
670
839
  /** Keeps the benchmark preview live without mixing transient frames into the final stdout table. */
@@ -806,19 +975,21 @@ async function runListModelsCommand(context, options) {
806
975
  context.io.stderr.write(`${errorMessageFrom(error) ?? "Invalid benchmark concurrency override."}\n`);
807
976
  return 2;
808
977
  }
978
+ const runtime = await startModelAdapters(config, context.io);
809
979
  const progress = context.io.stderr.isTTY === true ? createModelBenchmarkProgressDisplay({
810
980
  color: true,
811
- config,
981
+ config: runtime.setupConfig,
812
982
  output: context.io.stderr
813
983
  }) : void 0;
814
984
  let benchmarks;
815
985
  try {
816
- benchmarks = await benchmarkModels(config, {
986
+ benchmarks = await benchmarkModels(runtime.setupConfig, {
817
987
  onProgress: (snapshot) => progress?.update(snapshot),
818
988
  providerConcurrency
819
989
  });
820
990
  } finally {
821
991
  progress?.finish();
992
+ await runtime.shutdown();
822
993
  }
823
994
  context.io.stdout.write(formatModelBenchmarkList(config, benchmarks, { color: context.io.stdout.isTTY === true }));
824
995
  return 0;
@@ -927,15 +1098,21 @@ async function runPruneModelsCommand(context, options, confirm = confirmPruneMod
927
1098
  const plans = [];
928
1099
  let hadFailure = false;
929
1100
  for (const provider of providers) {
1101
+ const endpoint = provider.endpoint;
1102
+ if (endpoint === void 0) {
1103
+ hadFailure = true;
1104
+ context.io.stderr.write(`cannot prune provider "${escapeLineValue(provider.id)}" because it has no remote endpoint.\n`);
1105
+ continue;
1106
+ }
930
1107
  let remoteModelIds;
931
1108
  try {
932
- remoteModelIds = new Set(await probeModels(provider.endpoint, provider.headers));
1109
+ remoteModelIds = new Set(await probeModels(endpoint, provider.headers));
933
1110
  } catch (error) {
934
1111
  hadFailure = true;
935
- context.io.stderr.write(`failed to probe provider "${escapeLineValue(provider.id)}" at "${escapeLineValue(provider.endpoint)}": ${classifyProbeFailure(error)}.\n`);
1112
+ context.io.stderr.write(`failed to probe provider "${escapeLineValue(provider.id)}" at "${escapeLineValue(endpoint)}": ${classifyProbeFailure(error)}.\n`);
936
1113
  continue;
937
1114
  }
938
- const removedModels = provider.models.filter((model) => !remoteModelIds.has(model.id));
1115
+ const removedModels = provider.models.filter((model) => !isAcpModel(model) && !remoteModelIds.has(model.id));
939
1116
  const defaultModel = removedModels.find((model) => (model.aliases ?? []).includes("default"));
940
1117
  if (defaultModel !== void 0) {
941
1118
  hadFailure = true;
@@ -1536,7 +1713,7 @@ function createProviderEditorPrompts(prompts) {
1536
1713
  async headerInput() {
1537
1714
  return textResult(prompts, await prompts.text({
1538
1715
  message: "Headers (leave empty to skip; type .. to go back)",
1539
- placeholder: "Authorization=Bearer token, X-Test=true",
1716
+ placeholder: "Authorization=Bearer sk-or-v1-xxxxxx, X-Test=true",
1540
1717
  validate: (value) => {
1541
1718
  if (isBackInput(value ?? "")) return;
1542
1719
  try {
@@ -1655,6 +1832,7 @@ function textResult(prompts, result) {
1655
1832
  */
1656
1833
  async function runProviderEditor(input, promptPort) {
1657
1834
  const prompts = promptPort ?? createProviderEditorPrompts(await import("@clack/prompts"));
1835
+ if (input.source.kind === "acp") return runAcpProviderEditor(input, input.source, prompts);
1658
1836
  const existingProvider = input.mode === "update" ? input.existingProvider : void 0;
1659
1837
  const configuredHeaders = { ...existingProvider?.headers };
1660
1838
  const initialHeaders = applyHeaderSelection(configuredHeaders, Object.keys(configuredHeaders), {}) ?? {};
@@ -1786,6 +1964,30 @@ async function runProviderEditor(input, promptPort) {
1786
1964
  };
1787
1965
  }
1788
1966
  }
1967
+ function createAcpProvider(source, providerId) {
1968
+ return {
1969
+ id: providerId,
1970
+ models: [{
1971
+ ...source.args === void 0 ? {} : { args: [...source.args] },
1972
+ capabilities: ["tool-call"],
1973
+ command: source.command,
1974
+ driver: "acp",
1975
+ ...source.model
1976
+ }]
1977
+ };
1978
+ }
1979
+ function createAcpSummary(input, provider, defaultAliasTarget) {
1980
+ const model = provider.models[0];
1981
+ return [
1982
+ `${input.mode === "create" ? "Create" : "Update"} provider?`,
1983
+ `Provider: ${escapeLineValue(provider.id)}`,
1984
+ "Driver: ACP",
1985
+ `Command: ${escapeLineValue("command" in model ? model.command : "-")}`,
1986
+ `Arguments: ${"args" in model ? formatSummaryValues(model.args ?? []) : "(none)"}`,
1987
+ `Model: ${escapeLineValue(model.id)}`,
1988
+ `Default: ${defaultAliasTarget === void 0 ? "unchanged" : escapeLineValue(`${defaultAliasTarget.providerId}/${defaultAliasTarget.modelId}`)}`
1989
+ ].join("\n");
1990
+ }
1789
1991
  function createProvider(input, draft) {
1790
1992
  return {
1791
1993
  ...input.existingProvider,
@@ -1805,7 +2007,7 @@ function createSummary(input, provider, defaultAliasTarget) {
1805
2007
  return [
1806
2008
  `${input.mode === "create" ? "Create" : "Update"} provider?`,
1807
2009
  `Provider: ${escapeLineValue(provider.id)}`,
1808
- `Endpoint: ${escapeLineValue(provider.endpoint)}`,
2010
+ `Endpoint: ${escapeLineValue(provider.endpoint ?? "-")}`,
1809
2011
  `Headers: ${formatSummaryValues(Object.keys(provider.headers ?? {}))}`,
1810
2012
  `Added models: ${formatSummaryValues(additions)}`,
1811
2013
  `Removed models: ${formatSummaryValues(removals)}`,
@@ -1818,6 +2020,87 @@ function effectiveHeaders(draft) {
1818
2020
  function formatSummaryValues(values) {
1819
2021
  return values.length > 0 ? values.map(escapeLineValue).join(", ") : "(none)";
1820
2022
  }
2023
+ /**
2024
+ * Runs the preset-backed ACP branch of the provider editor.
2025
+ *
2026
+ * Triggering workflow:
2027
+ *
2028
+ * {@link runInteractiveSetup}
2029
+ * -> {@link runProviderEditor}
2030
+ * -> `provider-editor.acp-preset`
2031
+ * -> {@link runAcpProviderEditor}
2032
+ *
2033
+ * Upstream:
2034
+ * - {@link runProviderEditor}
2035
+ *
2036
+ * Downstream:
2037
+ * - {@link ProviderEditorResult} and the caller-owned setup-config write
2038
+ */
2039
+ async function runAcpProviderEditor(input, source, prompts) {
2040
+ let defaultAliasTarget;
2041
+ let providerId = input.existingProvider?.id ?? source.defaultProviderId;
2042
+ let step = "providerId";
2043
+ while (true) {
2044
+ if (step === "providerId") {
2045
+ const result = await prompts.providerId(providerId, input.mode === "create");
2046
+ if (result.status === "cancelled") return { status: "cancelled" };
2047
+ if (result.status === "back") return { status: "back" };
2048
+ providerId = result.value.trim();
2049
+ step = "defaultAction";
2050
+ continue;
2051
+ }
2052
+ const provider = createAcpProvider(source, providerId);
2053
+ if (step === "defaultAction") {
2054
+ const result = await prompts.defaultAction(source.model.id);
2055
+ if (result.status === "cancelled") return { status: "cancelled" };
2056
+ if (result.status === "back") {
2057
+ step = "providerId";
2058
+ continue;
2059
+ }
2060
+ defaultAliasTarget = result.value === "yes" ? {
2061
+ modelId: source.model.id,
2062
+ providerId
2063
+ } : void 0;
2064
+ step = result.value === "selectAnother" ? "defaultModel" : "confirm";
2065
+ continue;
2066
+ }
2067
+ if (step === "defaultModel") {
2068
+ const candidates = createDefaultModelCandidates(mergeSetupConfigs(input.config, {
2069
+ providers: [provider],
2070
+ version: 1
2071
+ }), provider.id, [source.model.id]);
2072
+ const selectedDefault = await prompts.defaultModel(candidates.map((candidate) => ({
2073
+ hint: candidate.isCurrentDefault ? "current default" : candidate.isNew ? "new" : void 0,
2074
+ label: candidate.label,
2075
+ value: candidate.value
2076
+ })));
2077
+ if (selectedDefault.status === "cancelled") return { status: "cancelled" };
2078
+ if (selectedDefault.status === "back") {
2079
+ step = "defaultAction";
2080
+ continue;
2081
+ }
2082
+ const candidate = candidates.find((item) => item.value === selectedDefault.value);
2083
+ if (candidate === void 0) return { status: "cancelled" };
2084
+ defaultAliasTarget = {
2085
+ modelId: candidate.modelId,
2086
+ providerId: candidate.providerId
2087
+ };
2088
+ step = "confirm";
2089
+ continue;
2090
+ }
2091
+ const confirmed = await prompts.confirm(createAcpSummary(input, provider, defaultAliasTarget));
2092
+ if (confirmed.status === "cancelled" || confirmed.status === "submitted" && confirmed.value === "no") return { status: "cancelled" };
2093
+ if (confirmed.status === "back") {
2094
+ step = "defaultAction";
2095
+ continue;
2096
+ }
2097
+ return {
2098
+ ...defaultAliasTarget === void 0 ? {} : { defaultAliasTarget },
2099
+ provider,
2100
+ status: "confirmed"
2101
+ };
2102
+ }
2103
+ }
1821
2104
  function splitInput(value) {
1822
2105
  return value.split(",").map((item) => item.trim()).filter(Boolean);
1823
2106
  }
@@ -1871,6 +2154,11 @@ function mergeProviderHeaders(existing, replacements) {
1871
2154
  return Object.keys(headers).length > 0 ? headers : void 0;
1872
2155
  }
1873
2156
  async function runNonInteractiveUpdate(context, options, providerEndpoint, existingProvider, config, path) {
2157
+ const endpoint = providerEndpoint ?? existingProvider.endpoint;
2158
+ if (endpoint === void 0) {
2159
+ context.io.stderr.write(`provider "${existingProvider.id}" has no remote endpoint to update.\n`);
2160
+ return 2;
2161
+ }
1874
2162
  let parsedHeaders;
1875
2163
  try {
1876
2164
  parsedHeaders = parseHeaderList(toArray$1(options.providerHeader));
@@ -1880,12 +2168,12 @@ async function runNonInteractiveUpdate(context, options, providerEndpoint, exist
1880
2168
  }
1881
2169
  const draft = {
1882
2170
  ...existingProvider,
1883
- endpoint: providerEndpoint ?? existingProvider.endpoint,
2171
+ endpoint,
1884
2172
  headers: mergeProviderHeaders(existingProvider.headers, parsedHeaders)
1885
2173
  };
1886
2174
  let remoteModelIds;
1887
2175
  try {
1888
- remoteModelIds = await probeModels(draft.endpoint, draft.headers);
2176
+ remoteModelIds = await probeModels(endpoint, draft.headers);
1889
2177
  } catch (error) {
1890
2178
  context.io.stderr.write(`failed to probe provider: ${errorMessageFrom(error) ?? "Unknown error."}\n`);
1891
2179
  return 2;
@@ -1933,13 +2221,21 @@ async function runUpdateProviderCommand(context, options) {
1933
2221
  context.io.stderr.write(formatUnknownProvider(providerId, scope));
1934
2222
  return 2;
1935
2223
  }
2224
+ const endpoint = providerEndpoint ?? existingProvider.endpoint;
2225
+ if (endpoint === void 0) {
2226
+ context.io.stderr.write(`provider "${existingProvider.id}" has no remote endpoint to update.\n`);
2227
+ return 2;
2228
+ }
1936
2229
  if (context.setupNoInteractive) return runNonInteractiveUpdate(context, options, providerEndpoint, existingProvider, config, path);
1937
2230
  const result = await runProviderEditor({
1938
2231
  config,
1939
- existingProvider,
2232
+ existingProvider: {
2233
+ ...existingProvider,
2234
+ endpoint
2235
+ },
1940
2236
  io: context.io,
1941
2237
  mode: "update",
1942
- source: providerUpdateSource(existingProvider.endpoint)
2238
+ source: providerUpdateSource(endpoint)
1943
2239
  });
1944
2240
  if (result.status !== "confirmed") return 0;
1945
2241
  const replaced = replaceSetupProvider(config, result.provider);
@@ -3421,7 +3717,7 @@ function isNodeError(error) {
3421
3717
  }
3422
3718
  async function runLintCommand(files, options, io, interceptConsoleOutput) {
3423
3719
  if (options.config) await assertConfigExists(io.cwd, options.config);
3424
- const [setupConfig, config] = await Promise.all([loadMergedSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
3720
+ const [{ defaultModel, setupConfig }, config] = await Promise.all([loadRunSetupConfig(io), loadAlintConfig(io.cwd, options.config)]);
3425
3721
  let lintTargets;
3426
3722
  try {
3427
3723
  lintTargets = await findLintTargets({
@@ -3456,42 +3752,48 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
3456
3752
  const writeResult = (runResult) => {
3457
3753
  io.stdout.write(formatDiagnostics(options.format, runResult, { color: io.stdout.isTTY === true }));
3458
3754
  };
3755
+ const runtime = await startModelAdapters(setupConfig, io);
3459
3756
  let result;
3460
3757
  try {
3461
- result = await runAlint({
3462
- cacheOnly: options.cacheOnly,
3463
- config,
3464
- cwd: io.cwd,
3465
- directories: lintTargets.directories,
3466
- files: lintTargets.files,
3467
- modelOverride: options.model,
3468
- outputLanguage: options.outputLanguage,
3469
- progress: mergeProgressReporters(progress?.reporter, statsCollector?.reporter),
3470
- runner,
3471
- setupConfig
3472
- });
3473
- } catch (error) {
3758
+ try {
3759
+ result = await runAlint({
3760
+ cacheOnly: options.cacheOnly,
3761
+ config,
3762
+ cwd: io.cwd,
3763
+ defaultModel,
3764
+ directories: lintTargets.directories,
3765
+ files: lintTargets.files,
3766
+ modelOverride: options.model,
3767
+ outputLanguage: options.outputLanguage,
3768
+ progress: mergeProgressReporters(progress?.reporter, statsCollector?.reporter),
3769
+ runner,
3770
+ setupConfig: runtime.setupConfig
3771
+ });
3772
+ } catch (error) {
3773
+ restoreProgressConsole?.();
3774
+ progress?.dispose();
3775
+ if (error instanceof AlintRunError) {
3776
+ await persistStats(error.result);
3777
+ writeResult(error.result);
3778
+ io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
3779
+ return 2;
3780
+ }
3781
+ if (error instanceof AlintRunCancelledError) {
3782
+ await persistStats(error.result);
3783
+ writeResult(error.result);
3784
+ io.stderr.write(formatCancelledError(error, io.stderr.isTTY === true));
3785
+ return 2;
3786
+ }
3787
+ throw error;
3788
+ }
3474
3789
  restoreProgressConsole?.();
3475
3790
  progress?.dispose();
3476
- if (error instanceof AlintRunError) {
3477
- await persistStats(error.result);
3478
- writeResult(error.result);
3479
- io.stderr.write(formatRunError(error, io.stderr.isTTY === true));
3480
- return 2;
3481
- }
3482
- if (error instanceof AlintRunCancelledError) {
3483
- await persistStats(error.result);
3484
- writeResult(error.result);
3485
- io.stderr.write(formatCancelledError(error, io.stderr.isTTY === true));
3486
- return 2;
3487
- }
3488
- throw error;
3791
+ await persistStats(result);
3792
+ writeResult(result);
3793
+ return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
3794
+ } finally {
3795
+ await runtime.shutdown();
3489
3796
  }
3490
- restoreProgressConsole?.();
3491
- progress?.dispose();
3492
- await persistStats(result);
3493
- writeResult(result);
3494
- return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
3495
3797
  }
3496
3798
  function shouldEnableProgress(options, io) {
3497
3799
  if (options.progress !== void 0) return options.progress;
@@ -3660,7 +3962,7 @@ async function runInteractiveSetup(io) {
3660
3962
  label: "Global",
3661
3963
  value: "global"
3662
3964
  }, {
3663
- label: "Local project",
3965
+ label: "Local project - Warning: You are about to store your credentials in the folder",
3664
3966
  value: "local"
3665
3967
  }]
3666
3968
  });
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-CL433bER.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-DFWnPI6m.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.3.1",
4
+ "version": "0.4.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.mts",
@@ -28,8 +28,9 @@
28
28
  "pathe": "^2.0.3",
29
29
  "table": "^6.9.0",
30
30
  "tinyrainbow": "^3.1.1",
31
- "@alint-js/config": "0.3.1",
32
- "@alint-js/core": "0.3.1"
31
+ "@alint-js/model-adapter-acp": "0.4.0",
32
+ "@alint-js/core": "0.4.0",
33
+ "@alint-js/config": "0.4.0"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@pnpm/find-workspace-dir": "^1000.1.5",