@isentinel/eslint-config 6.0.0-beta.45 → 6.0.0-beta.46

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
@@ -255,8 +255,8 @@ Other behaviours:
255
255
  (see `FAST_FILES_PER_WORKER`) than the type-aware pass.
256
256
  - Keeps a separate ESLint cache per pass so they never invalidate one another:
257
257
  `.eslintcache-fast-<key>` (fast pass), `.eslintcache-typeaware-<key>`
258
- (type-aware pass), and `.eslintcache-<key>` (the full single-pass config used
259
- by `--fix`, CI and `--type-aware=full`). A change to an
258
+ (type-aware pass), and `.eslintcache-<key>` (the full config, used by CI,
259
+ `--type-aware=full` and the fix child below). A change to an
260
260
  ESLint/oxlint/Prettier/tsconfig file or a lockfile clears each cache that is
261
261
  actually older than that change; a change to the root `package.json`
262
262
  resolution surface (`exports`, `imports`, `main`, `module`, `types`,
@@ -281,16 +281,46 @@ Other behaviours:
281
281
  value naming that branch. It is folded into the key, giving those configs
282
282
  separate caches too.
283
283
 
284
- - Runs `--fix` sequentially (`oxlint --fix`, then `eslint --fix`) so two writers
285
- never race on the same files.
284
+ - Treats `--fix` as a check followed by a fix, in three steps. `oxlint --fix`
285
+ goes first and alone, since it rewrites the files the ESLint children are
286
+ about to read. The run's ESLint passes then run as ordinary **checks** —
287
+ concurrent, own caches, no writes. By default those are the fast and
288
+ type-aware passes — the single full pass in CI; an explicit `--type-aware`
289
+ checks whichever passes that mode selects instead (`full` the single full
290
+ pass, `only` the type-aware pass alone, `off` the fast pass alone). Finally
291
+ **one** ESLint child applies fixes to **only the files those checks had
292
+ messages about**, using the full config (a superset of every pass, so it can
293
+ fix anything any of them found).
294
+
295
+ When the checks report nothing, which is the common case, that last child
296
+ never spawns and `--fix` costs what a check run costs. Being the run's only
297
+ ESLint writer, it also cannot race anything.
298
+
299
+ The file list comes from the check passes' own caches: ESLint stores each
300
+ file's result beside its cache entry and replays it on a cache hit, so a warm
301
+ cache is that pass's whole-tree verdict — including for a pass that
302
+ auto-skipped, whose verdict stands precisely because nothing it cares about
303
+ changed. `--no-cache` leaves no verdict to read, so the fix child falls back
304
+ to linting everything it was given. A target outside the working directory is
305
+ invisible to the listing the verdict is read against, so it is handed to the
306
+ fix child as it stands, alongside the reported files; that child runs without
307
+ the cache, since a stale entry for a file no verdict covers would let ESLint
308
+ skip it.
309
+
286
310
  - Lets every child run to completion and returns non-zero if any failed — an
287
311
  ordinary lint error in one tool no longer kills the others mid-run.
288
312
 
289
313
  > **Unused eslint-disable directives.** The default two-pass mode does **not**
290
314
  > report unused `eslint-disable` directives: a directive naming a rule that
291
315
  > lives in the _other_ pass would look unused to the pass that runs it and
292
- > produce a false positive. `--fix`, CI and `--type-aware=full` runs use the
293
- > single full config and still report unused directives.
316
+ > produce a false positive. CI and `--type-aware=full` runs use the single full
317
+ > config and still report them.
318
+ >
319
+ > A local `--fix` inherits this from its checks. The fix child runs the full
320
+ > config and would report an unused directive, but it only ever sees the files
321
+ > the checks reported, and a file whose _only_ problem is an unused directive is
322
+ > not one of them — so those directives survive a local fix run. In CI the check
323
+ > is the full config, so they are reported and removed as before.
294
324
 
295
325
  ### Hybrid detection
296
326
 
@@ -317,7 +347,7 @@ Any trailing positional arguments are treated as paths to lint (default `.`).
317
347
  | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
318
348
  | `--eslint` | Run only ESLint. |
319
349
  | `--oxlint` | Run only oxlint. |
320
- | `--fix` | Apply fixes: `oxlint --fix` then `eslint --fix` (sequential). |
350
+ | `--fix` | Check as usual, then fix only the files the checks reported (one child). |
321
351
  | `--agents`, `--no-agents` | Force agent-friendly output from both linters on or off (default: on in an agent session). |
322
352
  | `--type-aware=off\|only\|full` | Force a single ESLint pass: `off` fast-only, `only` type-aware-only, `full` the whole config in one pass. Cannot mix with `--fix`. |
323
353
  | `--no-oxlint-type-aware` | Skip oxlint's type-aware rules (no `oxlint-tsgolint` needed). |
package/dist/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { existsSync, readFileSync } from "fs";
10
10
  import { execSync } from "node:child_process";
11
11
  //#region package.json
12
- var version = "6.0.0-beta.45";
12
+ var version = "6.0.0-beta.46";
13
13
  var package_default = {
14
14
  name: "@isentinel/eslint-config",
15
15
  version,
@@ -175,7 +175,7 @@ var package_default = {
175
175
  },
176
176
  peerDependencies: {
177
177
  "@vitest/eslint-plugin": "^1.6.4",
178
- "eslint": "^10.4.0",
178
+ "eslint": "^10.10.0",
179
179
  "eslint-plugin-eslint-plugin": "^7.5.0",
180
180
  "eslint-plugin-jest": "^29.15.0",
181
181
  "eslint-plugin-jest-extended": "^3.0.0",
@@ -534,7 +534,7 @@ async function updateEslintFiles(result) {
534
534
  //#endregion
535
535
  //#region src/cli/constants-generated.ts
536
536
  const versionsMap = {
537
- "eslint": "10.8.1",
537
+ "eslint": "10.10.0",
538
538
  "eslint-plugin-jest": "29.16.0",
539
539
  "eslint-plugin-react-jsx": "5.17.3",
540
540
  "eslint-plugin-react-naming-convention": "5.17.3",
package/dist/index.d.mts CHANGED
@@ -1510,7 +1510,9 @@ interface MainRuleDefaults {
1510
1510
  "*": "off";
1511
1511
  };
1512
1512
  "no-unmodified-loop-condition": {
1513
- "*": "error";
1513
+ "*": ["error", {
1514
+ "checkConditionalExpressions": false;
1515
+ }];
1514
1516
  };
1515
1517
  "no-unneeded-ternary": {
1516
1518
  "*": ["error", {
@@ -6498,7 +6500,7 @@ interface OverrideSiteChains {
6498
6500
  type TestChain = [TestRuleDefaults, SourceFeatureRuleDefaults, MainRuleDefaults];
6499
6501
  type ValidateOptionsWith<O, VK extends VariantKey> = ValidateSite<O, "rules", MainChain, VK> & ValidateSubOptionsFromTable<O, OverrideSiteChains, VK> & ValidateTestOption<O, TestChain, VK>;
6500
6502
  //#endregion
6501
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/0a594adbd94ca75e2950b1104992894d49bc93f4f2bc935ad192d7104e4c271e/node_modules/eslint-config-flat-gitignore/dist/index.d.mts
6503
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/48bee61dfbd6416cb227d1c477eb8990bf234fa634e4ccd62a9520a90cc26602/node_modules/eslint-config-flat-gitignore/dist/index.d.mts
6502
6504
  interface FlatGitignoreOptions {
6503
6505
  /**
6504
6506
  * Name of the configuration.
@@ -9343,7 +9345,7 @@ interface RuleOptions {
9343
9345
  * Disallow unmodified loop conditions
9344
9346
  * @see https://eslint.org/docs/latest/rules/no-unmodified-loop-condition
9345
9347
  */
9346
- 'no-unmodified-loop-condition'?: Linter.RuleEntry<[]>;
9348
+ 'no-unmodified-loop-condition'?: Linter.RuleEntry<NoUnmodifiedLoopCondition>;
9347
9349
  /**
9348
9350
  * Disallow ternary operators when simpler alternatives exist
9349
9351
  * @see https://eslint.org/docs/latest/rules/no-unneeded-ternary
@@ -19662,6 +19664,10 @@ type NoUnderscoreDangle = [] | [{
19662
19664
  allowInArrayDestructuring?: boolean;
19663
19665
  allowInObjectDestructuring?: boolean;
19664
19666
  }];
19667
+ // ----- no-unmodified-loop-condition -----
19668
+ type NoUnmodifiedLoopCondition = [] | [{
19669
+ checkConditionalExpressions?: boolean;
19670
+ }];
19665
19671
  // ----- no-unneeded-ternary -----
19666
19672
  type NoUnneededTernary = [] | [{
19667
19673
  defaultAssignment?: boolean;
package/dist/index.mjs CHANGED
@@ -1889,7 +1889,7 @@ async function flawless(options = {}, prettierOptions = {}) {
1889
1889
  ];
1890
1890
  }
1891
1891
  //#endregion
1892
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@eslint/compat/2.1.0/90f1da88f603538864cab10657b202d55290237a3b641df9ac72b842a3be07f2/node_modules/@eslint/compat/dist/esm/index.js
1892
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@eslint/compat/2.1.0/c5793be99df3f1294a68a791ed84f1ed609714ef294e5fa5fdeeff97455db3c8/node_modules/@eslint/compat/dist/esm/index.js
1893
1893
  /**
1894
1894
  * @fileoverview Ignore file utilities for the compat package.
1895
1895
  * @author Nicholas C. Zakas
@@ -1918,7 +1918,7 @@ function convertIgnorePatternToMinimatch(pattern) {
1918
1918
  }
1919
1919
  var init_esm$1 = __esmMin((() => {}));
1920
1920
  //#endregion
1921
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/0a594adbd94ca75e2950b1104992894d49bc93f4f2bc935ad192d7104e4c271e/node_modules/eslint-config-flat-gitignore/dist/index.mjs
1921
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-config-flat-gitignore/2.3.0/48bee61dfbd6416cb227d1c477eb8990bf234fa634e4ccd62a9520a90cc26602/node_modules/eslint-config-flat-gitignore/dist/index.mjs
1922
1922
  var dist_exports = /* @__PURE__ */ __exportAll$2({ default: () => ignore });
1923
1923
  function toArray(array) {
1924
1924
  array = array ?? [];
@@ -6250,7 +6250,7 @@ async function unicorn({ complementIgnores, nameReplacements, roblox = true, roo
6250
6250
  ];
6251
6251
  }
6252
6252
  //#endregion
6253
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/a00fc88a04d0f2081267792f61f05ad4ceff0f52e1ff41acbbc866518b4fd9c1/node_modules/eslint-plugin-yml/lib/chunk-CfYAbeIz.mjs
6253
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/eedfb7e8cfd566a06319a5894d49f61723d6a8793c9b4a69e6556dd999563774/node_modules/eslint-plugin-yml/lib/chunk-CfYAbeIz.mjs
6254
6254
  var __defProp$1, __exportAll$1;
6255
6255
  var init_chunk_CfYAbeIz = __esmMin((() => {
6256
6256
  __defProp$1 = Object.defineProperty;
@@ -9433,7 +9433,7 @@ var init_lib$1 = __esmMin((() => {
9433
9433
  ({ ...meta_exports$1 });
9434
9434
  }));
9435
9435
  //#endregion
9436
- //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/a00fc88a04d0f2081267792f61f05ad4ceff0f52e1ff41acbbc866518b4fd9c1/node_modules/eslint-plugin-yml/lib/index.mjs
9436
+ //#region ../../../setup-pnpm/node_modules/.bin/store/v11/links/@/eslint-plugin-yml/3.5.0/eedfb7e8cfd566a06319a5894d49f61723d6a8793c9b4a69e6556dd999563774/node_modules/eslint-plugin-yml/lib/index.mjs
9437
9437
  var lib_exports = /* @__PURE__ */ __exportAll$2({
9438
9438
  configs: () => configs,
9439
9439
  default: () => src_default,
package/dist/lint-cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import process$1 from "node:process";
3
- import { getPackageInfoSync, isPackageExists } from "local-pkg";
3
+ import { isPackageExists } from "local-pkg";
4
4
  import yargs from "yargs";
5
5
  import "find-up-simple";
6
6
  import fs from "node:fs";
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
38
  }) : target, mod));
39
39
  //#endregion
40
40
  //#region package.json
41
- var version = "6.0.0-beta.45";
41
+ var version = "6.0.0-beta.46";
42
42
  //#endregion
43
43
  //#region src/lint-cli/lib/cli/types.ts
44
44
  /**
@@ -315,7 +315,7 @@ function parseArguments(argv, environment = process$1.env) {
315
315
  description: "Run only oxlint.",
316
316
  type: "boolean"
317
317
  }).option("fix", {
318
- description: "Apply fixes (oxlint then ESLint).",
318
+ description: "Fix the files the checks reported.",
319
319
  type: "boolean"
320
320
  }).option("agents", {
321
321
  description: "Agent-friendly output (default: on in an agent session).",
@@ -349,7 +349,7 @@ function parseArguments(argv, environment = process$1.env) {
349
349
  const oxlintOnly = parsed.oxlint === true;
350
350
  const { typeAware } = parsed;
351
351
  const fix = parsed.fix === true;
352
- if (fix && typeAware !== void 0) throw new CliError("Cannot combine --fix with --type-aware; --fix always uses the full config.");
352
+ if (fix && typeAware !== void 0) throw new CliError("Cannot combine --fix with --type-aware; the fix child always uses the full config.");
353
353
  const eslintArgs = eslintExtract.value !== void 0 ? splitArgs(eslintExtract.value) : [];
354
354
  const oxlintArgs = oxlintExtract.value !== void 0 ? splitArgs(oxlintExtract.value) : [];
355
355
  const rawPassthrough = parsed["--"];
@@ -474,7 +474,157 @@ function resolveRunContext(cwd, environment, mutate) {
474
474
  };
475
475
  }
476
476
  //#endregion
477
+ //#region src/lint-cli/lib/state.ts
478
+ /**
479
+ * Directory (relative to the project root) where the CLI keeps its persisted
480
+ * state. Lives under `node_modules/.cache` so it is discarded with the
481
+ * dependency tree and never committed.
482
+ */
483
+ const CACHE_DIRECTORY = path.join("node_modules", ".cache", "isentinel-lint");
484
+ /**
485
+ * The directory every state file lives in.
486
+ *
487
+ * @param cwd - The consumer project root.
488
+ * @returns The absolute path to the runner's cache directory.
489
+ */
490
+ function stateDirectory(cwd) {
491
+ return path.resolve(cwd, CACHE_DIRECTORY);
492
+ }
493
+ /**
494
+ * Resolve one state file inside {@link stateDirectory}.
495
+ *
496
+ * @param cwd - The consumer project root.
497
+ * @param name - The state's base name.
498
+ * @param parts - Further hyphen-joined segments, usually the config-variant key
499
+ * from `resolveCacheKey`. State that is consumed once (a stored hash, a
500
+ * drained builder) must carry that key, or the first variant to run absorbs
501
+ * the change on behalf of all of them.
502
+ * @returns The absolute path to the state file.
503
+ */
504
+ function statePath(cwd, name, ...parts) {
505
+ return path.join(stateDirectory(cwd), [name, ...parts].join("-"));
506
+ }
507
+ /**
508
+ * Read a UTF-8 file, or `undefined` when it cannot be read for any reason.
509
+ * Shared by every tolerant read in the CLI: state files, and the config-import
510
+ * closure walk.
511
+ *
512
+ * @param filePath - The file to read.
513
+ * @returns The file's content, or `undefined`.
514
+ */
515
+ function readFileIfPresent(filePath) {
516
+ try {
517
+ return fs.readFileSync(filePath, "utf8");
518
+ } catch {
519
+ return;
520
+ }
521
+ }
522
+ /**
523
+ * Read a state file written by {@link writeState}. Every failure mode — the
524
+ * file is missing, unreadable, malformed, or was written by another
525
+ * {@link STATE_VERSION} — degrades to `undefined`, which every caller treats as
526
+ * "unknown" and recomputes from.
527
+ *
528
+ * @template T - The shape this file's writer stores. Asserted, not checked:
529
+ * only the schema version is verified, exactly as the per-module casts this
530
+ * replaced did.
531
+ * @param filePath - The state file, from {@link statePath}.
532
+ * @returns The stored payload, or `undefined`.
533
+ */
534
+ function readState(filePath) {
535
+ const raw = readFileIfPresent(filePath);
536
+ return raw === void 0 ? void 0 : parseState(raw);
537
+ }
538
+ /**
539
+ * Persist a state payload, creating the cache directory as needed.
540
+ *
541
+ * The write is atomic (temp file plus rename), since `plan()` runs concurrently
542
+ * across packages in a parallel per-package lint setup and a torn file would be
543
+ * read back as "unknown" at best.
544
+ *
545
+ * Best-effort — a failed write leaves the state unknown, so the next run
546
+ * recomputes rather than trusting something stale.
547
+ *
548
+ * @param filePath - The state file, from {@link statePath}.
549
+ * @param data - The payload to store.
550
+ */
551
+ function writeState(filePath, data) {
552
+ const content = `${JSON.stringify({
553
+ data,
554
+ version: 2
555
+ })}\n`;
556
+ const temporary = `${filePath}.${process$1.pid}.tmp`;
557
+ try {
558
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
559
+ fs.writeFileSync(temporary, content);
560
+ fs.renameSync(temporary, filePath);
561
+ } catch {
562
+ try {
563
+ fs.rmSync(temporary, { force: true });
564
+ } catch {}
565
+ }
566
+ }
567
+ /**
568
+ * Compare a value against the stored one and replace it when they differ, the
569
+ * compare-and-swap every hash-drift bust runs: the caller acts on the outcome
570
+ * and never on the stored value itself.
571
+ *
572
+ * The value is consumed by the swap — once stored, every later run reads back
573
+ * `"unchanged"` — which is why hash state is keyed per config variant.
574
+ *
575
+ * A stored value this {@link STATE_VERSION} cannot read counts as `"changed"`,
576
+ * so a CLI upgrade invalidates rather than silently adopts what it finds.
577
+ *
578
+ * @param filePath - The state file, from {@link statePath}.
579
+ * @param value - The value this run computed.
580
+ * @returns How the stored value compared.
581
+ */
582
+ function swapState(filePath, value) {
583
+ const raw = readFileIfPresent(filePath);
584
+ if ((raw === void 0 ? void 0 : parseState(raw)) === value) return "unchanged";
585
+ writeState(filePath, value);
586
+ return raw === void 0 ? "first" : "changed";
587
+ }
588
+ /**
589
+ * Refresh a state file's mtime without rewriting it, so a reader comparing that
590
+ * mtime against another file's still sees the state as fresh. Best-effort: a
591
+ * missing file simply stays missing.
592
+ *
593
+ * @param filePath - The state file, from {@link statePath}.
594
+ */
595
+ function touchState(filePath) {
596
+ try {
597
+ const now = Date.now() / 1e3;
598
+ fs.utimesSync(filePath, now, now);
599
+ } catch {}
600
+ }
601
+ /**
602
+ * Parse one state file's content, or `undefined` when it is malformed or
603
+ * carries another {@link STATE_VERSION}.
604
+ *
605
+ * @template T - The shape this file's writer stores (asserted, not checked).
606
+ * @param raw - The file's content.
607
+ * @returns The stored payload, or `undefined`.
608
+ */
609
+ function parseState(raw) {
610
+ let parsed;
611
+ try {
612
+ parsed = JSON.parse(raw);
613
+ } catch {
614
+ return;
615
+ }
616
+ if (typeof parsed !== "object" || parsed === null) return;
617
+ const envelope = parsed;
618
+ return envelope.version === 2 ? envelope.data : void 0;
619
+ }
620
+ //#endregion
477
621
  //#region src/lint-cli/lib/exec/resolve.ts
622
+ /**
623
+ * A synthetic basename for `createRequire`, which resolves relative to a file
624
+ * rather than a directory. Spelled so it can never collide with a real consumer
625
+ * module.
626
+ */
627
+ const RESOLVE_ANCHOR = "__isentinel-lint__.js";
478
628
  /** Memoised {@link resolveLocalBin} results, keyed by `cwd\0name`. */
479
629
  const localBinCache = /* @__PURE__ */ new Map();
480
630
  /**
@@ -484,6 +634,10 @@ const localBinCache = /* @__PURE__ */ new Map();
484
634
  * Windows `.cmd`/`.ps1` quoting hazards. Memoised so the two ESLint passes
485
635
  * resolve the same bin once.
486
636
  *
637
+ * The walk is Node's own rather than a hand-rolled one: inside a worktree
638
+ * nested in its own checkout the two disagree, and taking the ancestor's copy
639
+ * silently runs a different version of the linter than every other tool sees.
640
+ *
487
641
  * @param name - The package name to resolve (for example `eslint`).
488
642
  * @param cwd - The directory to resolve from.
489
643
  * @returns The absolute path to the package's JavaScript entry.
@@ -492,12 +646,11 @@ function resolveLocalBin(name, cwd) {
492
646
  const cacheKey = `${cwd}\0${name}`;
493
647
  const cached = localBinCache.get(cacheKey);
494
648
  if (cached !== void 0) return cached;
495
- const info = getPackageInfoSync(name, { paths: [cwd] });
496
- if (info === void 0) throw new CliError(`Could not find "${name}". Install it in this project to run isentinel-lint.`);
497
- const { bin } = info.packageJson;
498
- const relative = typeof bin === "string" ? bin : bin?.[name];
649
+ const manifestPath = resolveManifest(name, cwd);
650
+ if (manifestPath === void 0) throw new CliError(`Could not find "${name}". Install it in this project to run isentinel-lint.`);
651
+ const relative = readBinEntry(manifestPath, name);
499
652
  if (relative === void 0) throw new CliError(`Package "${name}" does not declare a "${name}" bin entry.`);
500
- const resolved = path.resolve(info.rootPath, relative);
653
+ const resolved = path.resolve(path.dirname(manifestPath), relative);
501
654
  localBinCache.set(cacheKey, resolved);
502
655
  return resolved;
503
656
  }
@@ -537,6 +690,50 @@ function resolveIgnoredHelper() {
537
690
  const built = fileURLToPath(new URL("./lint-ignored.mjs", import.meta.url));
538
691
  return fs.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
539
692
  }
693
+ /**
694
+ * The path a package's manifest declares as the bin of the package's own name,
695
+ * relative to the package root. Both the shorthand string form and the map form
696
+ * count.
697
+ *
698
+ * @param manifestPath - The absolute path to the package's `package.json`.
699
+ * @param name - The bin name to look for.
700
+ * @returns The declared relative path, or undefined when there is none.
701
+ */
702
+ function readBinEntry(manifestPath, name) {
703
+ const raw = readFileIfPresent(manifestPath);
704
+ if (raw === void 0) return;
705
+ let parsed;
706
+ try {
707
+ parsed = JSON.parse(raw);
708
+ } catch {
709
+ return;
710
+ }
711
+ if (!isRecord(parsed)) return;
712
+ const { bin } = parsed;
713
+ if (typeof bin === "string") return bin;
714
+ const entry = isRecord(bin) ? bin[name] : void 0;
715
+ return typeof entry === "string" ? entry : void 0;
716
+ }
717
+ /**
718
+ * Locate a package's manifest exactly as a `require` call from `cwd` would.
719
+ *
720
+ * Reads the manifest as a subpath, so a package whose `exports` map hides
721
+ * `./package.json` counts as unresolvable. Both packages this serves publish
722
+ * it, and one that does not could not be spawned from its `bin` field anyway.
723
+ *
724
+ * @param name - The package name to resolve.
725
+ * @param cwd - The directory to resolve from.
726
+ * @returns The absolute path to the package's `package.json`, or undefined when
727
+ * the package is not installed.
728
+ */
729
+ function resolveManifest(name, cwd) {
730
+ const require = createRequire(path.join(cwd, RESOLVE_ANCHOR));
731
+ try {
732
+ return require.resolve(`${name}/package.json`);
733
+ } catch {
734
+ return;
735
+ }
736
+ }
540
737
  //#endregion
541
738
  //#region src/lint-cli/lib/exec/shell.ts
542
739
  const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
@@ -607,12 +804,13 @@ const PREFIX_COLOR = {
607
804
  /**
608
805
  * Run the composed children and aggregate their exit codes.
609
806
  *
610
- * Sequential when the caller asks for it `--fix` must not have two children
611
- * writing the same files at once, and a lone child gains nothing from the
612
- * concurrently harness. Otherwise every child runs at once. Either way all of
613
- * them run to completion: an ordinary lint failure in one no longer kills its
614
- * siblings, so the user keeps every result. The returned code is non-zero when
615
- * any child exited non-zero.
807
+ * Sequential when the caller asks for it: a lone child gains nothing from the
808
+ * concurrently harness, and the writers of a `--fix` run (the oxlint child, and
809
+ * later the one ESLint child that fixes) are each spawned on their own so
810
+ * nothing is reading a file while one of them rewrites it. Otherwise every
811
+ * child runs at once. Either way all of them run to completion: an ordinary
812
+ * lint failure in one no longer kills its siblings, so the user keeps every
813
+ * result. The returned code is non-zero when any child exited non-zero.
616
814
  *
617
815
  * @param commands - The child commands to run.
618
816
  * @param cwd - The working directory.
@@ -797,7 +995,7 @@ function composeEslintCommand(options, context) {
797
995
  args.push("--no-warn-ignored", "--concurrency", String(context.concurrency));
798
996
  if (options.cache && context.ci) args.push("--cache-strategy", "content");
799
997
  if (options.agents) args.push("--format", context.agentsFormatterPath);
800
- if (options.fix) args.push("--fix");
998
+ if (context.fix) args.push("--fix");
801
999
  args.push(...options.eslintArgs, ...context.paths);
802
1000
  return {
803
1001
  args,
@@ -837,6 +1035,7 @@ function composePasses(passes, runPlan, options) {
837
1035
  ci: runPlan.ci,
838
1036
  concurrency: pass.concurrency,
839
1037
  eslintLabel: pass.descriptor.label,
1038
+ fix: false,
840
1039
  paths: options.paths,
841
1040
  typeAwareEnv: pass.descriptor.typeAwareEnv
842
1041
  }));
@@ -871,150 +1070,6 @@ function compose(runPlan, options) {
871
1070
  notice: notices.length > 0 ? notices.join("") : void 0
872
1071
  };
873
1072
  }
874
- //#endregion
875
- //#region src/lint-cli/lib/state.ts
876
- /**
877
- * Directory (relative to the project root) where the CLI keeps its persisted
878
- * state. Lives under `node_modules/.cache` so it is discarded with the
879
- * dependency tree and never committed.
880
- */
881
- const CACHE_DIRECTORY = path.join("node_modules", ".cache", "isentinel-lint");
882
- /**
883
- * The directory every state file lives in.
884
- *
885
- * @param cwd - The consumer project root.
886
- * @returns The absolute path to the runner's cache directory.
887
- */
888
- function stateDirectory(cwd) {
889
- return path.resolve(cwd, CACHE_DIRECTORY);
890
- }
891
- /**
892
- * Resolve one state file inside {@link stateDirectory}.
893
- *
894
- * @param cwd - The consumer project root.
895
- * @param name - The state's base name.
896
- * @param parts - Further hyphen-joined segments, usually the config-variant key
897
- * from `resolveCacheKey`. State that is consumed once (a stored hash, a
898
- * drained builder) must carry that key, or the first variant to run absorbs
899
- * the change on behalf of all of them.
900
- * @returns The absolute path to the state file.
901
- */
902
- function statePath(cwd, name, ...parts) {
903
- return path.join(stateDirectory(cwd), [name, ...parts].join("-"));
904
- }
905
- /**
906
- * Read a UTF-8 file, or `undefined` when it cannot be read for any reason.
907
- * Shared by every tolerant read in the CLI: state files, and the config-import
908
- * closure walk.
909
- *
910
- * @param filePath - The file to read.
911
- * @returns The file's content, or `undefined`.
912
- */
913
- function readFileIfPresent(filePath) {
914
- try {
915
- return fs.readFileSync(filePath, "utf8");
916
- } catch {
917
- return;
918
- }
919
- }
920
- /**
921
- * Read a state file written by {@link writeState}. Every failure mode — the
922
- * file is missing, unreadable, malformed, or was written by another
923
- * {@link STATE_VERSION} — degrades to `undefined`, which every caller treats as
924
- * "unknown" and recomputes from.
925
- *
926
- * @template T - The shape this file's writer stores. Asserted, not checked:
927
- * only the schema version is verified, exactly as the per-module casts this
928
- * replaced did.
929
- * @param filePath - The state file, from {@link statePath}.
930
- * @returns The stored payload, or `undefined`.
931
- */
932
- function readState(filePath) {
933
- const raw = readFileIfPresent(filePath);
934
- return raw === void 0 ? void 0 : parseState(raw);
935
- }
936
- /**
937
- * Persist a state payload, creating the cache directory as needed.
938
- *
939
- * The write is atomic (temp file plus rename), since `plan()` runs concurrently
940
- * across packages in a parallel per-package lint setup and a torn file would be
941
- * read back as "unknown" at best.
942
- *
943
- * Best-effort — a failed write leaves the state unknown, so the next run
944
- * recomputes rather than trusting something stale.
945
- *
946
- * @param filePath - The state file, from {@link statePath}.
947
- * @param data - The payload to store.
948
- */
949
- function writeState(filePath, data) {
950
- const content = `${JSON.stringify({
951
- data,
952
- version: 2
953
- })}\n`;
954
- const temporary = `${filePath}.${process$1.pid}.tmp`;
955
- try {
956
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
957
- fs.writeFileSync(temporary, content);
958
- fs.renameSync(temporary, filePath);
959
- } catch {
960
- try {
961
- fs.rmSync(temporary, { force: true });
962
- } catch {}
963
- }
964
- }
965
- /**
966
- * Compare a value against the stored one and replace it when they differ, the
967
- * compare-and-swap every hash-drift bust runs: the caller acts on the outcome
968
- * and never on the stored value itself.
969
- *
970
- * The value is consumed by the swap — once stored, every later run reads back
971
- * `"unchanged"` — which is why hash state is keyed per config variant.
972
- *
973
- * A stored value this {@link STATE_VERSION} cannot read counts as `"changed"`,
974
- * so a CLI upgrade invalidates rather than silently adopts what it finds.
975
- *
976
- * @param filePath - The state file, from {@link statePath}.
977
- * @param value - The value this run computed.
978
- * @returns How the stored value compared.
979
- */
980
- function swapState(filePath, value) {
981
- const raw = readFileIfPresent(filePath);
982
- if ((raw === void 0 ? void 0 : parseState(raw)) === value) return "unchanged";
983
- writeState(filePath, value);
984
- return raw === void 0 ? "first" : "changed";
985
- }
986
- /**
987
- * Refresh a state file's mtime without rewriting it, so a reader comparing that
988
- * mtime against another file's still sees the state as fresh. Best-effort: a
989
- * missing file simply stays missing.
990
- *
991
- * @param filePath - The state file, from {@link statePath}.
992
- */
993
- function touchState(filePath) {
994
- try {
995
- const now = Date.now() / 1e3;
996
- fs.utimesSync(filePath, now, now);
997
- } catch {}
998
- }
999
- /**
1000
- * Parse one state file's content, or `undefined` when it is malformed or
1001
- * carries another {@link STATE_VERSION}.
1002
- *
1003
- * @template T - The shape this file's writer stores (asserted, not checked).
1004
- * @param raw - The file's content.
1005
- * @returns The stored payload, or `undefined`.
1006
- */
1007
- function parseState(raw) {
1008
- let parsed;
1009
- try {
1010
- parsed = JSON.parse(raw);
1011
- } catch {
1012
- return;
1013
- }
1014
- if (typeof parsed !== "object" || parsed === null) return;
1015
- const envelope = parsed;
1016
- return envelope.version === 2 ? envelope.data : void 0;
1017
- }
1018
1073
  /**
1019
1074
  * Base name of every ESLint cache file the runner manages. Each real file adds
1020
1075
  * a pass suffix and a config-variant key (see {@link cacheFileFor}), so this
@@ -1303,6 +1358,7 @@ function toPosix(value) {
1303
1358
  }
1304
1359
  //#endregion
1305
1360
  //#region src/lint-cli/lib/cache/entries.ts
1361
+ const CASE_INSENSITIVE_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "win32"]);
1306
1362
  /**
1307
1363
  * Compute the newest modification time across the given files. Callers hash the
1308
1364
  * cache-bust set once per run and compare each pass's cache mtime against the
@@ -1359,22 +1415,32 @@ function sweepStaleCaches(cwd, newestBustMtimeMs) {
1359
1415
  return removed;
1360
1416
  }
1361
1417
  /**
1362
- * Normalize a path for cache-key comparison: absolute, forward-slash and
1363
- * lower-cased. TypeScript emits forward-slash paths while ESLint keys the cache
1364
- * with OS-native ones, and Windows paths are case-insensitive this collapses
1418
+ * Normalize a path for cache-key comparison: absolute and forward-slash, plus
1419
+ * lower-cased where the platform's filesystem is case-insensitive. TypeScript
1420
+ * emits forward-slash paths while ESLint keys the cache with OS-native ones,
1421
+ * and Windows and macOS reach the same file under either case — this collapses
1365
1422
  * all of those into a single comparable form.
1366
1423
  *
1424
+ * Case survives everywhere else: on a case-sensitive filesystem `src/Foo.ts`
1425
+ * and `src/foo.ts` are two files with two cache entries, and folding them
1426
+ * together would let one shadow the other's verdict.
1427
+ *
1367
1428
  * @param filePath - The path to normalize.
1429
+ * @param platform - The platform whose filesystem the paths live on.
1368
1430
  * @returns The canonical key.
1369
1431
  */
1370
- function normalizePath(filePath) {
1371
- return toPosix(path.resolve(filePath)).toLowerCase();
1432
+ function normalizePath(filePath, platform = process$1.platform) {
1433
+ const resolved = toPosix(path.resolve(filePath));
1434
+ return CASE_INSENSITIVE_PLATFORMS.has(platform) ? resolved.toLowerCase() : resolved;
1372
1435
  }
1373
1436
  /**
1374
1437
  * Open an ESLint cache for reuse, or `undefined` when the file is missing (the
1375
- * caller then treats every target file as dirty). The returned handle backs
1376
- * both {@link DirtyCache.getUpdatedFiles} and {@link DirtyCache.removeEntries}
1377
- * so a pass parses the cache once instead of twice.
1438
+ * caller then treats every target file as dirty).
1439
+ *
1440
+ * The two reads share one parsed handle. The removal parses its own, because
1441
+ * asking a cache which files changed restamps every entry it looked at with
1442
+ * that file's current size and mtime: persisting that handle would hand ESLint
1443
+ * the previous run's results under a stamp saying they are current.
1378
1444
  *
1379
1445
  * @param cacheFilePath - The ESLint cache file to open.
1380
1446
  * @param useChecksum - Compare by content checksum instead of metadata.
@@ -1382,10 +1448,11 @@ function normalizePath(filePath) {
1382
1448
  */
1383
1449
  function openCache(cacheFilePath, useChecksum) {
1384
1450
  if (!fs.existsSync(cacheFilePath)) return;
1385
- const cache = fileEntryCache.createFromFile(cacheFilePath, useChecksum);
1451
+ const cache = load(cacheFilePath, useChecksum);
1386
1452
  return {
1453
+ filesWithMessages: (files) => filesWithMessagesIn(cache, files),
1387
1454
  getUpdatedFiles: (files) => cache.getUpdatedFiles(files),
1388
- removeEntries: (files) => removeEntriesFrom(cache, files)
1455
+ removeEntries: (files) => removeEntriesFrom(load(cacheFilePath, useChecksum), files)
1389
1456
  };
1390
1457
  }
1391
1458
  function safeMtimeMs(filePath) {
@@ -1418,9 +1485,45 @@ function removeCacheFile(cacheFilePath) {
1418
1485
  fs.rmSync(cacheFilePath, { force: true });
1419
1486
  } catch {}
1420
1487
  }
1421
- function removeEntriesFrom(cache, files) {
1488
+ /**
1489
+ * Load a cache file the way ESLint's own `LintResultCache` does. The two read
1490
+ * and write the same entries, and a reader whose change detection disagrees
1491
+ * with the writer's reports every file as dirty.
1492
+ *
1493
+ * @param cacheFilePath - The ESLint cache file to load.
1494
+ * @param useChecksum - Compare by content checksum instead of metadata.
1495
+ * @returns The loaded cache.
1496
+ */
1497
+ function load(cacheFilePath, useChecksum) {
1498
+ return fileEntryCache.createFromFile(cacheFilePath, {
1499
+ useCheckSum: useChecksum,
1500
+ useModifiedTime: !useChecksum
1501
+ });
1502
+ }
1503
+ /**
1504
+ * Index a cache's entry keys by their normalized form, so a caller holding
1505
+ * paths from elsewhere — TypeScript's forward slashes, a git listing — can find
1506
+ * the entry ESLint wrote under an OS-native path.
1507
+ *
1508
+ * @param cache - The loaded cache to index.
1509
+ * @returns Normalized path to the key the cache stores it under.
1510
+ */
1511
+ function keysByNormalizedPath(cache) {
1422
1512
  const keyByNormalized = /* @__PURE__ */ new Map();
1423
1513
  for (const key of cache.cache.keys()) keyByNormalized.set(normalizePath(key), key);
1514
+ return keyByNormalized;
1515
+ }
1516
+ function filesWithMessagesIn(cache, files) {
1517
+ const keyByNormalized = keysByNormalizedPath(cache);
1518
+ return files.filter((file) => {
1519
+ const key = keyByNormalized.get(normalizePath(file));
1520
+ if (key === void 0) return false;
1521
+ const messages = cache.cache.getKey(key)?.data?.results?.messages;
1522
+ return messages !== void 0 && messages.length > 0;
1523
+ });
1524
+ }
1525
+ function removeEntriesFrom(cache, files) {
1526
+ const keyByNormalized = keysByNormalizedPath(cache);
1424
1527
  let removed = 0;
1425
1528
  for (const file of files) {
1426
1529
  const key = keyByNormalized.get(normalizePath(file));
@@ -1429,7 +1532,7 @@ function removeEntriesFrom(cache, files) {
1429
1532
  removed += 1;
1430
1533
  }
1431
1534
  }
1432
- if (removed > 0) cache.cache.save(true);
1535
+ if (removed > 0) cache.cache.save();
1433
1536
  return removed;
1434
1537
  }
1435
1538
  //#endregion
@@ -3699,7 +3802,9 @@ function collectRepoFiles(cwd, targets) {
3699
3802
  const relatives = listFiles(cwd, ["."]);
3700
3803
  const isBustFile = (0, import_picomatch.default)([...CACHE_BUST_PATTERNS], { dot: true });
3701
3804
  const normalizedTargets = targets.map((target) => normalizeTarget(target, cwd));
3702
- const targetsOutsideCwd = normalizedTargets.some((target) => isOutsideCwd(target));
3805
+ const outsideCwdTargets = targets.filter((target) => {
3806
+ return isOutsideCwd(normalizeTarget(target, cwd));
3807
+ });
3703
3808
  const isWithinTargets = matchTargets(normalizedTargets);
3704
3809
  const bustFiles = collectAncestorBustFiles(cwd);
3705
3810
  const lintable = [];
@@ -3716,7 +3821,7 @@ function collectRepoFiles(cwd, targets) {
3716
3821
  bustFiles,
3717
3822
  configFiles: bustFiles.filter(isConfigEntryPoint),
3718
3823
  lintable,
3719
- targetsOutsideCwd,
3824
+ outsideCwdTargets,
3720
3825
  typeAware
3721
3826
  };
3722
3827
  }
@@ -3974,12 +4079,6 @@ function gitIgnoredTargets(cwd, targets) {
3974
4079
  * failure this feature cannot have.
3975
4080
  */
3976
4081
  /**
3977
- * A synthetic basename for `createRequire`, which resolves relative to a file
3978
- * rather than a directory. Spelled so it can never collide with a real consumer
3979
- * module.
3980
- */
3981
- const RESOLVE_ANCHOR = "__isentinel-lint__.js";
3982
- /**
3983
4082
  * Resolve the ESLint the consumer's config will be linted with: their own,
3984
4083
  * resolved from `cwd`, falling back to the one resolvable from this file (the
3985
4084
  * hoisted peer dependency) when `cwd` has no `node_modules` of its own — the
@@ -4528,7 +4627,10 @@ const TYPED_PASS = {
4528
4627
  typeAwareEnv: "only",
4529
4628
  typeAwareOnly: true
4530
4629
  };
4531
- /** The full-config pass (env unset, `.eslintcache`): CI, `--fix`, `=full`. */
4630
+ /**
4631
+ * The full-config pass (env unset, `.eslintcache`): CI, `=full`, and the child
4632
+ * a `--fix` run spawns over the files its checks reported (see `planFixChild`).
4633
+ */
4532
4634
  const FULL_PASS = {
4533
4635
  cacheFileBase: CACHE_FILE_DEFAULT,
4534
4636
  filesPerWorker: (limits) => limits.filesPerWorker,
@@ -4539,19 +4641,23 @@ const FULL_PASS = {
4539
4641
  };
4540
4642
  /**
4541
4643
  * Select the ESLint passes for the resolved mode. An explicit `--type-aware`
4542
- * always wins: `=full` (and `--fix`) collapse to the single full pass, while
4543
- * `=off`/`=only` run their one pass even in CI. Only when no mode is given does
4544
- * CI change the default — collapsing the concurrent two-pass split to one full
4545
- * pass; a local default run keeps the split (the typed pass may later be
4546
- * skipped). CI's `--cache-strategy content` is applied by the command composer
4547
- * to whichever pass runs, independently of this selection.
4644
+ * always wins: `=full` collapses to the single full pass, while `=off`/`=only`
4645
+ * run their one pass even in CI. Only when no mode is given does CI change the
4646
+ * default — collapsing the concurrent two-pass split to one full pass; a local
4647
+ * default run keeps the split (the typed pass may later be skipped). CI's
4648
+ * `--cache-strategy content` is applied by the command composer to whichever
4649
+ * pass runs, independently of this selection.
4650
+ *
4651
+ * `--fix` selects nothing of its own. Its ESLint children are the same checks
4652
+ * any other run has, and the fix itself is one further child composed from what
4653
+ * they reported (see `planFixChild`), which is why the checks stay concurrent.
4548
4654
  *
4549
4655
  * @param options - The parsed CLI options.
4550
4656
  * @param ci - Whether the run is in CI.
4551
4657
  * @returns The pass descriptors to plan, in run order.
4552
4658
  */
4553
4659
  function selectPasses(options, ci) {
4554
- if (options.fix || options.typeAware === "full") return [FULL_PASS];
4660
+ if (options.typeAware === "full") return [FULL_PASS];
4555
4661
  if (options.typeAware === "off") return [FAST_PASS];
4556
4662
  if (options.typeAware === "only") return [TYPED_PASS];
4557
4663
  if (ci) return [FULL_PASS];
@@ -4571,6 +4677,94 @@ function maxWorkersFor(descriptor, limits) {
4571
4677
  return descriptor.invalidation === "none" ? limits.maxWorkers : limits.typedMaxWorkers;
4572
4678
  }
4573
4679
  //#endregion
4680
+ //#region src/lint-cli/lib/plan/fix.ts
4681
+ /**
4682
+ * The files the check passes had a message about, in pass order and without
4683
+ * duplicates.
4684
+ *
4685
+ * Read out of each pass's own cache rather than out of its output. ESLint
4686
+ * stores the lint result beside every entry and replays it on a cache hit, so
4687
+ * the cache is the pass's whole-tree verdict whichever files it actually
4688
+ * re-linted this run — including for a pass that auto-skipped, whose verdict
4689
+ * stands precisely because nothing it cares about changed.
4690
+ *
4691
+ * Each pass only contributes the targets it lints: the type-aware pass never
4692
+ * sees the JSON and Markdown the fast pass covers.
4693
+ *
4694
+ * @param passes - The planned passes, run and auto-skipped alike.
4695
+ * @param run - The run context.
4696
+ * @param files - The lint-target lists to look the verdict up against.
4697
+ * @returns The reported files, empty when every pass came back clean.
4698
+ */
4699
+ function collectFixTargets(passes, run, files) {
4700
+ const targets = /* @__PURE__ */ new Set();
4701
+ for (const pass of passes) {
4702
+ const cache = openCache(path.resolve(run.cwd, pass.cacheFile), run.ci);
4703
+ const candidates = pass.descriptor.typeAwareOnly ? files.typeAware : files.lintable;
4704
+ const reported = cache?.filesWithMessages(candidates) ?? [];
4705
+ for (const file of reported) targets.add(file);
4706
+ }
4707
+ return [...targets];
4708
+ }
4709
+ /**
4710
+ * Compose the one ESLint child a `--fix` run spawns after its checks, or
4711
+ * `undefined` when they reported nothing and there is nothing to fix.
4712
+ *
4713
+ * The child runs the full config — a superset of both check configs, so it can
4714
+ * fix anything either of them found — over only the reported files. Being one
4715
+ * child, it is also the run's only ESLint writer.
4716
+ *
4717
+ * Its cache entries for those files are dropped first. The full config's cache
4718
+ * gets none of the invalidation the check caches do (no builder runs against
4719
+ * it), so it can hold a stale clean entry for a file the checks have since
4720
+ * found a message on, and ESLint would skip the very file it was handed.
4721
+ *
4722
+ * The child's targets are the union of what the verdicts cover and what they
4723
+ * cannot. `--no-cache` records no verdict at all, so the whole run falls back
4724
+ * to its own paths — the whole tree, as a fix run always used to lint. An
4725
+ * out-of-cwd target is invisible to the cwd-relative listing the verdicts are
4726
+ * looked up against (see {@link RepoFiles.outsideCwdTargets}), so it is passed
4727
+ * through as it stands while every in-cwd file stays narrowed to what the
4728
+ * checks reported.
4729
+ *
4730
+ * A raw path names no file whose cache entry can be dropped, so a child
4731
+ * carrying one runs without the cache rather than risk a stale clean entry
4732
+ * making ESLint skip it. That costs only the write: the covered files' entries
4733
+ * have just been dropped, so the cache would have saved that child no linting
4734
+ * anyway.
4735
+ *
4736
+ * @param passes - The planned passes whose verdicts narrow the child.
4737
+ * @param run - The run context.
4738
+ * @param inputs - The lint-target lists, CLI options, limits and formatter path.
4739
+ * @returns The fix child, or `undefined` when nothing was reported.
4740
+ */
4741
+ function planFixChild(passes, run, inputs) {
4742
+ const { files, limits, options } = inputs;
4743
+ const covered = options.cache ? collectFixTargets(passes, run, files) : [];
4744
+ const uncovered = options.cache ? files.outsideCwdTargets : options.paths;
4745
+ const targets = [...covered, ...uncovered];
4746
+ if (targets.length === 0) return;
4747
+ const cacheFile = cacheFileFor(CACHE_FILE_DEFAULT, run.key);
4748
+ if (covered.length > 0) openCache(path.resolve(run.cwd, cacheFile), run.ci)?.removeEntries(covered);
4749
+ return composeEslintCommand({
4750
+ ...options,
4751
+ cache: options.cache && uncovered.length === 0
4752
+ }, {
4753
+ agentsFormatterPath: inputs.agentsFormatterPath,
4754
+ cacheLocation: cacheFile,
4755
+ ci: run.ci,
4756
+ concurrency: options.concurrency ?? computeWorkerCount({
4757
+ dirtyCount: targets.length,
4758
+ filesPerWorker: limits.filesPerWorker,
4759
+ maxWorkers: limits.typedMaxWorkers
4760
+ }),
4761
+ eslintLabel: FULL_PASS.label,
4762
+ fix: true,
4763
+ paths: targets,
4764
+ typeAwareEnv: FULL_PASS.typeAwareEnv
4765
+ });
4766
+ }
4767
+ //#endregion
4574
4768
  //#region src/lint-cli/lib/typescript/buildinfo.ts
4575
4769
  /**
4576
4770
  * Whether the program described by an existing `.tsbuildinfo` is exactly the
@@ -5405,7 +5599,7 @@ function passDirtyCount(descriptor, cacheFile, context) {
5405
5599
  function sizePass(descriptor, context) {
5406
5600
  const cacheFile = cacheFileFor(descriptor.cacheFileBase, context.run.key);
5407
5601
  const dirtyCount = passDirtyCount(descriptor, cacheFile, context);
5408
- const conservative = context.files.targetsOutsideCwd;
5602
+ const conservative = context.files.outsideCwdTargets.length > 0;
5409
5603
  const filesPerWorker = descriptor.filesPerWorker(context.limits, context.run.environment);
5410
5604
  const maxWorkers = maxWorkersFor(descriptor, context.limits);
5411
5605
  const sizingDirtyCount = conservative ? maxWorkers * filesPerWorker : dirtyCount;
@@ -5465,7 +5659,8 @@ function plan(options, run) {
5465
5659
  passes: [],
5466
5660
  targetsOutsideCwd: false
5467
5661
  },
5468
- resolveDeferred: void 0
5662
+ resolveDeferred: void 0,
5663
+ resolveFixChild: void 0
5469
5664
  };
5470
5665
  const descriptors = selectPasses(options, ci);
5471
5666
  const files = collectRepoFiles(cwd, options.paths);
@@ -5495,10 +5690,11 @@ function plan(options, run) {
5495
5690
  newestBustMtime,
5496
5691
  options
5497
5692
  };
5498
- const staged = stageDescriptors(descriptors, options, run, {
5693
+ const targetsOutsideCwd = files.outsideCwdTargets.length > 0;
5694
+ const staged = stageDescriptors(descriptors, run, {
5499
5695
  multiPass: inputs.multiPass,
5500
- oxlint: oxlintDecision.run,
5501
- targetsOutsideCwd: sizingFiles.targetsOutsideCwd
5696
+ oxlint: oxlintDecision.run && !options.fix,
5697
+ targetsOutsideCwd
5502
5698
  });
5503
5699
  return {
5504
5700
  eager: {
@@ -5509,9 +5705,17 @@ function plan(options, run) {
5509
5705
  oxlintReason: oxlintDecision.reason,
5510
5706
  oxlintTypeAware,
5511
5707
  passes: sizePasses(staged.eager, run, inputs),
5512
- targetsOutsideCwd: files.targetsOutsideCwd
5708
+ targetsOutsideCwd
5513
5709
  },
5514
- resolveDeferred: staged.deferred.length > 0 ? () => sizePasses(staged.deferred, run, inputs) : void 0
5710
+ resolveDeferred: staged.deferred.length > 0 ? () => sizePasses(staged.deferred, run, inputs) : void 0,
5711
+ resolveFixChild: mutate && options.fix ? (passes) => {
5712
+ return planFixChild(passes, run, {
5713
+ agentsFormatterPath,
5714
+ files: sizingFiles,
5715
+ limits,
5716
+ options
5717
+ });
5718
+ } : void 0
5515
5719
  };
5516
5720
  }
5517
5721
  function resolveOxlintTypeAware(options) {
@@ -5523,12 +5727,10 @@ function resolveOxlintTypeAware(options) {
5523
5727
  * runs the TypeScript builder is ever held back — that is the whole cost being
5524
5728
  * moved off the critical path.
5525
5729
  *
5526
- * Everything else stays eager, and four cases opt out of staging entirely:
5730
+ * Everything else stays eager, and three cases opt out of staging entirely:
5527
5731
  *
5528
5732
  * - `--print` (`mutate` false) runs no builder anyway and must stay one pure
5529
5733
  * snapshot printed in a single go;
5530
- * - `--fix` runs its children one at a time, so a later spawn buys nothing and
5531
- * there is no reason to reopen the write race the sequential path prevents;
5532
5734
  * - a run with no eager child at all, where holding the builder-backed pass
5533
5735
  * back would only delay the whole run;
5534
5736
  * - a run whose one eager child could end up alone, because the type-aware pass
@@ -5537,19 +5739,18 @@ function resolveOxlintTypeAware(options) {
5537
5739
  * prefixed, piped harness for no gain.
5538
5740
  *
5539
5741
  * @param descriptors - The selected passes, in run order.
5540
- * @param options - The parsed CLI options.
5541
5742
  * @param run - The run context.
5542
5743
  * @param staging - The oxlint decision and the auto-skip inputs.
5543
5744
  * @returns The two stages, each in run order.
5544
5745
  */
5545
- function stageDescriptors(descriptors, options, run, staging) {
5746
+ function stageDescriptors(descriptors, run, staging) {
5546
5747
  const eager = descriptors.filter((descriptor) => descriptor.invalidation === "none");
5547
5748
  const deferred = descriptors.filter((descriptor) => descriptor.invalidation !== "none");
5548
5749
  const unstaged = {
5549
5750
  deferred: [],
5550
5751
  eager: descriptors
5551
5752
  };
5552
- if (!run.mutate || options.fix || deferred.length === 0) return unstaged;
5753
+ if (!run.mutate || deferred.length === 0) return unstaged;
5553
5754
  const eagerChildren = (staging.oxlint ? 1 : 0) + eager.length;
5554
5755
  const mayAutoSkip = staging.multiPass && !staging.targetsOutsideCwd;
5555
5756
  if (eagerChildren === 0 || eagerChildren === 1 && mayAutoSkip) return unstaged;
@@ -5579,13 +5780,47 @@ async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env)
5579
5780
  }
5580
5781
  if (commands.some((command) => command.bin === "oxlint" && command.args.includes("--type-aware")) && !isPackageExists("oxlint-tsgolint", { paths: [cwd] })) throw new CliError("oxlint-tsgolint is not installed, so oxlint cannot run type-aware rules. Install oxlint-tsgolint, or pass --no-oxlint-type-aware to skip type-aware linting.");
5581
5782
  if (notice !== void 0) process$1.stderr.write(notice);
5783
+ const { resolveFixChild } = staged;
5784
+ if (resolveFixChild === void 0) return (await runChecks(commands, cwd, staged, options)).code;
5785
+ const byBin = Object.groupBy(commands, (command) => command.bin);
5786
+ const oxlintCode = await execute(byBin.oxlint ?? [], cwd, true);
5787
+ const checks = await runChecks(byBin.eslint ?? [], cwd, staged, options);
5788
+ const fixChild = resolveFixChild(checks.passes);
5789
+ if (fixChild === void 0) return oxlintCode || checks.code;
5790
+ return oxlintCode || await execute([fixChild], cwd, true);
5791
+ }
5792
+ /**
5793
+ * Run a run's ESLint check children and report which passes they covered.
5794
+ *
5795
+ * A staged run is concurrent by construction (the planner only stages when at
5796
+ * least one other child is already linting). Its type-aware pass is planned —
5797
+ * TypeScript builder and all — only once the children above are running, and
5798
+ * its notice is emitted as soon as it is known rather than held to the end of
5799
+ * the run.
5800
+ *
5801
+ * @param commands - The composed check children.
5802
+ * @param cwd - The working directory.
5803
+ * @param staged - The staged plan the commands came from.
5804
+ * @param options - The parsed CLI options.
5805
+ * @returns The aggregated exit code and every pass that was planned.
5806
+ */
5807
+ async function runChecks(commands, cwd, staged, options) {
5808
+ const passes = [...staged.eager.passes];
5582
5809
  const { resolveDeferred } = staged;
5583
- if (resolveDeferred === void 0) return execute(commands, cwd, options.fix || commands.length <= 1);
5584
- return executeStaged(commands, cwd, () => {
5585
- const later = composePasses(resolveDeferred(), staged.eager, options);
5586
- if (later.notice !== void 0) process$1.stderr.write(later.notice);
5587
- return later.commands;
5588
- });
5810
+ if (resolveDeferred === void 0) return {
5811
+ code: await execute(commands, cwd, commands.length <= 1),
5812
+ passes
5813
+ };
5814
+ return {
5815
+ code: await executeStaged(commands, cwd, () => {
5816
+ const deferred = resolveDeferred();
5817
+ passes.push(...deferred);
5818
+ const later = composePasses(deferred, staged.eager, options);
5819
+ if (later.notice !== void 0) process$1.stderr.write(later.notice);
5820
+ return later.commands;
5821
+ }),
5822
+ passes
5823
+ };
5589
5824
  }
5590
5825
  //#endregion
5591
5826
  //#region src/lint-cli/index.ts
@@ -5597,7 +5832,8 @@ files actually need re-linting and managing per-mode caches.
5597
5832
  Flags:
5598
5833
  --eslint Run only ESLint.
5599
5834
  --oxlint Run only oxlint.
5600
- --fix Apply fixes: oxlint --fix then eslint --fix.
5835
+ --fix Check as usual, then fix only the files the checks
5836
+ reported, in one further ESLint child.
5601
5837
  --agents, --no-agents Emit agent-friendly output. On by default when an AI
5602
5838
  agent session is detected (AI_AGENT, CLAUDECODE, ...).
5603
5839
  --type-aware=off|only|full
@@ -29,6 +29,14 @@ function isStringArray(value) {
29
29
  return Array.isArray(value) && value.every((item) => typeof item === "string");
30
30
  }
31
31
  //#endregion
32
+ //#region src/lint-cli/lib/exec/resolve.ts
33
+ /**
34
+ * A synthetic basename for `createRequire`, which resolves relative to a file
35
+ * rather than a directory. Spelled so it can never collide with a real consumer
36
+ * module.
37
+ */
38
+ const RESOLVE_ANCHOR = "__isentinel-lint__.js";
39
+ //#endregion
32
40
  //#region src/lint-cli/lib/exec/eslint-install.ts
33
41
  /**
34
42
  * Locating the ESLint installation a run's config is resolved against.
@@ -41,12 +49,6 @@ function isStringArray(value) {
41
49
  * failure this feature cannot have.
42
50
  */
43
51
  /**
44
- * A synthetic basename for `createRequire`, which resolves relative to a file
45
- * rather than a directory. Spelled so it can never collide with a real consumer
46
- * module.
47
- */
48
- const RESOLVE_ANCHOR = "__isentinel-lint__.js";
49
- /**
50
52
  * Resolve the ESLint the consumer's config will be linted with: their own,
51
53
  * resolved from `cwd`, falling back to the one resolvable from this file (the
52
54
  * hoisted peer dependency) when `cwd` has no `node_modules` of its own — the
package/dist/oxlint.d.mts CHANGED
@@ -6660,7 +6660,7 @@ interface RuleOptions {
6660
6660
  * Disallow unmodified loop conditions
6661
6661
  * @see https://eslint.org/docs/latest/rules/no-unmodified-loop-condition
6662
6662
  */
6663
- 'no-unmodified-loop-condition'?: Linter.RuleEntry<[]>;
6663
+ 'no-unmodified-loop-condition'?: Linter.RuleEntry<NoUnmodifiedLoopCondition>;
6664
6664
  /**
6665
6665
  * Disallow ternary operators when simpler alternatives exist
6666
6666
  * @see https://eslint.org/docs/latest/rules/no-unneeded-ternary
@@ -16979,6 +16979,10 @@ type NoUnderscoreDangle = [] | [{
16979
16979
  allowInArrayDestructuring?: boolean;
16980
16980
  allowInObjectDestructuring?: boolean;
16981
16981
  }];
16982
+ // ----- no-unmodified-loop-condition -----
16983
+ type NoUnmodifiedLoopCondition = [] | [{
16984
+ checkConditionalExpressions?: boolean;
16985
+ }];
16982
16986
  // ----- no-unneeded-ternary -----
16983
16987
  type NoUnneededTernary = [] | [{
16984
16988
  defaultAssignment?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/eslint-config",
3
- "version": "6.0.0-beta.45",
3
+ "version": "6.0.0-beta.46",
4
4
  "description": "iSentinel's ESLint config",
5
5
  "keywords": [
6
6
  "eslint-config",
@@ -122,7 +122,7 @@
122
122
  "@types/yargs": "17.0.35",
123
123
  "@vitest/eslint-plugin": "1.6.26",
124
124
  "bumpp": "12.0.0",
125
- "eslint": "10.8.1",
125
+ "eslint": "10.10.0",
126
126
  "eslint-plugin-erasable-syntax-only": "0.4.2",
127
127
  "eslint-plugin-eslint-plugin": "7.5.0",
128
128
  "eslint-plugin-jest": "29.16.0",
@@ -145,11 +145,11 @@
145
145
  "typescript": "6.0.3",
146
146
  "unplugin-unused": "0.5.7",
147
147
  "vitest": "4.1.10",
148
- "@isentinel/eslint-config": "6.0.0-beta.45"
148
+ "@isentinel/eslint-config": "6.0.0-beta.46"
149
149
  },
150
150
  "peerDependencies": {
151
151
  "@vitest/eslint-plugin": "^1.6.4",
152
- "eslint": "^10.4.0",
152
+ "eslint": "^10.10.0",
153
153
  "eslint-plugin-eslint-plugin": "^7.5.0",
154
154
  "eslint-plugin-jest": "^29.15.0",
155
155
  "eslint-plugin-jest-extended": "^3.0.0",