@alint-js/cli 0.7.0 → 0.7.2

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
@@ -189,6 +189,18 @@ Ask model-backed rules to write diagnostics in a specific language:
189
189
  alint --lang zh-CN src
190
190
  ```
191
191
 
192
+ #### --rule
193
+
194
+ Run only the rules a filter names, to read one rule's findings without paying for every other rule in the run:
195
+
196
+ ```bash
197
+ alint --rule docs/review-copy src
198
+ alint --rule 'docs/*' src
199
+ alint --rule docs/review-copy,docs/naming src
200
+ ```
201
+
202
+ A pattern matches the configured rule id (`plugin/rule`) or the rule's own name. The flag is repeatable and accepts a comma-separated list. Filtered rules are left unplanned, so the run neither calls their models nor reports their diagnostics. When the filter matches no enabled rule, the run fails and lists the rule ids it did enable, because a silent empty run is indistinguishable from a clean one.
203
+
192
204
  `alint` returns exit code `0` when diagnostics contain no errors, including warning-only runs. It returns `1` when at least one error diagnostic is reported and `2` when the command cannot complete because of a configuration, input, or runtime failure. `alint output inspect` uses the same exit-code behavior for saved results.
193
205
 
194
206
  ### Inspect Configuration and Output
@@ -421,6 +433,31 @@ export default defineConfig([
421
433
  ])
422
434
  ```
423
435
 
436
+ #### Nested configs
437
+
438
+ Every `alint.config.*` below the repository root is loaded together with the root config, and each file is scoped to the directory that contains it:
439
+
440
+ ```ts
441
+ // packages/app/alint.config.ts
442
+ import { defineConfig } from '@alint-js/cli'
443
+
444
+ import { appPlugin } from './alint/plugin'
445
+
446
+ export default defineConfig([
447
+ {
448
+ plugins: { app: appPlugin },
449
+ rules: { 'app/layer-boundary': 'error' },
450
+ },
451
+ ])
452
+ ```
453
+
454
+ The item above reaches `packages/app/**` only. `files`, `directories`, and `ignores` resolve against the directory of the config that declares them, an item that declares no target patterns covers its whole subtree, and a deeper config overrides an outer one for its own directory. A package that owns a rule therefore needs one file, not an entry in the root config plus the same scope repeated in every tool that reads it.
455
+
456
+ - Discovery skips hidden directories, `node_modules`, and build output (`build`, `coverage`, `dist`, `out`, `vendor`).
457
+ - `--config <path>` pins exactly one config file and disables discovery, so a CI job can run one specific config.
458
+ - Every config file shares one plugin alias namespace and one plugin lockfile, and `alint plugin install` installs the plugins nested configs declare. Two files that bind one alias to different specifiers fail the load instead of resolving silently to one of them.
459
+ - Keep project-scoped rules (`onTargetProject`) in the root config: the project target sits outside a nested item's base path, so a nested config cannot claim it.
460
+
424
461
  #### Executable and static configs
425
462
 
426
463
  `alint` supports executable configs (`.js`, `.ts`, `.mjs`, `.cjs`, `.mts`, and `.cts`) and data-only static configs (`.toml`, `.yaml`, `.yml`, `.json`, `.jsonc`, and `.json5`).
@@ -488,6 +525,8 @@ arch = "./rules/architecture"
488
525
 
489
526
  `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.
490
527
 
528
+ Editing a rule's `instruction`, `includeFiles`, or `excludeFiles` invalidates the findings it cached: a declarative rule declares those inputs as its cache key, so a changed rule text never replays stale findings.
529
+
491
530
  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.
492
531
 
493
532
  Local plugins execute as trusted Node.js code. Directory containment checks validate the installed source; they are not a sandbox.
@@ -651,6 +690,19 @@ defineRule({
651
690
  })
652
691
  ```
653
692
 
693
+ Rules whose findings depend on inputs their source does not show must declare those inputs as `cacheKey`: an imported prompt, a shared message builder, or the text of a preset that builds the rule. `alint` hashes the rule source and `cacheKey` into every cache entry:
694
+
695
+ ```ts
696
+ import prompt from './prompt.md?raw'
697
+
698
+ defineRule({
699
+ cacheKey: { agentVersion: 1, prompt },
700
+ create: ctx => ({ /* ... */ }),
701
+ })
702
+ ```
703
+
704
+ Without a cache key, editing the prompt changes what the rule asks the model while the cache entry stays valid, and the next run replays the previous findings as cache hits without running the rule at all.
705
+
654
706
  ## Packages
655
707
 
656
708
  | Package | Purpose |
@@ -1,17 +1,54 @@
1
1
  #!/usr/bin/env node
2
- import { t as executeCli } from "../cli-BsBgbtds.mjs";
2
+ import { t as executeCli } from "../cli-_dmEoPTZ.mjs";
3
3
  import process from "node:process";
4
+ //#region src/cli/runtime/interrupt.ts
5
+ /** Binds one graceful process interrupt to an abort signal for the active CLI run. */
6
+ function bindInterruptSignal(source) {
7
+ const controller = new AbortController();
8
+ /**
9
+ * Aborts active CLI work when the process receives its first interrupt signal.
10
+ *
11
+ * Triggering workflow:
12
+ *
13
+ * {@link bindInterruptSignal}
14
+ * -> `InterruptSignalSource.once('SIGINT' | 'SIGTERM')`
15
+ * -> `handleInterrupt`
16
+ * -> `AbortController.abort()`
17
+ *
18
+ * Upstream:
19
+ * - {@link bindInterruptSignal} registers this handler for `SIGINT` and `SIGTERM`.
20
+ *
21
+ * Downstream:
22
+ * - Aborts {@link InterruptSignalBinding.signal}, which the lint command passes to `runAlint`.
23
+ */
24
+ const handleInterrupt = () => {
25
+ controller.abort();
26
+ };
27
+ source.once("SIGINT", handleInterrupt);
28
+ source.once("SIGTERM", handleInterrupt);
29
+ return {
30
+ dispose: () => {
31
+ source.off("SIGINT", handleInterrupt);
32
+ source.off("SIGTERM", handleInterrupt);
33
+ },
34
+ signal: controller.signal
35
+ };
36
+ }
37
+ //#endregion
4
38
  //#region src/bin/index.ts
39
+ const interrupt = bindInterruptSignal(process);
5
40
  executeCli(process.argv, {
6
41
  cwd: process.cwd(),
7
42
  stderr: process.stderr,
8
43
  stdin: process.stdin,
9
44
  stdout: process.stdout
10
- }).then((exitCode) => {
45
+ }, { signal: interrupt.signal }).then((exitCode) => {
11
46
  process.exitCode = exitCode;
12
47
  }).catch((error) => {
13
48
  process.stderr.write(`${formatError(error)}\n`);
14
49
  process.exitCode = 2;
50
+ }).finally(() => {
51
+ interrupt.dispose();
15
52
  });
16
53
  function formatError(error) {
17
54
  if (error instanceof Error) return error.message;
@@ -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.7.0";
27
+ var version = "0.7.2";
28
28
  //#endregion
29
29
  //#region src/cli/output.ts
30
30
  function escapeLineValue(value) {
@@ -2940,6 +2940,7 @@ async function createRunSession(io, options = {}) {
2940
2940
  outputLanguage: runOptions.outputLanguage,
2941
2941
  progress: runOptions.progress,
2942
2942
  projectTargets: runOptions.projectTargets,
2943
+ ruleFilter: runOptions.ruleFilter,
2943
2944
  runner: runOptions.runner,
2944
2945
  setupConfig,
2945
2946
  signal: runOptions.signal
@@ -3497,8 +3498,9 @@ async function executeLint(options) {
3497
3498
  modelOverride: options.modelOverride,
3498
3499
  outputLanguage: options.outputLanguage,
3499
3500
  progress: mergeProgressReporters(options.progress, statsCollector?.reporter),
3501
+ ruleFilter: options.ruleFilter,
3500
3502
  runner,
3501
- signal: options.createSignal?.()
3503
+ signal: options.signal ?? options.createSignal?.()
3502
3504
  });
3503
3505
  } catch (error) {
3504
3506
  if (error instanceof AlintRunError || error instanceof AlintRunCancelledError || error instanceof AlintCachePersistenceError) await persistStats(error.result);
@@ -4253,55 +4255,29 @@ function createCliProgressReporter(options) {
4253
4255
  };
4254
4256
  }
4255
4257
  function createRenderingProgressReporter(summary, renderer) {
4258
+ /**
4259
+ * Resets the summary and starts its bounded TTY render interval.
4260
+ *
4261
+ * Triggering workflow:
4262
+ *
4263
+ * `runAlint`
4264
+ * -> `ProgressReporter.onPrepareStart`
4265
+ * -> `handlePrepareStart`
4266
+ * -> `TtyProgressRenderer.start`
4267
+ *
4268
+ * Upstream:
4269
+ * - `runAlint` emits `onPrepareStart` before source discovery.
4270
+ *
4271
+ * Downstream:
4272
+ * - Delegates state reset to `summary.onPrepareStart` and starts the renderer interval.
4273
+ */
4274
+ const handlePrepareStart = (payload) => {
4275
+ summary.onPrepareStart?.(payload);
4276
+ renderer.start();
4277
+ };
4256
4278
  return {
4257
- onDiagnostic: (payload) => {
4258
- summary.onDiagnostic?.(payload);
4259
- renderer.render();
4260
- },
4261
- onExecuteEnd: (payload) => {
4262
- summary.onExecuteEnd?.(payload);
4263
- renderer.render();
4264
- },
4265
- onExecuteStart: (payload) => {
4266
- summary.onExecuteStart?.(payload);
4267
- renderer.render();
4268
- },
4269
- onFileReady: (payload) => {
4270
- summary.onFileReady?.(payload);
4271
- renderer.render();
4272
- },
4273
- onJobEnd: (payload) => {
4274
- summary.onJobEnd?.(payload);
4275
- renderer.render();
4276
- },
4277
- onJobQueued: (payload) => {
4278
- summary.onJobQueued?.(payload);
4279
- renderer.render();
4280
- },
4281
- onJobRetry: (payload) => {
4282
- summary.onJobRetry?.(payload);
4283
- renderer.render();
4284
- },
4285
- onJobStart: (payload) => {
4286
- summary.onJobStart?.(payload);
4287
- renderer.render();
4288
- },
4289
- onPrepareEnd: (payload) => {
4290
- summary.onPrepareEnd?.(payload);
4291
- renderer.render();
4292
- },
4293
- onPrepareStart: (payload) => {
4294
- summary.onPrepareStart?.(payload);
4295
- renderer.start();
4296
- },
4297
- onRunEnd: (payload) => {
4298
- summary.onRunEnd?.(payload);
4299
- renderer.render();
4300
- },
4301
- onUsage: (payload) => {
4302
- summary.onUsage?.(payload);
4303
- renderer.render();
4304
- }
4279
+ ...summary,
4280
+ onPrepareStart: handlePrepareStart
4305
4281
  };
4306
4282
  }
4307
4283
  //#endregion
@@ -4371,12 +4347,26 @@ function groupFailuresByRule(failures) {
4371
4347
  return [...groups];
4372
4348
  }
4373
4349
  //#endregion
4350
+ //#region src/cli/commands/lint/rule-filter.ts
4351
+ /**
4352
+ * Reads `--rule` into the patterns the run filters rules with.
4353
+ *
4354
+ * The flag accepts one pattern, a comma-separated list, or repeated `--rule` flags, because a
4355
+ * debugging session usually narrows to one rule but sometimes compares a plugin's rules.
4356
+ */
4357
+ function resolveRuleFilter(value) {
4358
+ if (value === void 0) return;
4359
+ const patterns = (Array.isArray(value) ? value : [value]).flatMap((entry) => entry.split(",")).map((entry) => entry.trim()).filter((entry) => entry !== "");
4360
+ if (patterns.length === 0) throw new Error("--rule requires a rule id, a rule name, or a glob pattern.");
4361
+ return [...new Set(patterns)];
4362
+ }
4363
+ //#endregion
4374
4364
  //#region src/cli/commands/lint/index.ts
4375
4365
  const lint = defineCommand({
4376
4366
  action: (context, files = [], options) => runLintCommand(files, {
4377
4367
  ...options,
4378
4368
  outputLanguage: options.lang ?? context.globalOptions.outputLanguage
4379
- }, context.io, context.interceptConsoleOutput),
4369
+ }, context.io, context.interceptConsoleOutput, context.signal),
4380
4370
  alias: ["!"],
4381
4371
  arguments: "[...files]",
4382
4372
  default: true,
@@ -4396,7 +4386,7 @@ async function assertConfigExists(cwd, configPath) {
4396
4386
  throw error;
4397
4387
  }
4398
4388
  }
4399
- async function runLintCommand(files, options, io, interceptConsoleOutput) {
4389
+ async function runLintCommand(files, options, io, interceptConsoleOutput, signal) {
4400
4390
  if (options.dirty && files.length > 0) {
4401
4391
  io.stderr.write("The --dirty option does not accept file arguments.\n");
4402
4392
  return 2;
@@ -4445,8 +4435,10 @@ async function runLintCommand(files, options, io, interceptConsoleOutput) {
4445
4435
  modelOverride: options.model,
4446
4436
  outputLanguage: options.outputLanguage,
4447
4437
  progress: progress?.reporter,
4438
+ ruleFilter: resolveRuleFilter(options.rule),
4448
4439
  runnerOptions: options,
4449
- session
4440
+ session,
4441
+ signal
4450
4442
  });
4451
4443
  } catch (error) {
4452
4444
  if (error instanceof NoFilesFoundError) {
@@ -4487,7 +4479,7 @@ function shouldEnableProgress(options, io) {
4487
4479
  //#endregion
4488
4480
  //#region src/cli/commands/lsp/index.ts
4489
4481
  const lsp = defineCommand({
4490
- action: async (context) => (await import("./server-C-kuoGl1.mjs")).startLspServer(context.io),
4482
+ action: async (context) => (await import("./server-Dxaq_DPo.mjs")).startLspServer(context.io),
4491
4483
  description: "Run alint as a language server over stdio",
4492
4484
  examples: [["# Point an editor at the project's own alint", "alint lsp"].join("\n")],
4493
4485
  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"),
@@ -5163,7 +5155,7 @@ const commandTree = [
5163
5155
  ];
5164
5156
  //#endregion
5165
5157
  //#region src/cli/cli.ts
5166
- async function executeCli(argv, io) {
5158
+ async function executeCli(argv, io, runtime = {}) {
5167
5159
  if (argv.includes("--version") || argv.includes("-v")) {
5168
5160
  io.stdout.write(`${version}\n`);
5169
5161
  return 0;
@@ -5176,12 +5168,13 @@ async function executeCli(argv, io) {
5176
5168
  pendingResult = result;
5177
5169
  return result;
5178
5170
  };
5179
- cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--cache-only", "Report only cached diagnostics; skip rules that miss the cache and call no model").option("-c, --config <path>", "Path to alint config file").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>", "Maximum rule executions across the entire run").option("--no-stats", "Do not record run stats for this run").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").version(version).help();
5171
+ cli.option("--no-cache", "Disable cache for this run").option("--cache-location <path>", "Path to the alint cache file or directory").option("--cache-only", "Report only cached diagnostics; skip rules that miss the cache and call no model").option("-c, --config <path>", "Path to alint config file").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 <pattern>", "Only run rules whose id, name, or glob matches (repeatable, comma-separated)").option("--rule-concurrency <count>", "Maximum rule executions across the entire run").option("--no-stats", "Do not record run stats for this run").option("--timeout-ms <ms>", "Rule execution timeout in milliseconds").version(version).help();
5180
5172
  registerCommandTree(cli, commandTree, {
5181
5173
  globalOptions,
5182
5174
  interceptConsoleOutput,
5183
5175
  io,
5184
- setupNoInteractive
5176
+ setupNoInteractive,
5177
+ signal: runtime.signal
5185
5178
  }, setPendingResult, {
5186
5179
  examples: [
5187
5180
  ["# Configure a provider interactively", "alint setup"].join("\n"),
package/dist/index.d.mts CHANGED
@@ -27,7 +27,10 @@ interface CliWritable {
27
27
  }
28
28
  //#endregion
29
29
  //#region src/cli/cli.d.ts
30
- declare function executeCli(argv: string[], io: CliIo): Promise<number>;
30
+ interface CliRuntimeOptions {
31
+ signal?: AbortSignal;
32
+ }
33
+ declare function executeCli(argv: string[], io: CliIo, runtime?: CliRuntimeOptions): Promise<number>;
31
34
  //#endregion
32
35
  //#region src/cli/reporters/index.d.ts
33
36
  interface FormatDiagnosticsOptions {
@@ -45,4 +48,4 @@ interface StylishReporterOptions {
45
48
  }
46
49
  declare function formatStylish(input: Diagnostic[] | RunResult, options?: StylishReporterOptions): string;
47
50
  //#endregion
48
- export { type AlintConfig, type AlintConfigItem, type CliIo, type ReporterName, type RunnerConfig, defineConfig, executeCli, formatDiagnostics, formatJson, formatStylish, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated };
51
+ export { type AlintConfig, type AlintConfigItem, type CliIo, type CliRuntimeOptions, type ReporterName, type RunnerConfig, defineConfig, executeCli, formatDiagnostics, formatJson, formatStylish, ignorePatternsAIAgents, ignorePatternsBuildOutputs, ignorePatternsCaches, ignorePatternsCommon, ignorePatternsEslintDefaults, ignorePatternsGenerated };
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-BsBgbtds.mjs";
1
+ import { i as formatJson, n as formatDiagnostics, r as formatStylish, t as executeCli } from "./cli-_dmEoPTZ.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 };
@@ -1,4 +1,4 @@
1
- import { a as createRunSession } from "./cli-BsBgbtds.mjs";
1
+ import { a as createRunSession } from "./cli-_dmEoPTZ.mjs";
2
2
  import { CLEAR_CACHE_COMMAND, RUN_FILE_COMMAND, RUN_WORKSPACE_COMMAND } from "./lsp-commands.mjs";
3
3
  import { realpath, rm } from "node:fs/promises";
4
4
  import { AlintRunCancelledError, AlintRunError } from "@alint-js/core";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alint-js/cli",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.7.2",
5
5
  "description": "Agentic code analysis CLI for model-backed lint rules",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -50,10 +50,10 @@
50
50
  "tinyexec": "^1.2.4",
51
51
  "tinyrainbow": "^3.1.1",
52
52
  "vscode-languageserver": "^10.1.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
+ "@alint-js/config": "0.7.2",
54
+ "@alint-js/core": "0.7.2",
55
+ "@alint-js/model-adapter-acp": "0.7.2",
56
+ "@alint-js/utils": "0.7.2"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@pnpm/find-workspace-dir": "^1000.1.5",