@alint-js/cli 0.5.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as formatJson, i as formatStylish, r as formatDiagnostics, t as executeCli } from "./cli-ChymU1Wr.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,8 +1,9 @@
1
- import { n as createRunSession } from "./cli-ChymU1Wr.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
- import { join, relative, sep } from "node:path";
5
5
  import { errorMessageFrom } from "@moeru/std";
6
+ import { join, relative, sep } from "node:path";
6
7
  import { Writable } from "node:stream";
7
8
  import { DidChangeWatchedFilesNotification } from "vscode-languageserver";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -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,8 +150,8 @@ 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 [];
114
- const found = groupByUri(await runFromCache(session, { files: [filePath] }), paths);
153
+ if (filePath === void 0 || explicitRunning) return [];
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);
117
157
  if (!found.has(documentUri) && diagnostics.delete(documentUri)) changed.add(documentUri);
@@ -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`);
@@ -159,8 +205,8 @@ async function runFromCache(session, options = {}) {
159
205
  try {
160
206
  return await session.run({
161
207
  cacheOnly: true,
162
- files: options.files,
163
- projectTargets: options.files === void 0 ? void 0 : false,
208
+ inputs: options.inputs,
209
+ projectTargets: options.inputs === void 0 ? void 0 : false,
164
210
  runner: {
165
211
  ...session.runner,
166
212
  stats: false
@@ -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,12 +1,31 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
+ "description": "Agentic code analysis CLI for model-backed lint rules",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/moeru-ai/alint.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "keywords": [
13
+ "alint",
14
+ "lint",
15
+ "linter",
16
+ "ai",
17
+ "code-analysis",
18
+ "cli"
19
+ ],
5
20
  "exports": {
6
21
  ".": {
7
22
  "types": "./dist/index.d.mts",
8
23
  "default": "./dist/index.mjs"
9
24
  },
25
+ "./lsp-commands": {
26
+ "types": "./dist/lsp-commands.d.mts",
27
+ "default": "./dist/lsp-commands.mjs"
28
+ },
10
29
  "./package.json": "./package.json"
11
30
  },
12
31
  "bin": {
@@ -21,23 +40,25 @@
21
40
  "cac": "^7.0.0",
22
41
  "cli-spinners": "^3.4.0",
23
42
  "date-fns": "^4.4.0",
43
+ "diff": "^9.0.0",
24
44
  "fast-string-truncated-width": "^3.0.3",
25
45
  "fast-string-width": "^3.0.2",
26
46
  "gitignore-fs": "^2.2.3",
27
47
  "minimatch": "^10.2.6",
28
48
  "pathe": "^2.0.3",
29
49
  "table": "^6.9.0",
50
+ "tinyexec": "^1.2.4",
30
51
  "tinyrainbow": "^3.1.1",
31
52
  "vscode-languageserver": "^10.1.0",
32
- "@alint-js/config": "0.5.0",
33
- "@alint-js/core": "0.5.0",
34
- "@alint-js/model-adapter-acp": "0.5.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"
35
57
  },
36
58
  "devDependencies": {
37
59
  "@pnpm/find-workspace-dir": "^1000.1.5",
38
60
  "local-pkg": "^1.2.1",
39
- "mlly": "^1.8.2",
40
- "tinyexec": "^1.2.4"
61
+ "mlly": "^1.8.2"
41
62
  },
42
63
  "scripts": {
43
64
  "alint": "node ./dist/bin/index.mjs",