@isentinel/eslint-config 6.0.0 → 6.0.1

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
@@ -230,8 +230,8 @@ It resolves the `eslint` and `oxlint` binaries from your project's
230
230
 
231
231
  By default (a local run, no `--type-aware`, no `--fix`) it runs **three**
232
232
  children concurrently via
233
- [`concurrently`](https://www.npmjs.com/package/concurrently), grouping their
234
- output:
233
+ [`concurrently`](https://www.npmjs.com/package/concurrently)'s core (no shell),
234
+ grouping their output:
235
235
 
236
236
  - **oxlint** — the full oxlint pass (including its type-aware rules).
237
237
  - **fast pass** — ESLint with only the syntactic (non-type-aware) rules, over
@@ -341,6 +341,32 @@ determined it fails open and runs both engines as before. Explicit `--eslint` /
341
341
 
342
342
  Any trailing positional arguments are treated as paths to lint (default `.`).
343
343
 
344
+ ### Large file selections
345
+
346
+ A git hook that hands the runner thousands of files should not put them on a
347
+ command line: `cmd.exe` stops at 8,191 characters and `CreateProcessW` at
348
+ 32,767, so tools end up splitting the list into batches that each start another
349
+ lint stack. Pass the list as data instead:
350
+
351
+ ```bash
352
+ git diff --cached --name-only -z --relative --diff-filter=ACMR | isentinel-lint --files0-from -
353
+ ```
354
+
355
+ `--files0-from <file|->` reads NUL-terminated UTF-8 paths from a file, or from
356
+ stdin for `-` (the `git -z` / `find -print0` shape). Relative entries resolve
357
+ against the working directory, exactly like positional paths (hence `--relative`
358
+ above when running from a sub-package). The paths go through the same planning,
359
+ caching and type-aware skip as positional ones, in one run. An empty list lints
360
+ nothing and exits 0 — it never falls back to `.`. A list that is not valid UTF-8
361
+ or holds an empty entry is refused, as is combining the flag with positional
362
+ paths.
363
+
364
+ The runner never puts paths on a command line itself either: every oxlint and
365
+ ESLint child is launched as `node <bootstrap> <engine bin>` with no shell, and
366
+ its full argument list is streamed to it over stdin. As a consequence ESLint's
367
+ own `--stdin` / `--stdin-filename` cannot be forwarded through `--eslint-args`
368
+ or `--`.
369
+
344
370
  ### Flags
345
371
 
346
372
  | Flag | Description |
@@ -355,6 +381,7 @@ Any trailing positional arguments are treated as paths to lint (default `.`).
355
381
  | `--concurrency <n\|off>` | Override the concurrency heuristic with a fixed worker count. |
356
382
  | `--eslint-args "<args>"` | Extra arguments forwarded verbatim to ESLint. |
357
383
  | `--oxlint-args "<args>"` | Extra arguments forwarded verbatim to oxlint. |
384
+ | `--files0-from <file\|->` | Read the paths to lint from a NUL-delimited UTF-8 list (`-` for stdin); an empty list exits 0. |
358
385
  | `--print` | Print the composed commands without running them. |
359
386
  | `-- <args>` | Forward args to the single selected tool (needs `--eslint`/`--oxlint`). |
360
387
  | `-h`, `--help` | Show help. |
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";
12
+ var version = "6.0.1";
13
13
  var package_default = {
14
14
  name: "@isentinel/eslint-config",
15
15
  version,
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,98 @@
1
+ import { Buffer } from "node:buffer";
2
+ import process from "node:process";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/lint-cli/bootstrap.ts
5
+ /**
6
+ * Engine bootstrap for the lint runner. Spawned as
7
+ * `node <this file> <engine bin>` with the engine's complete argument list as a
8
+ * JSON string array on stdin; it decodes that list, installs it as
9
+ * `process.argv` and then loads the engine's own bin in this same process.
10
+ *
11
+ * It exists to keep file lists off OS command lines: a hook selecting thousands
12
+ * of files would otherwise overflow `CreateProcessW` (32,767 chars) or cmd.exe
13
+ * (8,191) and have to be split into batches, each one starting another lint
14
+ * stack. The command line that launches this process has the same two
15
+ * arguments however many files the run lints.
16
+ *
17
+ * Kept free of imports beyond Node's own so it loads in milliseconds and
18
+ * nothing it pulls in can observe the placeholder `process.argv`.
19
+ */
20
+ /** The exit code for a launch this bootstrap could not complete. */
21
+ const BOOTSTRAP_FAILURE = 2;
22
+ /**
23
+ * Read stdin to its end. Asynchronous rather than `readFileSync(0)`, which can
24
+ * fail with `EAGAIN` on a non-blocking pipe before the writer has finished.
25
+ *
26
+ * @returns The bytes stdin carried.
27
+ */
28
+ async function readStdin() {
29
+ const chunks = [];
30
+ for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8"));
31
+ return Buffer.concat(chunks);
32
+ }
33
+ /**
34
+ * Whether a decoded payload is an array of strings. Restated rather than
35
+ * imported from the package's guards, so this entry stays dependency-free.
36
+ *
37
+ * @param value - The decoded payload.
38
+ * @returns Whether every element is a string.
39
+ */
40
+ function isStringArray(value) {
41
+ return Array.isArray(value) && value.every((element) => typeof element === "string");
42
+ }
43
+ /**
44
+ * Decode the engine argument list the runner streamed in.
45
+ *
46
+ * @param bytes - The raw stdin payload.
47
+ * @returns The arguments, verbatim.
48
+ */
49
+ function decodeArguments(bytes) {
50
+ const decoder = new TextDecoder("utf-8", { fatal: true });
51
+ const parsed = JSON.parse(decoder.decode(bytes));
52
+ if (!isStringArray(parsed)) throw new TypeError("expected a JSON array of argument strings on stdin");
53
+ return parsed;
54
+ }
55
+ /**
56
+ * Read and validate everything the launch needs. Only this phase is the
57
+ * bootstrap's own: its failures are reported as such and exit 2.
58
+ *
59
+ * @returns The launch.
60
+ * @rejects {Error} When the usage, stdin or argument payload is invalid.
61
+ */
62
+ async function prepare() {
63
+ const [bin, ...extra] = process.argv.slice(2);
64
+ if (bin === void 0 || bin.length === 0 || extra.length > 0) throw new Error("usage: node bootstrap <engine bin> (arguments as JSON on stdin)");
65
+ return {
66
+ args: decodeArguments(await readStdin()),
67
+ bin
68
+ };
69
+ }
70
+ /**
71
+ * Prepare the launch, then become the engine.
72
+ *
73
+ * The engine import stays outside the catch around {@link prepare}: its
74
+ * failures are left to Node's default handling, so they surface (stack and
75
+ * exit code included) exactly as they would from a direct `node <bin>` launch.
76
+ *
77
+ * @rejects {Error} Whatever the engine's own module evaluation throws.
78
+ */
79
+ async function main() {
80
+ let launch;
81
+ try {
82
+ launch = await prepare();
83
+ } catch (err) {
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ process.stderr.write(`isentinel-lint: could not start the engine: ${message}\n`);
86
+ process.exitCode = BOOTSTRAP_FAILURE;
87
+ return;
88
+ }
89
+ process.argv = [
90
+ process.execPath,
91
+ launch.bin,
92
+ ...launch.args
93
+ ];
94
+ await import(pathToFileURL(launch.bin).href);
95
+ }
96
+ main();
97
+ //#endregion
98
+ export {};
package/dist/lint-cli.mjs CHANGED
@@ -1,18 +1,20 @@
1
1
  import { createRequire } from "node:module";
2
2
  import process$1 from "node:process";
3
3
  import { isPackageExists } from "local-pkg";
4
+ import { Buffer as Buffer$1 } from "node:buffer";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
4
7
  import yargs from "yargs";
5
8
  import "find-up-simple";
6
- import fs from "node:fs";
7
- import path from "node:path";
9
+ import fs$1 from "node:fs";
8
10
  import "semver";
9
11
  import crypto from "node:crypto";
10
12
  import ansis from "ansis";
11
- import concurrently from "concurrently";
13
+ import { KillOnSignal, LogError, LogExit, LogOutput, Logger, createConcurrently } from "concurrently";
12
14
  import { execFileSync, spawn, spawnSync } from "node:child_process";
15
+ import os, { availableParallelism } from "node:os";
13
16
  import { Writable } from "node:stream";
14
17
  import { fileURLToPath } from "node:url";
15
- import os, { availableParallelism } from "node:os";
16
18
  import fileEntryCache from "file-entry-cache";
17
19
  //#region \0rolldown/runtime.js
18
20
  var __create = Object.create;
@@ -38,7 +40,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
38
40
  }) : target, mod));
39
41
  //#endregion
40
42
  //#region package.json
41
- var version = "6.0.0";
43
+ var version = "6.0.1";
42
44
  //#endregion
43
45
  //#region src/lint-cli/lib/cli/types.ts
44
46
  /**
@@ -48,6 +50,78 @@ var version = "6.0.0";
48
50
  var CliError = class extends Error {
49
51
  name = "CliError";
50
52
  };
53
+ //#endregion
54
+ //#region src/lint-cli/lib/cli/file-list.ts
55
+ /** The `--files0-from` value that names stdin rather than a file. */
56
+ const STDIN_SOURCE = "-";
57
+ /**
58
+ * Split a NUL-delimited, UTF-8 path list (the `git ls-files -z` / `find
59
+ * -print0` shape) into its paths.
60
+ *
61
+ * Every path is terminated by a NUL, though the last terminator may be left off
62
+ * as GNU's own `--files0-from` allows. Nothing else is interpreted: spaces,
63
+ * newlines, quotes, shell metacharacters and leading hyphens all stay part of
64
+ * the name. The bytes must be valid UTF-8 — a lossy decode would lint a file
65
+ * that is not the one selected — and no entry may be empty, since an empty
66
+ * path is never a file and most likely a broken producer.
67
+ *
68
+ * @param bytes - The raw list.
69
+ * @returns The paths, in list order. Empty for an empty list.
70
+ */
71
+ function decodeFileList(bytes) {
72
+ const decoder = new TextDecoder("utf-8", {
73
+ fatal: true,
74
+ ignoreBOM: true
75
+ });
76
+ let text;
77
+ try {
78
+ text = decoder.decode(bytes);
79
+ } catch {
80
+ throw new CliError("--files0-from: the file list is not valid UTF-8.");
81
+ }
82
+ if (text.length === 0) return [];
83
+ const entries = text.split("\0");
84
+ if (entries.at(-1) === "") entries.pop();
85
+ const empty = entries.indexOf("");
86
+ if (empty !== -1) throw new CliError(`--files0-from: entry ${empty + 1} of the file list is empty; expected NUL-terminated paths.`);
87
+ return entries;
88
+ }
89
+ /**
90
+ * Fill the run's paths from its `--files0-from` list, reading it once. Options
91
+ * without a list are returned as they are.
92
+ *
93
+ * An empty list stays empty: the caller treats it as nothing to lint, and it
94
+ * must never become the whole-tree default `.`.
95
+ *
96
+ * @param options - The parsed CLI options.
97
+ * @param cwd - The directory a relative list path resolves against.
98
+ * @param stdin - The stream `-` reads.
99
+ * @returns The options with `paths` set from the list.
100
+ * @rejects {CliError} When the list cannot be read or is malformed.
101
+ */
102
+ async function readFileList(options, cwd, stdin) {
103
+ const source = options.filesFrom;
104
+ if (source === void 0) return options;
105
+ const bytes = source === STDIN_SOURCE ? await readAll(stdin) : await readFile(source, cwd);
106
+ return {
107
+ ...options,
108
+ paths: decodeFileList(bytes)
109
+ };
110
+ }
111
+ async function readFile(source, cwd) {
112
+ try {
113
+ return await fs.readFile(path.resolve(cwd, source));
114
+ } catch (err) {
115
+ throw new CliError(`--files0-from: cannot read "${source}": ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ }
118
+ async function readAll(stream) {
119
+ const chunks = [];
120
+ for await (const chunk of stream) if (typeof chunk === "string") chunks.push(Buffer$1.from(chunk, "utf8"));
121
+ else if (chunk instanceof Uint8Array) chunks.push(Buffer$1.from(chunk));
122
+ else throw new CliError("--files0-from: stdin produced a non-byte chunk.");
123
+ return Buffer$1.concat(chunks);
124
+ }
51
125
  /**
52
126
  * The real TS/JS-family file extensions {@link GLOB_SRC_EXT} encodes (the glob
53
127
  * additionally matches non-existent combinations such as `cjsx`). Kept next to
@@ -288,6 +362,13 @@ function splitArgs(input) {
288
362
  //#endregion
289
363
  //#region src/lint-cli/lib/cli/options.ts
290
364
  /**
365
+ * The ESLint flags that make it lint source text read from its own stdin. The
366
+ * engine's stdin carries its argument list (see `bootstrap.ts`), so these can
367
+ * never work through the runner and are refused rather than silently linting
368
+ * empty input.
369
+ */
370
+ const STDIN_SOURCE_FLAGS = ["--stdin", "--stdin-filename"];
371
+ /**
291
372
  * Parse and validate an `isentinel-lint` argv slice (without the node/bin
292
373
  * prefix). Throws {@link CliError} on any invalid combination.
293
374
  *
@@ -305,7 +386,11 @@ function parseArguments(argv, environment = process$1.env) {
305
386
  const tail = separator === -1 ? [] : argv.slice(separator);
306
387
  const eslintExtract = extractValueOption(head, "eslint-args");
307
388
  const oxlintExtract = extractValueOption(eslintExtract.rest, "oxlint-args");
308
- const parsed = yargs([...oxlintExtract.rest, ...tail]).scriptName("isentinel-lint").parserConfiguration({
389
+ const filesExtract = extractValueOption(oxlintExtract.rest, "files0-from");
390
+ if (filesExtract.occurrences > 1) throw new CliError("Option --files0-from may only be given once.");
391
+ const filesFrom = filesExtract.value;
392
+ if (filesFrom?.length === 0) throw new CliError("Option --files0-from requires a path, or - for stdin.");
393
+ const parsed = yargs([...filesExtract.rest, ...tail]).scriptName("isentinel-lint").parserConfiguration({
309
394
  "boolean-negation": true,
310
395
  "populate--": true
311
396
  }).option("eslint", {
@@ -359,18 +444,22 @@ function parseArguments(argv, environment = process$1.env) {
359
444
  if (eslintOnly) eslintArgs.push(...passthrough);
360
445
  else oxlintArgs.push(...passthrough);
361
446
  }
447
+ const stdinFlag = eslintArgs.find(isStdinSourceFlag);
448
+ if (stdinFlag !== void 0) throw new CliError(`Cannot forward ${stdinFlag} to ESLint: isentinel-lint lints files, not source text on stdin. Run ESLint directly to lint stdin.`);
362
449
  const paths = parsed._.map(String).filter((value) => value.length > 0);
450
+ if (filesFrom !== void 0 && paths.length > 0) throw new CliError("Cannot combine --files0-from with positional paths; put every path in the list.");
363
451
  return {
364
452
  agents: parsed.agents ?? isInAgentSession(environment),
365
453
  cache: parsed.cache,
366
454
  concurrency: parseConcurrency(parsed.concurrency),
367
455
  eslint: eslintOnly,
368
456
  eslintArgs,
457
+ filesFrom,
369
458
  fix,
370
459
  oxlint: oxlintOnly,
371
460
  oxlintArgs,
372
461
  oxlintTypeAware: parsed.oxlintTypeAware,
373
- paths: paths.length > 0 ? paths : ["."],
462
+ paths: filesFrom !== void 0 || paths.length > 0 ? paths : ["."],
374
463
  print: parsed.print === true,
375
464
  typeAware
376
465
  };
@@ -389,6 +478,7 @@ function extractValueOption(argv, name) {
389
478
  const inline = `${flag}=`;
390
479
  const rest = [];
391
480
  let value;
481
+ let occurrences = 0;
392
482
  for (let index = 0; index < argv.length; index += 1) {
393
483
  const token = argv[index];
394
484
  if (token === void 0) continue;
@@ -396,20 +486,26 @@ function extractValueOption(argv, name) {
396
486
  const next = argv[index + 1];
397
487
  if (next === void 0) throw new CliError(`Option ${flag} requires a value.`);
398
488
  value = next;
489
+ occurrences += 1;
399
490
  index += 1;
400
491
  continue;
401
492
  }
402
493
  if (token.startsWith(inline)) {
403
494
  value = token.slice(inline.length);
495
+ occurrences += 1;
404
496
  continue;
405
497
  }
406
498
  rest.push(token);
407
499
  }
408
500
  return {
501
+ occurrences,
409
502
  rest,
410
503
  value
411
504
  };
412
505
  }
506
+ function isStdinSourceFlag(token) {
507
+ return STDIN_SOURCE_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`));
508
+ }
413
509
  function parseConcurrency(value) {
414
510
  if (value === void 0) return;
415
511
  if (value === "off") return "off";
@@ -514,7 +610,7 @@ function statePath(cwd, name, ...parts) {
514
610
  */
515
611
  function readFileIfPresent(filePath) {
516
612
  try {
517
- return fs.readFileSync(filePath, "utf8");
613
+ return fs$1.readFileSync(filePath, "utf8");
518
614
  } catch {
519
615
  return;
520
616
  }
@@ -555,12 +651,12 @@ function writeState(filePath, data) {
555
651
  })}\n`;
556
652
  const temporary = `${filePath}.${process$1.pid}.tmp`;
557
653
  try {
558
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
559
- fs.writeFileSync(temporary, content);
560
- fs.renameSync(temporary, filePath);
654
+ fs$1.mkdirSync(path.dirname(filePath), { recursive: true });
655
+ fs$1.writeFileSync(temporary, content);
656
+ fs$1.renameSync(temporary, filePath);
561
657
  } catch {
562
658
  try {
563
- fs.rmSync(temporary, { force: true });
659
+ fs$1.rmSync(temporary, { force: true });
564
660
  } catch {}
565
661
  }
566
662
  }
@@ -595,7 +691,7 @@ function swapState(filePath, value) {
595
691
  function touchState(filePath) {
596
692
  try {
597
693
  const now = Date.now() / 1e3;
598
- fs.utimesSync(filePath, now, now);
694
+ fs$1.utimesSync(filePath, now, now);
599
695
  } catch {}
600
696
  }
601
697
  /**
@@ -688,7 +784,23 @@ function resolveAgentsFormatter() {
688
784
  */
689
785
  function resolveIgnoredHelper() {
690
786
  const built = fileURLToPath(new URL("./lint-ignored.mjs", import.meta.url));
691
- return fs.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
787
+ return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../ignored-child.ts", import.meta.url));
788
+ }
789
+ /**
790
+ * Resolve the engine bootstrap shipped alongside this entry (built from
791
+ * `src/lint-cli/bootstrap.ts` to `lint-bootstrap.mjs`), which every engine
792
+ * child is launched through so its arguments travel over stdin.
793
+ *
794
+ * Falls back to the TypeScript source exactly as {@link resolveIgnoredHelper}
795
+ * does, for the same reason: the fixture tests run from `src`. Unlike that
796
+ * helper there is no degraded mode — without the bootstrap no engine runs — so
797
+ * the shipped path is checked by the integration tests against the build.
798
+ *
799
+ * @returns The absolute path to the bootstrap module.
800
+ */
801
+ function resolveBootstrap() {
802
+ const built = fileURLToPath(new URL("./lint-bootstrap.mjs", import.meta.url));
803
+ return fs$1.existsSync(built) ? built : fileURLToPath(new URL("../../bootstrap.ts", import.meta.url));
692
804
  }
693
805
  /**
694
806
  * The path a package's manifest declares as the bin of the package's own name,
@@ -735,65 +847,28 @@ function resolveManifest(name, cwd) {
735
847
  }
736
848
  }
737
849
  //#endregion
738
- //#region src/lint-cli/lib/exec/shell.ts
739
- const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
850
+ //#region src/lint-cli/lib/exec/execute.ts
851
+ /** The signals that cancel a run, matching `concurrently`'s `KillOnSignal`. */
852
+ const CANCEL_SIGNALS = [
853
+ "SIGHUP",
854
+ "SIGINT",
855
+ "SIGTERM"
856
+ ];
740
857
  /**
741
- * Render a command as a shell-equivalent line (env prefix + logical binary +
742
- * arguments) for `--print`. Uses POSIX quoting for stable, cross-platform
743
- * output.
744
- *
745
- * @param command - The child command to render.
746
- * @returns The shell-equivalent command line.
858
+ * The first cancelling signal received while this run's children were running.
859
+ * Shared across `execute` calls on purpose: a run is a sequence of them (three
860
+ * for a `--fix` run), and once the user has cancelled one of them none of
861
+ * the rest may start. {@link resetCancellation} Scopes it to one run.
747
862
  */
748
- function formatCommandLine(command) {
749
- const environmentPrefix = Object.entries(command.env).flatMap(([key, value]) => value === void 0 ? [] : [`${key}=${quotePosix(value)}`]).join(" ");
750
- const body = [command.bin, ...command.args].map(quotePosix).join(" ");
751
- return environmentPrefix.length > 0 ? `${environmentPrefix} ${body}` : body;
752
- }
863
+ let cancelledBy;
753
864
  /**
754
- * Build the command string concurrently runs through a shell. The tool is
755
- * launched via `node <binJs>` so no `.cmd`/`.ps1` shim quoting is involved.
756
- *
757
- * @param nodePath - Absolute path to the Node executable.
758
- * @param binJsPath - Absolute path to the tool's JavaScript entry.
759
- * @param args - The tool arguments.
760
- * @param platform - The platform whose shell quoting rules to apply.
761
- * @returns The shell command string.
865
+ * Forget any cancellation recorded by an earlier run in this process. Called
866
+ * as a run starts, so a second `runLint` in the same process is not refused
867
+ * for a signal the first one received.
762
868
  */
763
- function buildShellCommand(nodePath, binJsPath, args, platform) {
764
- const tokens = [
765
- nodePath,
766
- binJsPath,
767
- ...args
768
- ];
769
- if (platform === "win32") {
770
- const offending = tokens.find((token) => token.includes("%"));
771
- if (offending !== void 0) throw new CliError(`Cannot safely pass "${offending}" to cmd.exe: "%" triggers environment-variable expansion even inside quotes. Remove it, or run the tool directly.`);
772
- }
773
- const quote = platform === "win32" ? quoteWindows : quotePosix;
774
- return tokens.map(quote).join(" ");
775
- }
776
- function quotePosix(token) {
777
- if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
778
- return `'${token.replaceAll("'", "'\\''")}'`;
869
+ function resetCancellation() {
870
+ cancelledBy = void 0;
779
871
  }
780
- function quoteWindows(token) {
781
- if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
782
- let escaped = "";
783
- let slashes = 0;
784
- for (const char of token) {
785
- if (char === "\\") {
786
- slashes += 1;
787
- escaped += char;
788
- continue;
789
- }
790
- escaped += char === "\"" ? `${"\\".repeat(slashes)}\\"` : char;
791
- slashes = 0;
792
- }
793
- return `"${escaped}${"\\".repeat(slashes)}"`;
794
- }
795
- //#endregion
796
- //#region src/lint-cli/lib/exec/execute.ts
797
872
  /** Prefix colour per child label for `concurrently`; kept visually distinct. */
798
873
  const PREFIX_COLOR = {
799
874
  eslint: "blue",
@@ -818,7 +893,8 @@ const PREFIX_COLOR = {
818
893
  * @returns The aggregated exit code.
819
894
  */
820
895
  async function execute(commands, cwd, sequential) {
821
- return sequential ? runSequential(commands, cwd) : startGroup(commands, cwd);
896
+ const code = await (sequential ? runSequential(commands, cwd) : startGroup(commands, cwd));
897
+ return cancelledCode() ?? code;
822
898
  }
823
899
  /**
824
900
  * Run a staged set of children: spawn `commands` now, then resolve the rest and
@@ -845,13 +921,19 @@ async function execute(commands, cwd, sequential) {
845
921
  async function executeStaged(commands, cwd, resolveDeferred) {
846
922
  const eager = startGroup(commands, cwd);
847
923
  const deferred = resolveDeferred();
848
- if (deferred.length === 0) return eager;
924
+ await new Promise((resolve) => {
925
+ setImmediate(resolve);
926
+ });
927
+ if (cancelledBy !== void 0 || deferred.length === 0) {
928
+ const eagerCode = await eager;
929
+ return cancelledCode() ?? eagerCode;
930
+ }
849
931
  const held = createHeldOutput();
850
932
  const later = startGroup(deferred, cwd, held.stream);
851
933
  const eagerCode = await eager;
852
934
  held.release();
853
935
  const deferredCode = await later;
854
- return eagerCode === 0 ? deferredCode : eagerCode;
936
+ return cancelledCode() ?? (eagerCode === 0 ? deferredCode : eagerCode);
855
937
  }
856
938
  /**
857
939
  * Announce a child on stderr once it has been spawned. `concurrently` groups
@@ -867,30 +949,162 @@ function announce(command) {
867
949
  process$1.stderr.write(`${prefix} spawned ${command.bin}\n`);
868
950
  }
869
951
  /**
952
+ * The names the children of one group are registered and logged under: the
953
+ * engine, never its arguments. `concurrently` prints the name on each child's
954
+ * exit line, so it must stay short however many files the child lints. The
955
+ * line already carries the pass as its prefix (`[fast] eslint exited with code
956
+ * 1`), so the pass only joins the name when two children share an engine.
957
+ *
958
+ * @param commands - The group's children.
959
+ * @returns Each child paired with a name unique within the group.
960
+ */
961
+ function nameCommands(commands) {
962
+ const taken = /* @__PURE__ */ new Set();
963
+ return commands.map((command) => {
964
+ const base = commands.filter((other) => other.bin === command.bin).length > 1 ? `${command.bin} (${command.label})` : command.bin;
965
+ let name = base;
966
+ let suffix = 1;
967
+ while (taken.has(name)) {
968
+ suffix += 1;
969
+ name = `${base} #${String(suffix)}`;
970
+ }
971
+ taken.add(name);
972
+ return [name, command];
973
+ });
974
+ }
975
+ /**
976
+ * Spawn an engine child through the bootstrap and stream it its arguments.
977
+ *
978
+ * The OS command line is always `node <bootstrap> <bin>`, with no shell, so its
979
+ * length does not depend on the file list and no path is ever interpreted by a
980
+ * shell. The complete argument list follows as JSON on the child's stdin,
981
+ * which is why stdin is always a pipe here whatever the caller asked for.
982
+ *
983
+ * @param launch - The child and its resolved bin.
984
+ * @param options - Spawn options (cwd, env, stdout/stderr handling).
985
+ * @returns The spawned child.
986
+ */
987
+ function launchEngine({ bin, command }, options) {
988
+ const stdio = Array.isArray(options.stdio) ? options.stdio : [];
989
+ const child = spawn(process$1.execPath, [resolveBootstrap(), bin], {
990
+ ...options,
991
+ shell: false,
992
+ stdio: [
993
+ "pipe",
994
+ stdio[1] ?? "inherit",
995
+ stdio[2] ?? "inherit"
996
+ ]
997
+ });
998
+ child.stdin?.on("error", () => {});
999
+ child.stdin?.end(JSON.stringify(command.args));
1000
+ return child;
1001
+ }
1002
+ /**
1003
+ * Kill a group's children and stop it spawning more when its output stream
1004
+ * fails — typically `EPIPE` once the reader of a piped report (`| head`) has
1005
+ * gone. Mirrors `concurrently`'s own `OutputErrorHandler`, which its package
1006
+ * exports do not expose.
1007
+ *
1008
+ * @param outputStream - The stream the group writes to.
1009
+ * @param abortController - Aborts the group.
1010
+ * @returns The flow controller.
1011
+ */
1012
+ function killOnOutputError(outputStream, abortController) {
1013
+ return { handle: (commands) => {
1014
+ function onError() {
1015
+ for (const command of commands) command.kill();
1016
+ abortController.abort();
1017
+ }
1018
+ outputStream.on("error", onError);
1019
+ return {
1020
+ commands,
1021
+ onFinish: () => {
1022
+ outputStream.off("error", onError);
1023
+ }
1024
+ };
1025
+ } };
1026
+ }
1027
+ /**
1028
+ * Record a cancelling signal for as long as children are running, and hand it
1029
+ * to `onSignal`. Listening at all replaces Node's default of exiting on the
1030
+ * signal, which is what lets the children be stopped and reaped first rather
1031
+ * than orphaned.
1032
+ *
1033
+ * @param onSignal - Forwards the signal to the running children; omitted when
1034
+ * something else (`concurrently`'s `KillOnSignal`) already does.
1035
+ * @returns Removes the listeners.
1036
+ */
1037
+ function trackCancellation(onSignal) {
1038
+ function listener(signal) {
1039
+ cancelledBy ??= signal;
1040
+ onSignal?.(signal);
1041
+ }
1042
+ for (const signal of CANCEL_SIGNALS) process$1.on(signal, listener);
1043
+ return () => {
1044
+ for (const signal of CANCEL_SIGNALS) process$1.off(signal, listener);
1045
+ };
1046
+ }
1047
+ /**
870
1048
  * Start every command as one `concurrently` group and announce the children.
871
1049
  * Unlike the previous `killOthersOn: ["failure"]` behaviour, an ordinary lint
872
1050
  * failure in one child no longer kills its siblings — each runs to the end so
873
1051
  * the user keeps every result. The resolved code is non-zero when any child
874
1052
  * exited non-zero; the promise never rejects.
875
1053
  *
1054
+ * Built on `concurrently`'s core rather than its default export, because the
1055
+ * wrapper always launches a command *string* through a shell (`cmd.exe` on
1056
+ * Windows), and that string would have to carry every lint target. Here the
1057
+ * group's spawner launches each engine directly through the bootstrap (see
1058
+ * {@link launchEngine}); the command string is only the label `concurrently`
1059
+ * logs. The controllers are the ones the wrapper wires that do anything under
1060
+ * the options this runner always used — output and error logging, exit lines,
1061
+ * signal forwarding and the broken-output-pipe guard — and nothing else.
1062
+ *
876
1063
  * @param commands - The child commands to run.
877
1064
  * @param cwd - The working directory.
878
1065
  * @param outputStream - Where the group writes its child output; defaults to
879
1066
  * stdout. A staged run passes a held stream (see {@link createHeldOutput}).
880
1067
  * @returns The aggregated exit code.
881
1068
  */
882
- async function startGroup(commands, cwd, outputStream) {
883
- const { result } = concurrently(commands.map((command) => {
1069
+ async function startGroup(commands, cwd, outputStream = process$1.stdout) {
1070
+ if (cancelledBy !== void 0 || commands.length === 0) return 0;
1071
+ const launches = new Map(nameCommands(commands).map(([name, command]) => {
1072
+ return [name, {
1073
+ bin: resolveLocalBin(command.bin, cwd),
1074
+ command
1075
+ }];
1076
+ }));
1077
+ const logger = new Logger({});
1078
+ const abortController = new AbortController();
1079
+ const stopTracking = trackCancellation();
1080
+ const { result } = createConcurrently(Array.from(launches, ([name, { command }]) => {
884
1081
  return {
885
1082
  name: command.label,
886
- command: buildShellCommand(process$1.execPath, resolveLocalBin(command.bin, cwd), command.args, process$1.platform),
1083
+ command: name,
887
1084
  env: command.env,
888
1085
  prefixColor: PREFIX_COLOR[command.label]
889
1086
  };
890
1087
  }), {
1088
+ abortSignal: abortController.signal,
1089
+ controllers: [
1090
+ new LogError({ logger }),
1091
+ new LogOutput({ logger }),
1092
+ new LogExit({ logger }),
1093
+ new KillOnSignal({
1094
+ abortController,
1095
+ process: process$1
1096
+ }),
1097
+ killOnOutputError(outputStream, abortController)
1098
+ ],
891
1099
  cwd,
892
1100
  group: true,
893
- outputStream
1101
+ logger,
1102
+ outputStream,
1103
+ spawn: (name, options) => {
1104
+ const launch = launches.get(name);
1105
+ if (launch === void 0) throw new Error(`isentinel-lint: no engine registered as "${name}".`);
1106
+ return launchEngine(launch, options);
1107
+ }
894
1108
  });
895
1109
  for (const command of commands) announce(command);
896
1110
  try {
@@ -898,12 +1112,29 @@ async function startGroup(commands, cwd, outputStream) {
898
1112
  return 0;
899
1113
  } catch {
900
1114
  return 1;
1115
+ } finally {
1116
+ stopTracking();
901
1117
  }
902
1118
  }
1119
+ function signalExitCode(signal) {
1120
+ return 128 + os.constants.signals[signal];
1121
+ }
1122
+ /**
1123
+ * The exit code for a cancelled run (`128 + signal number`, as a shell reports
1124
+ * it), or `undefined` when nothing cancelled it.
1125
+ *
1126
+ * @returns The exit code, when cancelled.
1127
+ */
1128
+ function cancelledCode() {
1129
+ return cancelledBy === void 0 ? void 0 : signalExitCode(cancelledBy);
1130
+ }
903
1131
  async function spawnChild(command, cwd) {
904
- const binJsPath = resolveLocalBin(command.bin, cwd);
1132
+ const bin = resolveLocalBin(command.bin, cwd);
905
1133
  return new Promise((resolve) => {
906
- const child = spawn(process$1.execPath, [binJsPath, ...command.args], {
1134
+ const child = launchEngine({
1135
+ bin,
1136
+ command
1137
+ }, {
907
1138
  cwd,
908
1139
  env: {
909
1140
  ...process$1.env,
@@ -912,17 +1143,23 @@ async function spawnChild(command, cwd) {
912
1143
  stdio: "inherit"
913
1144
  });
914
1145
  announce(command);
1146
+ const stopTracking = trackCancellation((signal) => {
1147
+ child.kill(signal);
1148
+ });
915
1149
  child.on("error", () => {
1150
+ stopTracking();
916
1151
  resolve(1);
917
1152
  });
918
- child.on("close", (code) => {
919
- resolve(code ?? 1);
1153
+ child.on("close", (code, signal) => {
1154
+ stopTracking();
1155
+ resolve(code ?? (signal === null ? 1 : signalExitCode(signal)));
920
1156
  });
921
1157
  });
922
1158
  }
923
1159
  async function runSequential(commands, cwd) {
924
1160
  let exitCode = 0;
925
1161
  for (const command of commands) {
1162
+ if (cancelledBy !== void 0) break;
926
1163
  const code = await spawnChild(command, cwd);
927
1164
  if (code !== 0) exitCode = code;
928
1165
  }
@@ -961,6 +1198,26 @@ function createHeldOutput() {
961
1198
  };
962
1199
  }
963
1200
  //#endregion
1201
+ //#region src/lint-cli/lib/exec/shell.ts
1202
+ const SAFE_TOKEN = /^[\w@+=:,./-]+$/;
1203
+ /**
1204
+ * Render a command as a shell-equivalent line (env prefix + logical binary +
1205
+ * arguments) for `--print`. Uses POSIX quoting for stable, cross-platform
1206
+ * output.
1207
+ *
1208
+ * @param command - The child command to render.
1209
+ * @returns The shell-equivalent command line.
1210
+ */
1211
+ function formatCommandLine(command) {
1212
+ const environmentPrefix = Object.entries(command.env).flatMap(([key, value]) => value === void 0 ? [] : [`${key}=${quotePosix(value)}`]).join(" ");
1213
+ const body = [command.bin, ...command.args].map(quotePosix).join(" ");
1214
+ return environmentPrefix.length > 0 ? `${environmentPrefix} ${body}` : body;
1215
+ }
1216
+ function quotePosix(token) {
1217
+ if (token.length > 0 && SAFE_TOKEN.test(token)) return token;
1218
+ return `'${token.replaceAll("'", "'\\''")}'`;
1219
+ }
1220
+ //#endregion
964
1221
  //#region src/lint-cli/lib/plan/command.ts
965
1222
  /**
966
1223
  * Compose the oxlint child command.
@@ -974,7 +1231,7 @@ function composeOxlintCommand(options, context) {
974
1231
  if (options.agents) args.push("--format", "agent");
975
1232
  if (context.oxlintTypeAware) args.push("--type-aware");
976
1233
  if (options.fix) args.push("--fix");
977
- args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...context.paths);
1234
+ args.push("--no-error-on-unmatched-pattern", ...options.oxlintArgs, ...pathArguments(context.paths));
978
1235
  return {
979
1236
  args,
980
1237
  bin: "oxlint",
@@ -996,7 +1253,7 @@ function composeEslintCommand(options, context) {
996
1253
  if (options.cache && context.ci) args.push("--cache-strategy", "content");
997
1254
  if (options.agents) args.push("--format", context.agentsFormatterPath);
998
1255
  if (context.fix) args.push("--fix");
999
- args.push(...options.eslintArgs, ...context.paths);
1256
+ args.push(...options.eslintArgs, ...pathArguments(context.paths));
1000
1257
  return {
1001
1258
  args,
1002
1259
  bin: "eslint",
@@ -1004,6 +1261,19 @@ function composeEslintCommand(options, context) {
1004
1261
  label: context.eslintLabel
1005
1262
  };
1006
1263
  }
1264
+ /**
1265
+ * The lint targets as the tail of an engine's argument list. A relative target
1266
+ * that starts with `-` would be read as a flag, so it is passed as `./<path>`.
1267
+ * A `--` separator is not enough: ESLint's bin scans the raw argv for
1268
+ * `--init`, `--mcp`, `--stdin` and `--debug` regardless of it, so a listed file
1269
+ * named `--init` would start the config wizard.
1270
+ *
1271
+ * @param paths - The lint targets.
1272
+ * @returns The arguments that pass them as paths.
1273
+ */
1274
+ function pathArguments(paths) {
1275
+ return paths.map((target) => target.startsWith("-") ? `./${target}` : target);
1276
+ }
1007
1277
  //#endregion
1008
1278
  //#region src/lint-cli/lib/plan/compose.ts
1009
1279
  /** The stderr notice emitted when a lint target resolves outside the cwd. */
@@ -1170,7 +1440,7 @@ function applyHashBust(run, bust, hash) {
1170
1440
  const cleared = [];
1171
1441
  for (const base of bust.caches) {
1172
1442
  const cacheFilePath = path.resolve(run.cwd, cacheFileFor(base, run.key));
1173
- fs.rmSync(cacheFilePath, { force: true });
1443
+ fs$1.rmSync(cacheFilePath, { force: true });
1174
1444
  cleared.push(cacheFilePath);
1175
1445
  }
1176
1446
  return {
@@ -1447,7 +1717,7 @@ function normalizePath(filePath, platform = process$1.platform) {
1447
1717
  * @returns The loaded cache, or `undefined` when the file does not exist.
1448
1718
  */
1449
1719
  function openCache(cacheFilePath, useChecksum) {
1450
- if (!fs.existsSync(cacheFilePath)) return;
1720
+ if (!fs$1.existsSync(cacheFilePath)) return;
1451
1721
  const cache = load(cacheFilePath, useChecksum);
1452
1722
  return {
1453
1723
  filesWithMessages: (files) => filesWithMessagesIn(cache, files),
@@ -1457,7 +1727,7 @@ function openCache(cacheFilePath, useChecksum) {
1457
1727
  }
1458
1728
  function safeMtimeMs(filePath) {
1459
1729
  try {
1460
- return fs.statSync(filePath).mtimeMs;
1730
+ return fs$1.statSync(filePath).mtimeMs;
1461
1731
  } catch {
1462
1732
  return;
1463
1733
  }
@@ -1474,7 +1744,7 @@ function safeMtimeMs(filePath) {
1474
1744
  function listCacheFiles(cwd) {
1475
1745
  let entries;
1476
1746
  try {
1477
- entries = fs.readdirSync(cwd);
1747
+ entries = fs$1.readdirSync(cwd);
1478
1748
  } catch {
1479
1749
  return [];
1480
1750
  }
@@ -1482,7 +1752,7 @@ function listCacheFiles(cwd) {
1482
1752
  }
1483
1753
  function removeCacheFile(cacheFilePath) {
1484
1754
  try {
1485
- fs.rmSync(cacheFilePath, { force: true });
1755
+ fs$1.rmSync(cacheFilePath, { force: true });
1486
1756
  } catch {}
1487
1757
  }
1488
1758
  /**
@@ -3491,7 +3761,7 @@ const WALK_BUDGET = 8192;
3491
3761
  function findWorkspaceRoot(cwd) {
3492
3762
  let current = cwd;
3493
3763
  for (;;) {
3494
- for (const marker of WORKSPACE_ROOT_MARKERS) if (fs.existsSync(path.join(current, marker))) return current;
3764
+ for (const marker of WORKSPACE_ROOT_MARKERS) if (fs$1.existsSync(path.join(current, marker))) return current;
3495
3765
  const parent = path.dirname(current);
3496
3766
  if (parent === current) return cwd;
3497
3767
  current = parent;
@@ -3535,7 +3805,7 @@ function findWorkspaceMembers(root) {
3535
3805
  function walk(directory, depth) {
3536
3806
  let entries;
3537
3807
  try {
3538
- entries = fs.readdirSync(directory, { withFileTypes: true });
3808
+ entries = fs$1.readdirSync(directory, { withFileTypes: true });
3539
3809
  } catch {
3540
3810
  return true;
3541
3811
  }
@@ -3545,7 +3815,7 @@ function findWorkspaceMembers(root) {
3545
3815
  if (budget < 0) return false;
3546
3816
  const absolute = path.join(directory, entry.name);
3547
3817
  const relative = toPosix(path.relative(root, absolute));
3548
- if (isMember(relative) && fs.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
3818
+ if (isMember(relative) && fs$1.existsSync(path.join(absolute, "package.json"))) members.push(absolute);
3549
3819
  if (depth < maxDepth && !walk(absolute, depth + 1)) return false;
3550
3820
  }
3551
3821
  return true;
@@ -3873,7 +4143,7 @@ function oxlintTargets(cwd, targets) {
3873
4143
  const extension = path.extname(target).toLowerCase();
3874
4144
  if (extension === "" || TYPE_AWARE_EXTENSION_SET.has(extension)) return true;
3875
4145
  try {
3876
- return fs.statSync(path.resolve(cwd, target)).isDirectory();
4146
+ return fs$1.statSync(path.resolve(cwd, target)).isDirectory();
3877
4147
  } catch {
3878
4148
  return false;
3879
4149
  }
@@ -3910,7 +4180,7 @@ function collectAncestorBustFiles(cwd) {
3910
4180
  for (;;) {
3911
4181
  let entries = [];
3912
4182
  try {
3913
- entries = fs.readdirSync(current, { withFileTypes: true });
4183
+ entries = fs$1.readdirSync(current, { withFileTypes: true });
3914
4184
  } catch {
3915
4185
  entries = [];
3916
4186
  }
@@ -3933,7 +4203,7 @@ function collectAncestorBustFiles(cwd) {
3933
4203
  * @returns The normalized cwd-relative posix target.
3934
4204
  */
3935
4205
  function normalizeTarget(target, cwd) {
3936
- let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : toPosix(target);
4206
+ let value = path.isAbsolute(target) ? toPosix(path.relative(cwd, target)) : path.posix.normalize(toPosix(target));
3937
4207
  if (value.startsWith("./")) value = value.slice(2);
3938
4208
  while (value.endsWith("/")) value = value.slice(0, -1);
3939
4209
  return value;
@@ -3960,8 +4230,15 @@ function isOutsideCwd(normalizedTarget) {
3960
4230
  */
3961
4231
  function matchTargets(normalized) {
3962
4232
  if (normalized.some((target) => target === "" || target === ".")) return () => true;
4233
+ const targets = new Set(normalized);
3963
4234
  return (relative) => {
3964
- return normalized.some((target) => relative === target || relative.startsWith(`${target}/`));
4235
+ let candidate = relative;
4236
+ for (;;) {
4237
+ if (targets.has(candidate)) return true;
4238
+ const slash = candidate.lastIndexOf("/");
4239
+ if (slash === -1) return false;
4240
+ candidate = candidate.slice(0, slash);
4241
+ }
3965
4242
  };
3966
4243
  }
3967
4244
  function isTypeAwareFile(filePath) {
@@ -3974,6 +4251,7 @@ function gitListFiles(cwd, pathSpecs) {
3974
4251
  try {
3975
4252
  return execFileSync("git", [
3976
4253
  "ls-files",
4254
+ "-z",
3977
4255
  "--cached",
3978
4256
  "--others",
3979
4257
  "--exclude-standard",
@@ -3983,7 +4261,7 @@ function gitListFiles(cwd, pathSpecs) {
3983
4261
  cwd,
3984
4262
  encoding: "utf8",
3985
4263
  maxBuffer: 64 * 1024 * 1024
3986
- }).split("\n").filter((line) => line.length > 0);
4264
+ }).split("\0").filter((line) => line.length > 0);
3987
4265
  } catch {
3988
4266
  return;
3989
4267
  }
@@ -3991,7 +4269,7 @@ function gitListFiles(cwd, pathSpecs) {
3991
4269
  function walkDirectory(root, current, accumulator) {
3992
4270
  let entries;
3993
4271
  try {
3994
- entries = fs.readdirSync(current, { withFileTypes: true });
4272
+ entries = fs$1.readdirSync(current, { withFileTypes: true });
3995
4273
  } catch {
3996
4274
  return;
3997
4275
  }
@@ -4010,7 +4288,7 @@ function walkFallback(cwd, targets) {
4010
4288
  const absolute = path.resolve(cwd, target);
4011
4289
  let stat;
4012
4290
  try {
4013
- stat = fs.statSync(absolute);
4291
+ stat = fs$1.statSync(absolute);
4014
4292
  } catch {
4015
4293
  continue;
4016
4294
  }
@@ -4049,11 +4327,12 @@ function gitIgnoredTargets(cwd, targets) {
4049
4327
  const output = execFileSync("git", [
4050
4328
  "check-ignore",
4051
4329
  "--no-index",
4052
- "--stdin"
4330
+ "--stdin",
4331
+ "-z"
4053
4332
  ], {
4054
4333
  cwd,
4055
4334
  encoding: "utf8",
4056
- input: targets.join("\n"),
4335
+ input: `${targets.join("\0")}\0`,
4057
4336
  maxBuffer: 64 * 1024 * 1024,
4058
4337
  stdio: [
4059
4338
  "pipe",
@@ -4061,7 +4340,7 @@ function gitIgnoredTargets(cwd, targets) {
4061
4340
  "ignore"
4062
4341
  ]
4063
4342
  });
4064
- return new Set(output.split("\n").map((line) => line.trim()).filter(Boolean));
4343
+ return new Set(output.split("\0").filter(Boolean));
4065
4344
  } catch {
4066
4345
  return /* @__PURE__ */ new Set();
4067
4346
  }
@@ -4334,7 +4613,7 @@ function queryIgnoredFiles(cwd, targets) {
4334
4613
  "ignore"
4335
4614
  ]
4336
4615
  });
4337
- const parsed = JSON.parse(fs.readFileSync(outFile, "utf8"));
4616
+ const parsed = JSON.parse(fs$1.readFileSync(outFile, "utf8"));
4338
4617
  if (!isRecord(parsed)) return;
4339
4618
  const { basePath, entries, ignored, mode } = parsed;
4340
4619
  if (mode === "answers" && isStringArray(ignored)) return {
@@ -4350,7 +4629,7 @@ function queryIgnoredFiles(cwd, targets) {
4350
4629
  } catch {
4351
4630
  return;
4352
4631
  } finally {
4353
- fs.rmSync(outFile, { force: true });
4632
+ fs$1.rmSync(outFile, { force: true });
4354
4633
  }
4355
4634
  }
4356
4635
  //#endregion
@@ -4393,7 +4672,7 @@ function readHybridStatus(cwd) {
4393
4672
  function writeHybridStatus(cwd, oxlint) {
4394
4673
  let installed;
4395
4674
  try {
4396
- installed = fs.existsSync(path.resolve(cwd, "node_modules"));
4675
+ installed = fs$1.existsSync(path.resolve(cwd, "node_modules"));
4397
4676
  } catch {
4398
4677
  return;
4399
4678
  }
@@ -5096,7 +5375,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
5096
5375
  if (entryReadable) warnOnce(`no TypeScript files resolved from ${path.basename(configPath)}; skipping type-aware cache invalidation`);
5097
5376
  return;
5098
5377
  }
5099
- fs.mkdirSync(stateDirectory(cwd), { recursive: true });
5378
+ fs$1.mkdirSync(stateDirectory(cwd), { recursive: true });
5100
5379
  const resolutionGate = computeResolutionGate(cwd);
5101
5380
  const affected = /* @__PURE__ */ new Set();
5102
5381
  let warmProjects = 0;
@@ -5143,7 +5422,7 @@ function computeAffectedFiles({ key, cwd }, mode) {
5143
5422
  function hasBuilderState({ key, cwd }, mode) {
5144
5423
  const prefix = `${path.basename(statePath(cwd, BUILD_INFO_STATE, modeSuffix(mode), key))}-`;
5145
5424
  try {
5146
- return fs.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
5425
+ return fs$1.readdirSync(stateDirectory(cwd)).some((name) => name.startsWith(prefix) && !name.endsWith(".tmp"));
5147
5426
  } catch {
5148
5427
  return false;
5149
5428
  }
@@ -5358,10 +5637,10 @@ function persistBuilderState(builder, buildInfoPath) {
5358
5637
  if (path.normalize(fileName) !== normalizedBuildInfo) return;
5359
5638
  const temporary = `${fileName}.${process$1.pid}.tmp`;
5360
5639
  try {
5361
- fs.writeFileSync(temporary, data);
5362
- fs.renameSync(temporary, fileName);
5640
+ fs$1.writeFileSync(temporary, data);
5641
+ fs$1.renameSync(temporary, fileName);
5363
5642
  } catch (err) {
5364
- fs.rmSync(temporary, { force: true });
5643
+ fs$1.rmSync(temporary, { force: true });
5365
5644
  throw err;
5366
5645
  }
5367
5646
  });
@@ -5376,7 +5655,7 @@ function persistBuilderState(builder, buildInfoPath) {
5376
5655
  */
5377
5656
  function writeGateState(gatePath, gateValue) {
5378
5657
  if (gateValue === void 0) {
5379
- fs.rmSync(gatePath, { force: true });
5658
+ fs$1.rmSync(gatePath, { force: true });
5380
5659
  return;
5381
5660
  }
5382
5661
  writeState(gatePath, gateValue);
@@ -5397,7 +5676,7 @@ function writeGateState(gatePath, gateValue) {
5397
5676
  * @returns The affected result.
5398
5677
  */
5399
5678
  function runBuilder({ buildInfoPath, gatePath, project, resolutionGate, ts }) {
5400
- const firstRun = !fs.existsSync(buildInfoPath);
5679
+ const firstRun = !fs$1.existsSync(buildInfoPath);
5401
5680
  const options = {
5402
5681
  ...project.options,
5403
5682
  composite: false,
@@ -5480,7 +5759,7 @@ function applyTypeAwareInvalidation(run, { alreadyDirty, cache, cacheLocation, m
5480
5759
  }
5481
5760
  const threshold = resolveAffectedBustThreshold(run.environment);
5482
5761
  if (affectedTargets.length > threshold) {
5483
- fs.rmSync(cacheLocation, { force: true });
5762
+ fs$1.rmSync(cacheLocation, { force: true });
5484
5763
  return {
5485
5764
  busted: true,
5486
5765
  firstRun: false,
@@ -5767,11 +6046,15 @@ function stageDescriptors(descriptors, run, staging) {
5767
6046
  * @param argv - The argument slice (without the node/bin prefix).
5768
6047
  * @param cwd - The working directory (defaults to `process.cwd()`; injected in tests).
5769
6048
  * @param environment - The process environment (defaults to `process.env`).
6049
+ * @param stdin - Where `--files0-from -` reads its list (defaults to
6050
+ * `process.stdin`).
5770
6051
  * @returns The process exit code.
5771
6052
  * @rejects {CliError} When the arguments are invalid or a tool is missing.
5772
6053
  */
5773
- async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env) {
5774
- const options = parseArguments(argv, environment);
6054
+ async function runLint(argv, cwd = process$1.cwd(), environment = process$1.env, stdin = process$1.stdin) {
6055
+ resetCancellation();
6056
+ const options = await readFileList(parseArguments(argv, environment), cwd, stdin);
6057
+ if (options.paths.length === 0) return 0;
5775
6058
  const staged = plan(options, resolveRunContext(cwd, environment, !options.print));
5776
6059
  const { commands, notice } = compose(staged.eager, options);
5777
6060
  if (options.print) {
@@ -5824,7 +6107,7 @@ async function runChecks(commands, cwd, staged, options) {
5824
6107
  }
5825
6108
  //#endregion
5826
6109
  //#region src/lint-cli/index.ts
5827
- const HELP = `isentinel-lint [flags] [paths...]
6110
+ const HELP = `isentinel-lint [flags] [paths... | --files0-from <file|->]
5828
6111
 
5829
6112
  Runs oxlint and ESLint together, sizing ESLint's --concurrency from how many
5830
6113
  files actually need re-linting and managing per-mode caches.
@@ -5844,6 +6127,9 @@ Flags:
5844
6127
  --concurrency <n|off> Override the concurrency heuristic.
5845
6128
  --eslint-args "<args>" Extra arguments for ESLint.
5846
6129
  --oxlint-args "<args>" Extra arguments for oxlint.
6130
+ --files0-from <file|-> Read the paths to lint from a NUL-delimited UTF-8
6131
+ list (- for stdin) instead of positional arguments.
6132
+ An empty list lints nothing and exits 0.
5847
6133
  --print Print the composed commands without running them.
5848
6134
  -- <args> Forward args to the single selected tool.
5849
6135
  -h, --help Show this help.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/eslint-config",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "iSentinel's ESLint config",
5
5
  "keywords": [
6
6
  "eslint-config",
@@ -145,7 +145,7 @@
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"
148
+ "@isentinel/eslint-config": "6.0.1"
149
149
  },
150
150
  "peerDependencies": {
151
151
  "@vitest/eslint-plugin": "^1.6.4",