@alint-js/cli 0.6.0 → 0.7.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
@@ -219,18 +219,17 @@ alint output inspect alint-output.json
219
219
 
220
220
  ### Editor Integration
221
221
 
222
- > **Work in progress.** `alint lsp` currently publishes cached diagnostics when an editor opens a
223
- > workspace, and nothing more. Saving a file does not refresh it, changing `alint.config.ts` does
224
- > not reload it, and the run commands below are advertised but not yet implemented. Restart the
225
- > server to pick up either kind of change.
222
+ `alint lsp` runs alint as a language server on stdin and stdout. Any editor with LSP support can
223
+ use it.
226
224
 
227
- `alint lsp` runs alint as a language server over stdin and stdout.
225
+ The server is cache-first. It publishes diagnostics that earlier runs stored, and it never calls a
226
+ model on its own, so opening a workspace costs nothing. A cold cache shows no diagnostics. Run
227
+ `alint` once to fill it.
228
228
 
229
- It is cache-first. The server reads diagnostics that earlier runs already stored and **never calls
230
- a model on its own**, so opening a workspace costs nothing. A cold cache therefore shows nothing —
231
- run `alint` once to populate it.
229
+ Diagnostics appear when the editor opens the workspace, and they refresh when you save a file. The
230
+ server reloads `alint.config.ts` after it changes.
232
231
 
233
- Point any LSP editor at the command. In Neovim:
232
+ Configure the editor to start the command. In Neovim:
234
233
 
235
234
  ```lua
236
235
  vim.lsp.config.alint = {
@@ -239,20 +238,23 @@ vim.lsp.config.alint = {
239
238
  }
240
239
  ```
241
240
 
242
- The server declares two commands, `alint.runFile` and `alint.runWorkspace`, which will start runs
243
- that call models and spend tokens. Both return `MethodNotFound` today.
241
+ The server declares three commands. `alint.runFile` and `alint.runWorkspace` start runs that call
242
+ models and spend tokens; both report progress and accept cancellation. `alint.clearCache` deletes
243
+ the cached diagnostics.
244
244
 
245
- A VS Code extension lives in `apps/vscode`. It starts the server and shows the diagnostics; it is
246
- not published to the marketplace yet. To run it from a checkout:
245
+ #### VS Code
246
+
247
+ The extension in `apps/vscode` starts the server and displays the diagnostics. It is not published
248
+ to the Marketplace yet. To run it from a checkout:
247
249
 
248
250
  ```bash
249
251
  pnpm -F @alint-js/vscode build
250
- code --extensionDevelopmentPath=apps/vscode /path/to/your/project
252
+ code --extensionDevelopmentPath=apps/vscode/dist /path/to/your/project
251
253
  ```
252
254
 
253
- It resolves the alint executable from the `alint.path` setting, then `node_modules/.bin/alint` in
254
- the workspace folder, then `PATH`. The workspace install wins so that the server and the alint you
255
- run in a terminal share one cache.
255
+ The extension finds the alint executable in this order: the `alint.path` setting, then
256
+ `node_modules/.bin/alint` in the workspace folder, then `PATH`. The workspace installation takes
257
+ precedence, so the server and a terminal run use the same cache.
256
258
 
257
259
  ### Codex stop-gate plugin (optional)
258
260
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-cQihuHqx.mjs";
2
+ import { t as executeCli } from "../cli-BsBgbtds.mjs";
3
3
  import process from "node:process";
4
4
  //#region src/bin/index.ts
5
5
  executeCli(process.argv, {
@@ -3,7 +3,7 @@ import { inspect } from "node:util";
3
3
  import { cac } from "cac";
4
4
  import { appendFile, chmod, mkdir, opendir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { addProviderModels, getGlobalSetupConfigPath, getProjectSetupConfigPath, getStatsDir, installStaticPlugins, isAcpModel, loadAlintConfig, loadAlintConfigWithMetadata, loadSetupConfig, mergeSetupConfigs, normalizeSetupConfig, pruneProviderModels, removeProviderModels, replaceSetupProvider, setProviderEndpoint, setProviderHeader, setStopGateConfig, unsetProviderHeader, writeSetupConfig } from "@alint-js/config";
6
- import { AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, defaultStopGateConfig, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, resolveConfigForDirectory, resolveConfigForFile, resolveStopGateConfig, runAlint, stableHash } from "@alint-js/core";
6
+ import { AlintCachePersistenceError, AlintRunCancelledError, AlintRunError, benchmarkModels, defaultStopGateConfig, hasDiscoveryFilePatterns, matchesDiscoveryFile, maximumStopGateTimeoutMs, normalizeConfig, normalizeRunnerCacheConfig, resolveConfigForDirectory, resolveConfigForFile, resolveStopGateConfig, runAlint, stableHash } from "@alint-js/core";
7
7
  import { isAbsolute, join, relative, resolve } from "pathe";
8
8
  import { errorMessageFrom } from "@moeru/std";
9
9
  import { errorMessageFrom as errorMessageFrom$1 } from "@moeru/std/error";
@@ -24,7 +24,7 @@ import { Minimatch, minimatch } from "minimatch";
24
24
  import fastStringTruncatedWidth from "fast-string-truncated-width";
25
25
  import { formatDuration, intervalToDuration } from "date-fns";
26
26
  //#region package.json
27
- var version = "0.6.0";
27
+ var version = "0.7.0";
28
28
  //#endregion
29
29
  //#region src/cli/output.ts
30
30
  function escapeLineValue(value) {
@@ -2877,7 +2877,7 @@ function mergeRunnerConfigs(setupRunner, configRunner) {
2877
2877
  };
2878
2878
  return Object.values(runner).some((value) => value !== void 0) ? runner : void 0;
2879
2879
  }
2880
- /** The last `runner` block in the flattened config wins, key by key. */
2880
+ /** The last `runner` block in the flattened config takes precedence, key by key. */
2881
2881
  function resolveConfigRunner(config) {
2882
2882
  return normalizeConfig(config).reduce((merged, item) => item.runner ? {
2883
2883
  ...merged,
@@ -2915,7 +2915,9 @@ async function createRunSession(io, options = {}) {
2915
2915
  const runtime = await (options.startModelAdapters ?? startModelAdapters)(fileSetupConfig, io);
2916
2916
  const { setupConfig } = runtime;
2917
2917
  let shutdown;
2918
+ const runner = mergeRunnerConfigs(setupConfig.runner, resolveConfigRunner(config));
2918
2919
  return {
2920
+ cache: normalizeRunnerCacheConfig(runner?.cache, io.cwd),
2919
2921
  config,
2920
2922
  cwd: io.cwd,
2921
2923
  defaultModel,
@@ -2943,7 +2945,7 @@ async function createRunSession(io, options = {}) {
2943
2945
  signal: runOptions.signal
2944
2946
  });
2945
2947
  },
2946
- runner: mergeRunnerConfigs(setupConfig.runner, resolveConfigRunner(config)),
2948
+ runner,
2947
2949
  setupConfig,
2948
2950
  shutdown: () => shutdown ??= runtime.shutdown()
2949
2951
  };
@@ -4485,10 +4487,10 @@ function shouldEnableProgress(options, io) {
4485
4487
  //#endregion
4486
4488
  //#region src/cli/commands/lsp/index.ts
4487
4489
  const lsp = defineCommand({
4488
- action: async (context) => (await import("./server-xx0w7UQL.mjs")).startLspServer(context.io),
4490
+ action: async (context) => (await import("./server-C-kuoGl1.mjs")).startLspServer(context.io),
4489
4491
  description: "Run alint as a language server over stdio",
4490
4492
  examples: [["# Point an editor at the project's own alint", "alint lsp"].join("\n")],
4491
- help: ["Serve alint diagnostics over the Language Server Protocol on stdin/stdout.", "The server is cache-first: it publishes findings that are already cached and never calls a model on its own. Runs that spend tokens happen only through the `alint.runFile` and `alint.runWorkspace` commands."].join("\n\n"),
4493
+ help: ["Serve alint diagnostics over the Language Server Protocol on stdin/stdout.", "The server is cache-first. It publishes diagnostics that are already cached, and it never calls a model on its own. Runs that spend tokens happen only through the `alint.runFile` and `alint.runWorkspace` commands."].join("\n\n"),
4492
4494
  name: "lsp",
4493
4495
  options: [{
4494
4496
  description: "Use stdio transport (default; accepted for client compatibility)",
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-cQihuHqx.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-BsBgbtds.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 };
@@ -0,0 +1,16 @@
1
+ //#region src/cli/commands/lsp/command-ids.d.ts
2
+ /**
3
+ * Command ids the language server declares in `executeCommandProvider`.
4
+ *
5
+ * They are part of the protocol surface, so any editor integration needs them, not only the VS
6
+ * Code extension in this repository. This module imports nothing, so a client can bundle it
7
+ * without pulling in the CLI.
8
+ */
9
+ /** Deletes cached diagnostics. A client asks the user before it sends this. */
10
+ declare const CLEAR_CACHE_COMMAND = "alint.clearCache";
11
+ /** Runs one file. The argument is the document URI. */
12
+ declare const RUN_FILE_COMMAND = "alint.runFile";
13
+ /** Runs every file the config matches. It takes no arguments. */
14
+ declare const RUN_WORKSPACE_COMMAND = "alint.runWorkspace";
15
+ //#endregion
16
+ export { CLEAR_CACHE_COMMAND, RUN_FILE_COMMAND, RUN_WORKSPACE_COMMAND };
@@ -0,0 +1,16 @@
1
+ //#region src/cli/commands/lsp/command-ids.ts
2
+ /**
3
+ * Command ids the language server declares in `executeCommandProvider`.
4
+ *
5
+ * They are part of the protocol surface, so any editor integration needs them, not only the VS
6
+ * Code extension in this repository. This module imports nothing, so a client can bundle it
7
+ * without pulling in the CLI.
8
+ */
9
+ /** Deletes cached diagnostics. A client asks the user before it sends this. */
10
+ const CLEAR_CACHE_COMMAND = "alint.clearCache";
11
+ /** Runs one file. The argument is the document URI. */
12
+ const RUN_FILE_COMMAND = "alint.runFile";
13
+ /** Runs every file the config matches. It takes no arguments. */
14
+ const RUN_WORKSPACE_COMMAND = "alint.runWorkspace";
15
+ //#endregion
16
+ export { CLEAR_CACHE_COMMAND, RUN_FILE_COMMAND, RUN_WORKSPACE_COMMAND };
@@ -1,5 +1,6 @@
1
- import { a as createRunSession } from "./cli-cQihuHqx.mjs";
2
- import { realpath } from "node:fs/promises";
1
+ import { a as createRunSession } from "./cli-BsBgbtds.mjs";
2
+ import { CLEAR_CACHE_COMMAND, RUN_FILE_COMMAND, RUN_WORKSPACE_COMMAND } from "./lsp-commands.mjs";
3
+ import { realpath, rm } from "node:fs/promises";
3
4
  import { AlintRunCancelledError, AlintRunError } from "@alint-js/core";
4
5
  import { errorMessageFrom } from "@moeru/std";
5
6
  import { join, relative, sep } from "node:path";
@@ -90,12 +91,51 @@ async function createFolderSession(options) {
90
91
  let session = await createRunSession(runIo);
91
92
  let diagnostics = /* @__PURE__ */ new Map();
92
93
  let scheduled;
94
+ let explicitRun = Promise.resolve();
95
+ let explicitRunning = false;
93
96
  const replaceMap = (next) => {
94
97
  const changed = /* @__PURE__ */ new Set([...next.keys(), ...diagnostics.keys()]);
95
98
  diagnostics = next;
96
99
  return [...changed];
97
100
  };
101
+ const runOnce = async (runOptions) => {
102
+ const runPaths = runOptions.inputs?.map((uri) => toRunPath(uri, paths)).filter((path) => path !== void 0);
103
+ const changed = /* @__PURE__ */ new Set();
104
+ const merge = (result) => {
105
+ for (const [uri, list] of groupByUri(result, paths)) {
106
+ diagnostics.set(uri, list);
107
+ changed.add(uri);
108
+ }
109
+ };
110
+ explicitRunning = true;
111
+ try {
112
+ merge(await session.run({
113
+ inputs: runPaths,
114
+ progress: {
115
+ onDiagnostic: ({ diagnostic }) => {
116
+ const uri = toDocumentUri(diagnostic.filePath, paths);
117
+ diagnostics.set(uri, [...diagnostics.get(uri) ?? [], toLspDiagnostic(diagnostic)]);
118
+ changed.add(uri);
119
+ },
120
+ onJobStart: ({ job, progress }) => runOptions.onProgress?.(progress, job.inputPath)
121
+ },
122
+ projectTargets: runPaths === void 0 ? void 0 : false,
123
+ runner: session.runner,
124
+ signal: runOptions.signal
125
+ }));
126
+ } catch (error) {
127
+ if (!(error instanceof AlintRunError) && !(error instanceof AlintRunCancelledError)) throw error;
128
+ merge(error.result);
129
+ } finally {
130
+ explicitRunning = false;
131
+ if (changed.size > 0) options.onChanged?.([...changed]);
132
+ }
133
+ };
98
134
  return {
135
+ clearCache: async () => {
136
+ await rm(session.cache.location, { force: true });
137
+ return replaceMap(/* @__PURE__ */ new Map());
138
+ },
99
139
  cwd,
100
140
  get diagnostics() {
101
141
  return diagnostics;
@@ -110,7 +150,7 @@ async function createFolderSession(options) {
110
150
  folderUri: options.folderUri,
111
151
  refreshFile: async (uri) => {
112
152
  const filePath = toRunPath(uri, paths);
113
- if (filePath === void 0) return [];
153
+ if (filePath === void 0 || explicitRunning) return [];
114
154
  const found = groupByUri(await runFromCache(session, { inputs: [filePath] }), paths);
115
155
  const changed = /* @__PURE__ */ new Set([...found.keys()]);
116
156
  const documentUri = toDocumentUri(filePath, paths);
@@ -119,6 +159,7 @@ async function createFolderSession(options) {
119
159
  return [...changed];
120
160
  },
121
161
  refreshFromCache: async () => {
162
+ if (explicitRunning) return;
122
163
  replaceMap(groupByUri(await runFromCache(session), paths));
123
164
  },
124
165
  reloadConfig: async () => {
@@ -126,12 +167,17 @@ async function createFolderSession(options) {
126
167
  session = await createRunSession(runIo);
127
168
  await replaced.shutdown();
128
169
  },
170
+ runExplicit: (runOptions) => {
171
+ explicitRun = explicitRun.catch(() => {}).then(() => runOnce(runOptions));
172
+ return explicitRun;
173
+ },
129
174
  scheduleWorkspacePass: () => {
130
175
  if (scheduled !== void 0) return;
131
176
  scheduled = setTimeout(() => {
132
177
  scheduled = void 0;
133
178
  (async () => {
134
179
  try {
180
+ if (explicitRunning) return;
135
181
  options.onChanged?.(replaceMap(groupByUri(await runFromCache(session), paths)));
136
182
  } catch (error) {
137
183
  options.io.stderr.write(`alint lsp: workspace pass failed: ${errorMessageFrom(error) ?? "unknown error"}\n`);
@@ -197,9 +243,9 @@ function toRunPath(uri, paths) {
197
243
  /**
198
244
  * Collects document URIs and publishes each one once per window, open documents first.
199
245
  *
200
- * An LSP publish carries a whole document, so one publish per finding would send the same document
201
- * many times. A workspace pass can queue thousands of documents, and the open one must not wait
202
- * for the rest.
246
+ * An LSP publish contains a whole document, so one publish per diagnostic would send the same
247
+ * document many times. A workspace pass can queue thousands of documents, and the open one must
248
+ * not wait for the rest.
203
249
  */
204
250
  function createPublisher(options) {
205
251
  const open = /* @__PURE__ */ new Set();
@@ -238,9 +284,6 @@ function createPublisher(options) {
238
284
  //#region src/cli/commands/lsp/server.ts
239
285
  /** Long enough to collect a workspace pass into few messages, short enough to keep a save prompt. */
240
286
  const PUBLISH_FLUSH_MS = 200;
241
- /** Two commands, because a workspace run can cost orders of magnitude more than one file. */
242
- const RUN_FILE_COMMAND = "alint.runFile";
243
- const RUN_WORKSPACE_COMMAND = "alint.runWorkspace";
244
287
  async function startLspServer(io) {
245
288
  const { stdin } = io;
246
289
  if (!stdin) {
@@ -282,7 +325,11 @@ async function startLspServer(io) {
282
325
  watchesConfig = params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
283
326
  return { capabilities: {
284
327
  executeCommandProvider: {
285
- commands: [RUN_FILE_COMMAND, RUN_WORKSPACE_COMMAND],
328
+ commands: [
329
+ CLEAR_CACHE_COMMAND,
330
+ RUN_FILE_COMMAND,
331
+ RUN_WORKSPACE_COMMAND
332
+ ],
286
333
  workDoneProgress: true
287
334
  },
288
335
  textDocumentSync: {
@@ -302,7 +349,7 @@ async function startLspServer(io) {
302
349
  try {
303
350
  await openFolder(folderUri);
304
351
  } catch (error) {
305
- connection.console.error(`alint: ${folderUri}: ${errorMessageFrom(error) ?? "could not read cached diagnostics"}`);
352
+ connection.console.error(`${folderUri}: ${errorMessageFrom(error) ?? "could not read cached diagnostics"}`);
306
353
  }
307
354
  }));
308
355
  });
@@ -311,7 +358,7 @@ async function startLspServer(io) {
311
358
  try {
312
359
  for (const uri of await folder.refreshFile(textDocument.uri)) publisher.queue(uri);
313
360
  } catch (error) {
314
- connection.console.error(`alint: ${textDocument.uri}: ${errorMessageFrom(error) ?? "could not refresh the saved file"}`);
361
+ connection.console.error(`${textDocument.uri}: ${errorMessageFrom(error) ?? "could not refresh the saved file"}`);
315
362
  }
316
363
  }));
317
364
  });
@@ -322,16 +369,55 @@ async function startLspServer(io) {
322
369
  connection.onDidCloseTextDocument(({ textDocument }) => {
323
370
  publisher.setOpen(textDocument.uri, false);
324
371
  });
372
+ connection.onExecuteCommand(async ({ arguments: args, command }) => {
373
+ if (command === "alint.clearCache") {
374
+ await Promise.all([...folders.values()].map(async (folder) => {
375
+ try {
376
+ for (const uri of await folder.clearCache()) publisher.queue(uri);
377
+ } catch (error) {
378
+ connection.console.error(`${folder.folderUri}: ${errorMessageFrom(error) ?? "could not clear the cache"}`);
379
+ }
380
+ }));
381
+ return;
382
+ }
383
+ const inputs = command === "alint.runFile" ? args?.filter((value) => typeof value === "string") : void 0;
384
+ if (command === "alint.runFile" && (inputs === void 0 || inputs.length === 0)) {
385
+ connection.console.error(`${RUN_FILE_COMMAND} needs a document URI.`);
386
+ return;
387
+ }
388
+ const reporter = await connection.window.createWorkDoneProgress();
389
+ const controller = new AbortController();
390
+ reporter.begin(inputs === void 0 ? "Running alint on the workspace" : "Running alint on the current file", 0, void 0, true);
391
+ reporter.token.onCancellationRequested(() => controller.abort());
392
+ try {
393
+ await Promise.all([...folders.values()].map(async (folder) => {
394
+ try {
395
+ await folder.runExplicit({
396
+ inputs,
397
+ onProgress: (progress, inputPath) => {
398
+ const percentage = progress.jobsTotal === 0 ? 0 : Math.round(progress.jobsCompleted / progress.jobsTotal * 100);
399
+ reporter.report(percentage, inputPath);
400
+ },
401
+ signal: controller.signal
402
+ });
403
+ } catch (error) {
404
+ connection.console.error(`${folder.folderUri}: ${errorMessageFrom(error) ?? "the run failed"}`);
405
+ }
406
+ }));
407
+ } finally {
408
+ reporter.done();
409
+ }
410
+ });
325
411
  connection.onDidChangeWatchedFiles(() => {
326
412
  Promise.all([...folders.values()].map(async (folder) => {
327
413
  try {
328
414
  await folder.reloadConfig();
329
415
  folder.scheduleWorkspacePass();
330
416
  } catch (error) {
331
- connection.console.error(`alint: ${folder.folderUri}: ${errorMessageFrom(error) ?? "could not reload the config"}`);
417
+ connection.console.error(`${folder.folderUri}: ${errorMessageFrom(error) ?? "could not reload the config"}`);
332
418
  }
333
419
  }));
334
- connection.console.info("alint: config changed; cached diagnostics were cleared and need a new run");
420
+ connection.console.info("Configuration changed. Cached diagnostics were cleared. Run alint again to recreate them.");
335
421
  });
336
422
  connection.onShutdown(async () => {
337
423
  publisher.dispose();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "Agentic code analysis CLI for model-backed lint rules",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -22,6 +22,10 @@
22
22
  "types": "./dist/index.d.mts",
23
23
  "default": "./dist/index.mjs"
24
24
  },
25
+ "./lsp-commands": {
26
+ "types": "./dist/lsp-commands.d.mts",
27
+ "default": "./dist/lsp-commands.mjs"
28
+ },
25
29
  "./package.json": "./package.json"
26
30
  },
27
31
  "bin": {
@@ -46,10 +50,10 @@
46
50
  "tinyexec": "^1.2.4",
47
51
  "tinyrainbow": "^3.1.1",
48
52
  "vscode-languageserver": "^10.1.0",
49
- "@alint-js/config": "0.6.0",
50
- "@alint-js/core": "0.6.0",
51
- "@alint-js/utils": "0.6.0",
52
- "@alint-js/model-adapter-acp": "0.6.0"
53
+ "@alint-js/model-adapter-acp": "0.7.0",
54
+ "@alint-js/core": "0.7.0",
55
+ "@alint-js/config": "0.7.0",
56
+ "@alint-js/utils": "0.7.0"
53
57
  },
54
58
  "devDependencies": {
55
59
  "@pnpm/find-workspace-dir": "^1000.1.5",