lintmax 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,1687 @@
1
+ #!/usr/bin/env bun
2
+ import { i as DEFAULT_SHARED_IGNORE_PATTERNS, o as OXLINT_CLI_ALLOW } from "./constants-BRYEJmsZ.mjs";
3
+ import { i as joinPath, n as fromFileUrl, t as dirnamePath } from "./path-Cu_Nf2ct.mjs";
4
+ import { _ as run, a as PRETTIER_MD_ARGS, b as writeJson, c as decodeText, d as lintmaxRoot, f as pathExists, g as resolveBin, h as readVersion, i as CliExitError, l as ensureDirectory, m as readRequiredJson, o as cacheDir, p as readJson, r as sync, s as cwd, u as ignoreEntries, v as runCapture, y as usage } from "./src-D4isRHIx.mjs";
5
+ import { Glob, env, file, spawnSync, write } from "bun";
6
+ import { lstatSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import ts from "typescript";
9
+ //#region src/init.ts
10
+ const initScripts = async ({ pkg, pkgPath }) => {
11
+ const scripts = pkg.scripts ?? {};
12
+ let changed = false;
13
+ if (!scripts.fix) {
14
+ scripts.fix = "lintmax fix";
15
+ changed = true;
16
+ }
17
+ if (!scripts.check) {
18
+ scripts.check = "lintmax check";
19
+ changed = true;
20
+ }
21
+ if (!changed) return;
22
+ pkg.scripts = scripts;
23
+ await writeJson({
24
+ data: pkg,
25
+ path: pkgPath
26
+ });
27
+ };
28
+ const initTsconfig = async ({ configFiles }) => {
29
+ const tsconfigPath = joinPath(cwd, "tsconfig.json");
30
+ if (!await pathExists({ path: tsconfigPath })) {
31
+ const tsconfig = { extends: "lintmax/tsconfig" };
32
+ if (configFiles.length > 0) tsconfig.include = configFiles;
33
+ await writeJson({
34
+ data: tsconfig,
35
+ path: tsconfigPath
36
+ });
37
+ return;
38
+ }
39
+ try {
40
+ const tsconfig = await readRequiredJson({ path: tsconfigPath });
41
+ let changed = false;
42
+ if (tsconfig.extends !== "lintmax/tsconfig") {
43
+ tsconfig.extends = "lintmax/tsconfig";
44
+ changed = true;
45
+ }
46
+ const toAdd = configFiles.filter((f) => !(tsconfig.include ?? []).includes(f));
47
+ if (toAdd.length > 0) {
48
+ tsconfig.include = [...tsconfig.include ?? [], ...toAdd];
49
+ changed = true;
50
+ }
51
+ if (changed) await writeJson({
52
+ data: tsconfig,
53
+ path: tsconfigPath
54
+ });
55
+ } catch {
56
+ process.stderr.write("tsconfig.json: could not parse, add \"extends\": \"lintmax/tsconfig\" manually\n");
57
+ }
58
+ };
59
+ const initGitignore = async () => {
60
+ const gitignorePath = joinPath(cwd, ".gitignore");
61
+ if (await pathExists({ path: gitignorePath })) {
62
+ const content = await file(gitignorePath).text();
63
+ const toAdd = [];
64
+ for (const entry of ignoreEntries) if (!content.includes(entry)) toAdd.push(entry);
65
+ if (toAdd.length > 0) await write(gitignorePath, `${content.trimEnd()}\n${toAdd.join("\n")}\n`);
66
+ return;
67
+ }
68
+ await write(gitignorePath, `${ignoreEntries.join("\n")}\n`);
69
+ };
70
+ const findLegacyConfigs = async () => {
71
+ const checks = [
72
+ ".eslintrc",
73
+ ".eslintrc.json",
74
+ ".eslintrc.js",
75
+ ".eslintrc.cjs",
76
+ ".eslintrc.yml",
77
+ ".eslintrc.yaml",
78
+ ".prettierrc",
79
+ ".prettierrc.json",
80
+ ".prettierrc.js",
81
+ ".prettierrc.yml",
82
+ ".prettierrc.yaml",
83
+ ".prettierrc.toml",
84
+ "biome.json",
85
+ "biome.jsonc",
86
+ ".oxlintrc.json"
87
+ ].map(async (configFile) => ({
88
+ configFile,
89
+ exists: await pathExists({ path: joinPath(cwd, configFile) })
90
+ }));
91
+ const resolved = await Promise.all(checks);
92
+ const found = [];
93
+ for (const item of resolved) if (item.exists) found.push(item.configFile);
94
+ return found;
95
+ };
96
+ const runInit = async () => {
97
+ const pkgPath = joinPath(cwd, "package.json");
98
+ if (!await pathExists({ path: pkgPath })) throw new CliExitError({
99
+ code: 1,
100
+ message: "No package.json found"
101
+ });
102
+ const pkg = await readRequiredJson({ path: pkgPath });
103
+ const configFiles = [];
104
+ if (await pathExists({ path: joinPath(cwd, "lintmax.config.ts") })) configFiles.push("lintmax.config.ts");
105
+ await initScripts({
106
+ pkg,
107
+ pkgPath
108
+ });
109
+ await initTsconfig({ configFiles });
110
+ await initGitignore();
111
+ const foundLegacy = await findLegacyConfigs();
112
+ process.stdout.write("tsconfig.json extends lintmax/tsconfig");
113
+ if (configFiles.length > 0) process.stdout.write(`, include: ${configFiles.join(", ")}`);
114
+ process.stdout.write("\n");
115
+ process.stdout.write("package.json \"fix\": \"lintmax fix\", \"check\": \"lintmax check\"\n");
116
+ process.stdout.write(`.gitignore ${ignoreEntries.join(", ")}\n`);
117
+ if (foundLegacy.length > 0) process.stdout.write(`\nLegacy configs found (can be removed): ${foundLegacy.join(", ")}\n`);
118
+ process.stdout.write("\nRun: bun fix\n");
119
+ };
120
+ //#endregion
121
+ //#region src/rule-equivalence.ts
122
+ const equivalenceGroups = [
123
+ [
124
+ "lint/suspicious/noExplicitAny",
125
+ "@typescript-eslint/no-explicit-any",
126
+ "typescript-eslint(no-explicit-any)"
127
+ ],
128
+ [
129
+ "lint/correctness/noUnusedVariables",
130
+ "@typescript-eslint/no-unused-vars",
131
+ "eslint(no-unused-vars)"
132
+ ],
133
+ [
134
+ "lint/suspicious/noDebugger",
135
+ "no-debugger",
136
+ "eslint(no-debugger)"
137
+ ],
138
+ [
139
+ "lint/correctness/noUnreachable",
140
+ "no-unreachable",
141
+ "eslint(no-unreachable)"
142
+ ],
143
+ [
144
+ "lint/suspicious/noDoubleEquals",
145
+ "eqeqeq",
146
+ "eslint(eqeqeq)"
147
+ ],
148
+ [
149
+ "lint/suspicious/noDuplicateCase",
150
+ "no-duplicate-case",
151
+ "eslint(no-duplicate-case)"
152
+ ],
153
+ [
154
+ "lint/suspicious/noFallthroughSwitchClause",
155
+ "no-fallthrough",
156
+ "eslint(no-fallthrough)"
157
+ ],
158
+ [
159
+ "lint/suspicious/noRedeclare",
160
+ "@typescript-eslint/no-redeclare",
161
+ "typescript-eslint(no-redeclare)"
162
+ ],
163
+ [
164
+ "lint/suspicious/noShadowRestrictedNames",
165
+ "no-shadow-restricted-names",
166
+ "eslint(no-shadow-restricted-names)"
167
+ ],
168
+ [
169
+ "lint/correctness/useIsNan",
170
+ "use-isnan",
171
+ "eslint(use-isnan)"
172
+ ],
173
+ [
174
+ "lint/correctness/noConstAssign",
175
+ "no-const-assign",
176
+ "eslint(no-const-assign)"
177
+ ],
178
+ [
179
+ "lint/correctness/noNewSymbol",
180
+ "no-new-symbol",
181
+ "eslint(no-new-symbol)"
182
+ ],
183
+ [
184
+ "lint/correctness/noUndeclaredVariables",
185
+ "no-undef",
186
+ "eslint(no-undef)"
187
+ ],
188
+ [
189
+ "lint/suspicious/noEmptyBlockStatements",
190
+ "no-empty",
191
+ "eslint(no-empty)"
192
+ ],
193
+ [
194
+ "lint/suspicious/noSelfCompare",
195
+ "no-self-compare",
196
+ "eslint(no-self-compare)"
197
+ ],
198
+ [
199
+ "lint/complexity/noUselessConstructor",
200
+ "@typescript-eslint/no-useless-constructor",
201
+ "eslint(no-useless-constructor)"
202
+ ],
203
+ [
204
+ "lint/suspicious/noArrayIndexKey",
205
+ "react/no-array-index-key",
206
+ "eslint-plugin-react(no-array-index-key)"
207
+ ],
208
+ [
209
+ "lint/correctness/useExhaustiveDependencies",
210
+ "react-hooks/exhaustive-deps",
211
+ "react-hooks(exhaustive-deps)"
212
+ ],
213
+ [
214
+ "lint/correctness/useHookAtTopLevel",
215
+ "react-hooks/rules-of-hooks",
216
+ "react-hooks(rules-of-hooks)"
217
+ ]
218
+ ];
219
+ const ruleToCanonical = /* @__PURE__ */ new Map();
220
+ for (const group of equivalenceGroups) {
221
+ const canonical = group[0] ?? "";
222
+ for (const rule of group) ruleToCanonical.set(rule, canonical);
223
+ }
224
+ const getCanonicalRule = (rule) => ruleToCanonical.get(rule) ?? rule;
225
+ //#endregion
226
+ //#region src/aggregate.ts
227
+ const LINTER_PRIORITY = {
228
+ biome: 0,
229
+ eslint: 2,
230
+ oxlint: 1,
231
+ prettier: 3,
232
+ "sort-package-json": 4
233
+ };
234
+ const cwdPrefix = `${process.cwd()}/`;
235
+ const normalizePath = (filePath) => {
236
+ if (filePath.startsWith(cwdPrefix)) return filePath.slice(cwdPrefix.length);
237
+ return filePath;
238
+ };
239
+ const parseBiomeDiagnostics = ({ stdout }) => {
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(stdout);
243
+ } catch {
244
+ return [];
245
+ }
246
+ if (!Array.isArray(parsed.diagnostics)) return [];
247
+ const results = [];
248
+ for (const d of parsed.diagnostics) {
249
+ const filePath = d.location?.path;
250
+ const { category } = d;
251
+ if (filePath && category) {
252
+ const line = d.location?.start?.line ?? 0;
253
+ results.push({
254
+ file: normalizePath(filePath),
255
+ line,
256
+ linter: "biome",
257
+ rule: category
258
+ });
259
+ }
260
+ }
261
+ return results;
262
+ };
263
+ const parseOxlintDiagnostics = ({ stdout }) => {
264
+ let parsed;
265
+ try {
266
+ parsed = JSON.parse(stdout);
267
+ } catch {
268
+ return [];
269
+ }
270
+ if (!Array.isArray(parsed.diagnostics)) return [];
271
+ const results = [];
272
+ for (const d of parsed.diagnostics) {
273
+ const filePath = d.filename ?? "";
274
+ const rule = d.code;
275
+ if (rule) {
276
+ const line = d.labels?.[0]?.span?.line ?? 0;
277
+ results.push({
278
+ file: filePath.length > 0 ? normalizePath(filePath) : "<unknown>",
279
+ line,
280
+ linter: "oxlint",
281
+ rule
282
+ });
283
+ }
284
+ }
285
+ return results;
286
+ };
287
+ const parseEslintDiagnostics = ({ stdout }) => {
288
+ let parsed;
289
+ try {
290
+ parsed = JSON.parse(stdout);
291
+ } catch {
292
+ return [];
293
+ }
294
+ if (!Array.isArray(parsed)) return [];
295
+ const results = [];
296
+ for (const fileEntry of parsed) {
297
+ const { filePath } = fileEntry;
298
+ if (filePath && Array.isArray(fileEntry.messages)) for (const msg of fileEntry.messages) {
299
+ const rule = msg.ruleId ?? (msg.severity === 2 || msg.fatal ? `parse-error: ${msg.message?.slice(0, 80) ?? "unknown"}` : null);
300
+ if (rule) results.push({
301
+ file: normalizePath(filePath),
302
+ line: msg.line ?? 0,
303
+ linter: "eslint",
304
+ rule
305
+ });
306
+ }
307
+ }
308
+ return results;
309
+ };
310
+ const parsePrettierOutput = ({ stdout }) => {
311
+ const results = [];
312
+ const lines = stdout.trim().split("\n");
313
+ for (const line of lines) {
314
+ const filePath = line.trim();
315
+ if (filePath.length > 0) results.push({
316
+ file: normalizePath(filePath),
317
+ line: 0,
318
+ linter: "prettier",
319
+ rule: "unformatted"
320
+ });
321
+ }
322
+ return results;
323
+ };
324
+ const parseSortPackageJsonOutput = ({ exitCode, stdout }) => {
325
+ if (exitCode === 0) return [];
326
+ const results = [];
327
+ const lines = stdout.trim().split("\n");
328
+ for (const line of lines) {
329
+ const filePath = line.trim();
330
+ if (filePath.length > 0 && filePath.endsWith(".json")) results.push({
331
+ file: normalizePath(filePath),
332
+ line: 0,
333
+ linter: "sort-package-json",
334
+ rule: "unsorted"
335
+ });
336
+ }
337
+ return results;
338
+ };
339
+ const dedup = (diagnostics) => {
340
+ const seen = /* @__PURE__ */ new Map();
341
+ for (const d of diagnostics) if (d.line === 0) {
342
+ const key = `${d.file}\0${d.linter}\0${d.rule}`;
343
+ seen.set(key, d);
344
+ } else {
345
+ const canonical = getCanonicalRule(d.rule);
346
+ const key = `${d.file}\0${d.line}\0${canonical}`;
347
+ const existing = seen.get(key);
348
+ if (!existing || (LINTER_PRIORITY[d.linter] ?? 99) < (LINTER_PRIORITY[existing.linter] ?? 99)) seen.set(key, d);
349
+ }
350
+ return [...seen.values()];
351
+ };
352
+ const linterSort = (a, b) => (LINTER_PRIORITY[a] ?? 99) - (LINTER_PRIORITY[b] ?? 99);
353
+ const aggregate = ({ diagnostics }) => {
354
+ const deduped = dedup(diagnostics);
355
+ const fileMap = /* @__PURE__ */ new Map();
356
+ for (const d of deduped) {
357
+ let linterMap = fileMap.get(d.file);
358
+ if (!linterMap) {
359
+ linterMap = /* @__PURE__ */ new Map();
360
+ fileMap.set(d.file, linterMap);
361
+ }
362
+ let ruleMap = linterMap.get(d.linter);
363
+ if (!ruleMap) {
364
+ ruleMap = /* @__PURE__ */ new Map();
365
+ linterMap.set(d.linter, ruleMap);
366
+ }
367
+ let lines = ruleMap.get(d.rule);
368
+ if (!lines) {
369
+ lines = [];
370
+ ruleMap.set(d.rule, lines);
371
+ }
372
+ if (d.line > 0) lines.push(d.line);
373
+ }
374
+ const files = [];
375
+ const sortedFiles = [...fileMap.keys()].toSorted();
376
+ for (const filePath of sortedFiles) {
377
+ const linterMap = fileMap.get(filePath) ?? /* @__PURE__ */ new Map();
378
+ const linters = [];
379
+ const sortedLinters = [...linterMap.keys()].toSorted(linterSort);
380
+ for (const linterName of sortedLinters) {
381
+ const ruleMap = linterMap.get(linterName) ?? /* @__PURE__ */ new Map();
382
+ const rules = [];
383
+ const sortedRules = [...ruleMap.keys()].toSorted();
384
+ for (const ruleName of sortedRules) {
385
+ const lines = ruleMap.get(ruleName) ?? [];
386
+ const uniqueLines = [...new Set(lines)].toSorted((a, b) => a - b);
387
+ rules.push({
388
+ lines: uniqueLines,
389
+ rule: ruleName
390
+ });
391
+ }
392
+ linters.push({
393
+ linter: linterName,
394
+ rules
395
+ });
396
+ }
397
+ files.push({
398
+ file: filePath,
399
+ linters
400
+ });
401
+ }
402
+ return files;
403
+ };
404
+ //#endregion
405
+ //#region src/class-name.ts
406
+ /** biome-ignore-all lint/nursery/noContinue: AST traversal requires continue */
407
+ const CN_NAMES = new Set(["cn"]);
408
+ const BANNED_CALLEE_NAMES = new Set([
409
+ "classnames",
410
+ "clsx",
411
+ "cx",
412
+ "twMerge"
413
+ ]);
414
+ const isJsxClassName = (node) => ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className";
415
+ const isCallToCn = (node) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && CN_NAMES.has(node.expression.text);
416
+ const isJoinCall = (node) => ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "join";
417
+ const isBannedCallee = (node) => ts.isCallExpression(node) && ts.isIdentifier(node.expression) && BANNED_CALLEE_NAMES.has(node.expression.text);
418
+ const isStringLiteral = (node) => ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node);
419
+ const findClassNameViolations = ({ sourceText }) => {
420
+ const sourceFile = ts.createSourceFile("file.tsx", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
421
+ const violations = [];
422
+ const visit = (node) => {
423
+ if (isJsxClassName(node)) {
424
+ const init = node.initializer;
425
+ if (init && ts.isJsxExpression(init) && init.expression) {
426
+ const expr = init.expression;
427
+ if (!(isStringLiteral(expr) || isCallToCn(expr))) {
428
+ const line = sourceFile.getLineAndCharacterOfPosition(expr.getStart()).line + 1;
429
+ if (ts.isTemplateLiteral(expr)) violations.push({
430
+ line,
431
+ rule: "cn/no-template-literal"
432
+ });
433
+ else if (ts.isConditionalExpression(expr)) violations.push({
434
+ line,
435
+ rule: "cn/no-ternary"
436
+ });
437
+ else if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.PlusToken) violations.push({
438
+ line,
439
+ rule: "cn/no-concatenation"
440
+ });
441
+ else if (isBannedCallee(expr)) violations.push({
442
+ line,
443
+ rule: "cn/no-banned-callee"
444
+ });
445
+ else if (isJoinCall(expr)) violations.push({
446
+ line,
447
+ rule: "cn/no-join"
448
+ });
449
+ else if (ts.isCallExpression(expr) && !isCallToCn(expr)) {
450
+ const callee = expr.expression;
451
+ if (ts.isIdentifier(callee) && BANNED_CALLEE_NAMES.has(callee.text)) violations.push({
452
+ line,
453
+ rule: "cn/no-banned-callee"
454
+ });
455
+ }
456
+ }
457
+ }
458
+ }
459
+ if (isBannedCallee(node)) {
460
+ const call = node;
461
+ const { parent } = call;
462
+ if (!(ts.isJsxExpression(parent) && isJsxClassName(parent.parent))) {
463
+ const line = sourceFile.getLineAndCharacterOfPosition(call.getStart()).line + 1;
464
+ violations.push({
465
+ line,
466
+ rule: "cn/no-banned-callee"
467
+ });
468
+ }
469
+ }
470
+ ts.forEachChild(node, visit);
471
+ };
472
+ visit(sourceFile);
473
+ return violations;
474
+ };
475
+ const TSX_EXTENSIONS = new Set([".tsx"]);
476
+ const isTsxFile = (path) => {
477
+ const dot = path.lastIndexOf(".");
478
+ return dot > path.lastIndexOf("/") && TSX_EXTENSIONS.has(path.slice(dot));
479
+ };
480
+ const checkClassNameFile = async (filePath) => {
481
+ if (!isTsxFile(filePath)) return [];
482
+ const f = file(filePath);
483
+ if (!await f.exists()) return [];
484
+ return findClassNameViolations({ sourceText: await f.text() }).map((v) => ({
485
+ file: filePath,
486
+ line: v.line,
487
+ linter: "cn",
488
+ rule: v.rule
489
+ }));
490
+ };
491
+ const checkClassName = async ({ root }) => {
492
+ const glob = new Glob("**/*.tsx");
493
+ const allDiagnostics = [];
494
+ for await (const path of glob.scan({
495
+ absolute: true,
496
+ cwd: root,
497
+ dot: false
498
+ })) {
499
+ if (path.includes("node_modules") || path.includes("readonly") || path.includes(".next") || path.includes("dist")) continue;
500
+ const diagnostics = await checkClassNameFile(path);
501
+ allDiagnostics.push(...diagnostics);
502
+ }
503
+ return allDiagnostics;
504
+ };
505
+ //#endregion
506
+ //#region src/rules.ts
507
+ const extractBiomeRules = async () => {
508
+ const pkgPath = fromFileUrl(import.meta.resolve("@biomejs/biome/configuration_schema.json"));
509
+ const defs = JSON.parse(await file(pkgPath).text()).$defs ?? {};
510
+ const categories = [
511
+ "a11y",
512
+ "complexity",
513
+ "correctness",
514
+ "nursery",
515
+ "performance",
516
+ "security",
517
+ "style",
518
+ "suspicious"
519
+ ];
520
+ const results = [];
521
+ for (const cat of categories) {
522
+ const key = cat.charAt(0).toUpperCase() + cat.slice(1);
523
+ const rules = Object.keys(defs[key]?.properties ?? {});
524
+ for (const rule of rules) results.push({
525
+ fixable: false,
526
+ linter: "biome",
527
+ rule: `lint/${cat}/${rule}`
528
+ });
529
+ }
530
+ return results;
531
+ };
532
+ const OXLINT_FIX_MARKERS = new Set([
533
+ "⚠️🛠️️",
534
+ "💡",
535
+ "🛠️",
536
+ "🛠️💡"
537
+ ]);
538
+ const extractOxlintRules = () => {
539
+ const output = decodeText(spawnSync({
540
+ cmd: [
541
+ "bun",
542
+ "node_modules/.bin/oxlint",
543
+ "-c",
544
+ joinPath(cwd, "node_modules/.cache/lintmax/.oxlintrc.json"),
545
+ "--rules"
546
+ ],
547
+ cwd,
548
+ stderr: "pipe",
549
+ stdout: "pipe"
550
+ }).stdout);
551
+ const results = [];
552
+ const lines = output.split("\n");
553
+ for (const line of lines) if (line.startsWith("|") && !line.includes("Rule name")) {
554
+ const cols = line.split("|").map((c) => c.trim());
555
+ const ruleName = cols[1];
556
+ const source = cols[2];
557
+ const fixCol = cols[5] ?? "";
558
+ const enabledCol = cols[4] ?? "";
559
+ if (ruleName && source && !ruleName.startsWith("---") && enabledCol.includes("✅")) results.push({
560
+ fixable: OXLINT_FIX_MARKERS.has(fixCol.trim()),
561
+ linter: "oxlint",
562
+ rule: source === "oxc" ? ruleName : `${source}(${ruleName})`
563
+ });
564
+ }
565
+ return results;
566
+ };
567
+ const extractEslintRules = async () => {
568
+ const eslintBin = await resolveBin({
569
+ bin: "eslint",
570
+ pkg: "eslint"
571
+ });
572
+ const configPath = joinPath(cwd, "node_modules/.cache/lintmax/eslint.generated.mjs");
573
+ const dummyFile = joinPath(cwd, "_lintmax_dummy.ts");
574
+ await write(dummyFile, "export {}\n");
575
+ const result = spawnSync({
576
+ cmd: [
577
+ "bun",
578
+ eslintBin,
579
+ "--config",
580
+ configPath,
581
+ "--print-config",
582
+ dummyFile
583
+ ],
584
+ cwd,
585
+ stderr: "pipe",
586
+ stdout: "pipe"
587
+ });
588
+ spawnSync({
589
+ cmd: [
590
+ "rm",
591
+ "-f",
592
+ dummyFile
593
+ ],
594
+ stderr: "pipe",
595
+ stdout: "pipe"
596
+ });
597
+ if (result.exitCode !== 0) return [];
598
+ let parsed;
599
+ try {
600
+ parsed = JSON.parse(decodeText(result.stdout));
601
+ } catch {
602
+ return [];
603
+ }
604
+ const allRules = parsed.rules ?? {};
605
+ const results = [];
606
+ for (const [rule, config] of Object.entries(allRules)) {
607
+ const level = Array.isArray(config) ? config[0] : config;
608
+ if (level !== 0 && level !== "off") results.push({
609
+ fixable: false,
610
+ linter: "eslint",
611
+ rule
612
+ });
613
+ }
614
+ return results;
615
+ };
616
+ const extractAllRules = async () => {
617
+ const oxlint = extractOxlintRules();
618
+ const [biome, eslint] = await Promise.all([extractBiomeRules(), extractEslintRules()]);
619
+ return [
620
+ ...biome,
621
+ ...oxlint,
622
+ ...eslint
623
+ ].toSorted((a, b) => {
624
+ const linterCmp = a.linter.localeCompare(b.linter);
625
+ if (linterCmp !== 0) return linterCmp;
626
+ return a.rule.localeCompare(b.rule);
627
+ });
628
+ };
629
+ const formatRulesCompact = (rules) => {
630
+ const byLinter = /* @__PURE__ */ new Map();
631
+ for (const r of rules) {
632
+ let list = byLinter.get(r.linter);
633
+ if (!list) {
634
+ list = [];
635
+ byLinter.set(r.linter, list);
636
+ }
637
+ list.push(r.rule);
638
+ }
639
+ const parts = [];
640
+ for (const [linter, ruleList] of byLinter) {
641
+ parts.push(`${linter} (${ruleList.length})`);
642
+ for (const rule of ruleList) parts.push(` ${rule}`);
643
+ }
644
+ return parts.join("\n");
645
+ };
646
+ const formatRulesHuman = (rules) => {
647
+ const header = "Linter Rule";
648
+ const separator = "─".repeat(60);
649
+ const lines = [header, separator];
650
+ for (const r of rules) lines.push(`${r.linter.padEnd(16)}${r.rule}`);
651
+ lines.push(separator);
652
+ lines.push(`Total: ${rules.length} rules`);
653
+ return lines.join("\n");
654
+ };
655
+ //#endregion
656
+ //#region src/clean-ignores.ts
657
+ /** biome-ignore-all lint/performance/noAwaitInLoops: sequential file writes */
658
+ /** biome-ignore-all lint/nursery/useNamedCaptureGroup: not needed */
659
+ const eslintLineRe = /^(\s*(?:\/\/|\/\*)\s*(?:eslint-disable(?:-next-line)?)\s+)(.+?)(\s*\*\/)?$/u;
660
+ const oxlintLineRe = /^(\s*(?:\/\/|\/\*)\s*(?:oxlint-disable(?:-next-line)?)\s+)(.+?)(\s*\*\/)?$/u;
661
+ const biomeLineRe = /^(\s*(?:\/\/|\/\*\*)\s*biome-ignore(?:-all)?\s+)([\w/]+)(.*?)$/u;
662
+ const trailingCommentRe = /\s*--.*$/u;
663
+ const trailingCloseRe = /\s*\*\/$/u;
664
+ const oxlintPrefixRe = /^(?:oxc|eslint|typescript-eslint|typescript_eslint|react|react-hooks|react_hooks|jsx-a11y|jsx_a11y|import|nextjs|next|jsdoc|promise|unicorn|vitest|jest|eslint-plugin-react-perf|eslint-plugin-jsx-a11y|eslint-plugin-react|eslint-plugin-promise|eslint-plugin-unicorn|react-perf|react_perf|@next\/next|@typescript-eslint|@eslint-react)[\\/(/]/u;
665
+ const trailingParenRe = /\)$/u;
666
+ const trailingSepRe = /[\\/(/]$/u;
667
+ const eslintPluginPrefixRe = /^eslint-plugin-/u;
668
+ const normalizeRule = (rule) => {
669
+ const variants = [rule];
670
+ const oxMatch = oxlintPrefixRe.exec(rule);
671
+ if (oxMatch) {
672
+ const bare = rule.slice(oxMatch[0].length).replace(trailingParenRe, "");
673
+ variants.push(bare);
674
+ const prefix = oxMatch[0].replace(trailingSepRe, "");
675
+ variants.push(`${prefix}/${bare}`);
676
+ variants.push(`${prefix}(${bare})`);
677
+ if (prefix === "eslint") variants.push(bare);
678
+ if (prefix === "typescript-eslint") variants.push(`@typescript-eslint/${bare}`);
679
+ if (prefix.startsWith("eslint-plugin-")) {
680
+ const short = prefix.replace(eslintPluginPrefixRe, "");
681
+ variants.push(`${short}/${bare}`);
682
+ variants.push(`${short}(${bare})`);
683
+ }
684
+ }
685
+ if (rule.startsWith("@typescript-eslint/")) variants.push(`typescript-eslint(${rule.slice(19)})`);
686
+ if (rule.startsWith("@next/next/")) variants.push(`nextjs(${rule.slice(11)})`);
687
+ if (rule.startsWith("@eslint-react/")) variants.push(`react(${rule.slice(14)})`);
688
+ const extra = [];
689
+ for (const v of variants) {
690
+ if (v.includes("_")) extra.push(v.replaceAll("_", "-"));
691
+ if (v.includes("-")) extra.push(v.replaceAll("-", "_"));
692
+ }
693
+ for (const e of extra) variants.push(e);
694
+ return variants;
695
+ };
696
+ const loadOxlintOffRules = async () => {
697
+ const config = await readRequiredJson({ path: joinPath(process.cwd(), cacheDir, ".oxlintrc.json") }).catch(() => null);
698
+ if (!config) return /* @__PURE__ */ new Set();
699
+ const off = /* @__PURE__ */ new Set();
700
+ for (const [rule, val] of Object.entries(config.rules ?? {})) if ((Array.isArray(val) ? String(val[0]) : val) === "off") {
701
+ off.add(rule);
702
+ for (const v of normalizeRule(rule)) off.add(v);
703
+ }
704
+ for (const rule of OXLINT_CLI_ALLOW) {
705
+ off.add(rule);
706
+ for (const v of normalizeRule(rule)) off.add(v);
707
+ }
708
+ return off;
709
+ };
710
+ const buildActiveRuleSet = async () => {
711
+ const rules = await extractAllRules();
712
+ const active = /* @__PURE__ */ new Set();
713
+ for (const r of rules) {
714
+ active.add(r.rule);
715
+ for (const v of normalizeRule(r.rule)) active.add(v);
716
+ }
717
+ return active;
718
+ };
719
+ const isRuleActive = (rule, active, oxlintOff) => {
720
+ if (oxlintOff?.has(rule)) return false;
721
+ if (oxlintOff) {
722
+ for (const v of normalizeRule(rule)) if (oxlintOff.has(v)) return false;
723
+ }
724
+ if (active.has(rule)) return true;
725
+ for (const v of normalizeRule(rule)) if (active.has(v)) return true;
726
+ if (!oxlintOff) return false;
727
+ return true;
728
+ };
729
+ const splitRules = (str) => str.split(",").map((r) => r.trim().replace(trailingCommentRe, "").replace(trailingCloseRe, "")).filter(Boolean);
730
+ const processMultiRuleLine = ({ active, isOxlint, line, match, oxlintOff, result }) => {
731
+ const prefix = match[1] ?? "";
732
+ const rulesStr = match[2] ?? "";
733
+ const suffix = match[3] ?? "";
734
+ const rules = splitRules(rulesStr);
735
+ const kept = rules.filter((r) => isRuleActive(r, active, isOxlint ? oxlintOff : void 0));
736
+ if (kept.length === 0) return rules.length;
737
+ if (kept.length < rules.length) {
738
+ result.push(`${prefix}${kept.join(", ")}${suffix}`);
739
+ return rules.length - kept.length;
740
+ }
741
+ result.push(line);
742
+ return 0;
743
+ };
744
+ const cleanFileIgnores = async (filePath, active, oxlintOff) => {
745
+ const f = file(filePath);
746
+ if (!await f.exists()) return 0;
747
+ const lines = (await f.text()).split("\n");
748
+ const result = [];
749
+ let removed = 0;
750
+ for (const line of lines) {
751
+ eslintLineRe.lastIndex = 0;
752
+ oxlintLineRe.lastIndex = 0;
753
+ biomeLineRe.lastIndex = 0;
754
+ const eslintMatch = eslintLineRe.exec(line);
755
+ if (eslintMatch) removed += processMultiRuleLine({
756
+ active,
757
+ line,
758
+ match: eslintMatch,
759
+ result
760
+ });
761
+ else {
762
+ const oxlintMatch = oxlintLineRe.exec(line);
763
+ if (oxlintMatch) removed += processMultiRuleLine({
764
+ active,
765
+ isOxlint: true,
766
+ line,
767
+ match: oxlintMatch,
768
+ oxlintOff,
769
+ result
770
+ });
771
+ else {
772
+ const biomeMatch = biomeLineRe.exec(line);
773
+ if (biomeMatch && !isRuleActive(biomeMatch[2] ?? "", active)) removed += 1;
774
+ else result.push(line);
775
+ }
776
+ }
777
+ }
778
+ if (removed > 0) await write(filePath, result.join("\n"));
779
+ return removed;
780
+ };
781
+ const cleanIgnores = async (filePaths) => {
782
+ const active = await buildActiveRuleSet();
783
+ const oxlintOff = await loadOxlintOffRules();
784
+ let cleaned = 0;
785
+ const files = [];
786
+ for (const fp of filePaths) {
787
+ const count = await cleanFileIgnores(fp, active, oxlintOff);
788
+ if (count > 0) {
789
+ cleaned += count;
790
+ files.push(fp);
791
+ }
792
+ }
793
+ return {
794
+ cleaned,
795
+ files
796
+ };
797
+ };
798
+ //#endregion
799
+ //#region src/comments.ts
800
+ const KEEP_PATTERN = /eslint-disable|biome-ignore|oxlint-disable|@ts-nocheck|@ts-expect-error|@ts-ignore|@refresh|@flow|istanbul ignore|c8 ignore|webpackChunkName|prettier-ignore|noinspection|nolint|@jsx|@jsxImportSource|@jsxFrag|@license|@preserve|type-coverage:ignore/u;
801
+ const WHITESPACE_ONLY = /^\s*$/u;
802
+ const COMMENT_EXTENSIONS = new Set([
803
+ ".cjs",
804
+ ".js",
805
+ ".jsx",
806
+ ".mjs",
807
+ ".mts",
808
+ ".ts",
809
+ ".tsx"
810
+ ]);
811
+ const extOf = (path) => {
812
+ const dot = path.lastIndexOf(".");
813
+ return dot > path.lastIndexOf("/") ? path.slice(dot) : "";
814
+ };
815
+ const isCommentCandidate = (path) => COMMENT_EXTENSIONS.has(extOf(path));
816
+ const findDeletableComments = ({ sourceText }) => {
817
+ const sourceFile = ts.createSourceFile("file.ts", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
818
+ const seen = /* @__PURE__ */ new Set();
819
+ const deletable = [];
820
+ const visit = (node) => {
821
+ const leading = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
822
+ const trailing = ts.getTrailingCommentRanges(sourceText, node.getEnd());
823
+ const ranges = [...leading ?? [], ...trailing ?? []];
824
+ for (const range of ranges) if (!seen.has(range.pos)) {
825
+ seen.add(range.pos);
826
+ const text = sourceText.slice(range.pos, range.end);
827
+ if (!(text.startsWith("#!") || text.startsWith("/**") || text.startsWith("/// <") || KEEP_PATTERN.test(text))) {
828
+ const line = sourceFile.getLineAndCharacterOfPosition(range.pos).line + 1;
829
+ deletable.push({
830
+ end: range.end,
831
+ line,
832
+ start: range.pos
833
+ });
834
+ }
835
+ }
836
+ ts.forEachChild(node, visit);
837
+ };
838
+ visit(sourceFile);
839
+ deletable.sort((a, b) => a.start - b.start);
840
+ return deletable;
841
+ };
842
+ const deleteComments = ({ sourceText }) => {
843
+ const comments = findDeletableComments({ sourceText });
844
+ if (comments.length === 0) return sourceText;
845
+ const parts = [];
846
+ let cursor = 0;
847
+ for (const c of comments) {
848
+ const beforeChunk = sourceText.slice(cursor, c.start);
849
+ const lineStart = beforeChunk.lastIndexOf("\n") + 1;
850
+ const indent = beforeChunk.slice(lineStart);
851
+ let afterEnd = c.end;
852
+ if (sourceText[afterEnd] === "\n") afterEnd += 1;
853
+ else if (sourceText[afterEnd] === "\r" && sourceText[afterEnd + 1] === "\n") afterEnd += 2;
854
+ if (WHITESPACE_ONLY.test(indent) && afterEnd <= sourceText.length) {
855
+ parts.push(beforeChunk.slice(0, lineStart));
856
+ cursor = afterEnd;
857
+ } else {
858
+ parts.push(beforeChunk);
859
+ cursor = c.end;
860
+ }
861
+ }
862
+ parts.push(sourceText.slice(cursor));
863
+ return parts.join("");
864
+ };
865
+ const processFile = async (filePath) => {
866
+ if (!isCommentCandidate(filePath)) return {
867
+ diagnostics: [],
868
+ modified: false
869
+ };
870
+ const f = file(filePath);
871
+ if (!await f.exists()) return {
872
+ diagnostics: [],
873
+ modified: false
874
+ };
875
+ const comments = findDeletableComments({ sourceText: await f.text() });
876
+ const diagnostics = [];
877
+ for (const c of comments) diagnostics.push({
878
+ file: filePath,
879
+ line: c.line,
880
+ linter: "comments",
881
+ rule: "deletable"
882
+ });
883
+ return {
884
+ diagnostics,
885
+ modified: false
886
+ };
887
+ };
888
+ const processFileForFix = async (filePath) => {
889
+ if (!isCommentCandidate(filePath)) return false;
890
+ const f = file(filePath);
891
+ if (!await f.exists()) return false;
892
+ const sourceText = await f.text();
893
+ const result = deleteComments({ sourceText });
894
+ if (result !== sourceText) {
895
+ await write(filePath, result);
896
+ return true;
897
+ }
898
+ return false;
899
+ };
900
+ const checkComments = async ({ files }) => {
901
+ return (await Promise.all(files.map(async (f) => processFile(f)))).flatMap((r) => r.diagnostics);
902
+ };
903
+ const fixComments = async ({ files }) => {
904
+ return (await Promise.all(files.map(async (f) => processFileForFix(f)))).filter(Boolean).length;
905
+ };
906
+ //#endregion
907
+ //#region src/compact.ts
908
+ const lstatSafe = (p) => {
909
+ try {
910
+ return lstatSync(p);
911
+ } catch {
912
+ return null;
913
+ }
914
+ };
915
+ const COMPACT_REGEX = /(?:\r?\n){2,}/gu;
916
+ const compactBasenames = new Set([
917
+ ".env.example",
918
+ ".gitignore",
919
+ ".npmrc",
920
+ ".prettierignore",
921
+ "Dockerfile",
922
+ "Makefile"
923
+ ]);
924
+ const compactExtensions = new Set([
925
+ ".cjs",
926
+ ".css",
927
+ ".gql",
928
+ ".graphql",
929
+ ".html",
930
+ ".js",
931
+ ".json",
932
+ ".jsonc",
933
+ ".jsx",
934
+ ".mjs",
935
+ ".mts",
936
+ ".scss",
937
+ ".sql",
938
+ ".ts",
939
+ ".tsx",
940
+ ".txt",
941
+ ".yaml",
942
+ ".yml"
943
+ ]);
944
+ const basename = ({ path }) => {
945
+ const index = path.lastIndexOf("/");
946
+ if (index === -1) return path;
947
+ return path.slice(index + 1);
948
+ };
949
+ const extension = ({ path }) => {
950
+ const slashIndex = path.lastIndexOf("/");
951
+ const dotIndex = path.lastIndexOf(".");
952
+ return dotIndex > slashIndex ? path.slice(dotIndex) : "";
953
+ };
954
+ const compactContent = ({ content }) => content.replace(COMPACT_REGEX, "\n");
955
+ const isCompactCandidate = ({ relativePath }) => {
956
+ const fileName = basename({ path: relativePath });
957
+ if (compactBasenames.has(fileName)) return true;
958
+ return compactExtensions.has(extension({ path: relativePath }));
959
+ };
960
+ const isBinary = ({ bytes }) => {
961
+ for (const byte of bytes) if (byte === 0) return true;
962
+ return false;
963
+ };
964
+ const listCompactFiles = ({ env, root }) => {
965
+ const result = spawnSync({
966
+ cmd: [
967
+ "git",
968
+ "-C",
969
+ root,
970
+ "ls-files",
971
+ "-z",
972
+ "--cached",
973
+ "--others",
974
+ "--exclude-standard"
975
+ ],
976
+ env,
977
+ stderr: "pipe",
978
+ stdout: "pipe"
979
+ });
980
+ if (result.exitCode !== 0) {
981
+ const stderr = decodeText(result.stderr).trim();
982
+ if (stderr.toLowerCase().includes("not a git repository")) return [];
983
+ throw new CliExitError({
984
+ code: result.exitCode,
985
+ message: stderr.length > 0 ? stderr : "Failed to list files for compact step"
986
+ });
987
+ }
988
+ return decodeText(result.stdout).split("\0").filter((e) => e.length > 0 && e !== "bun.lock").filter((e) => !lstatSafe(join(root, e))?.isSymbolicLink());
989
+ };
990
+ const runCompact = async ({ env, human = false, mode, root }) => {
991
+ const files = listCompactFiles({
992
+ env,
993
+ root
994
+ });
995
+ const results = await Promise.all(files.map(async (relativePath) => {
996
+ if (!isCompactCandidate({ relativePath })) return {
997
+ changed: false,
998
+ relativePath,
999
+ scanned: false
1000
+ };
1001
+ const absolutePath = joinPath(root, relativePath);
1002
+ const source = file(absolutePath);
1003
+ if (!await source.exists()) return {
1004
+ changed: false,
1005
+ relativePath,
1006
+ scanned: false
1007
+ };
1008
+ const bytes = new Uint8Array(await source.arrayBuffer());
1009
+ if (isBinary({ bytes })) return {
1010
+ changed: false,
1011
+ relativePath,
1012
+ scanned: true
1013
+ };
1014
+ const content = decodeText(bytes);
1015
+ const compacted = compactContent({ content });
1016
+ if (content === compacted) return {
1017
+ changed: false,
1018
+ relativePath,
1019
+ scanned: true
1020
+ };
1021
+ if (mode === "fix") await write(absolutePath, compacted);
1022
+ return {
1023
+ changed: true,
1024
+ relativePath,
1025
+ scanned: true
1026
+ };
1027
+ }));
1028
+ const changed = [];
1029
+ let scanned = 0;
1030
+ for (const result of results) {
1031
+ if (result.scanned) scanned += 1;
1032
+ if (result.changed) changed.push(result.relativePath);
1033
+ }
1034
+ if (mode === "fix") {
1035
+ if (human) {
1036
+ process.stdout.write(`[compact] Scanned ${scanned} files\n`);
1037
+ process.stdout.write(`[compact] Updated ${changed.length} files\n`);
1038
+ }
1039
+ return;
1040
+ }
1041
+ if (changed.length === 0) return;
1042
+ const shown = changed.slice(0, 10);
1043
+ const suffix = changed.length > shown.length ? `\n...and ${changed.length - shown.length} more` : "";
1044
+ throw new CliExitError({
1045
+ code: 1,
1046
+ message: `[compact]\nFiles requiring compaction:\n${shown.join("\n")}${suffix}\nRun: lintmax fix`
1047
+ });
1048
+ };
1049
+ //#endregion
1050
+ //#region src/format.ts
1051
+ const formatGrouped = ({ files }) => {
1052
+ if (files.length === 0) return "";
1053
+ const parts = [];
1054
+ for (const f of files) {
1055
+ parts.push(f.file);
1056
+ for (const l of f.linters) {
1057
+ parts.push(` ${l.linter}`);
1058
+ for (const r of l.rules) {
1059
+ const lineStr = r.lines.length > 0 ? r.lines.join(",") : "";
1060
+ parts.push(` ${lineStr}${lineStr.length > 0 ? " " : ""}${r.rule}`);
1061
+ }
1062
+ }
1063
+ }
1064
+ return parts.join("\n");
1065
+ };
1066
+ //#endregion
1067
+ //#region src/jsx-extension.ts
1068
+ /** biome-ignore-all lint/nursery/noContinue: scan loop */
1069
+ const containsJsxNode = (sourceFile) => {
1070
+ let found = false;
1071
+ const visit = (node) => {
1072
+ if (found) return;
1073
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
1074
+ found = true;
1075
+ return;
1076
+ }
1077
+ ts.forEachChild(node, visit);
1078
+ };
1079
+ ts.forEachChild(sourceFile, visit);
1080
+ return found;
1081
+ };
1082
+ const hasJsx = (sourceText) => {
1083
+ const diags = ts.createSourceFile("file.ts", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).parseDiagnostics;
1084
+ if (!(Array.isArray(diags) && diags.length > 0)) return false;
1085
+ return containsJsxNode(ts.createSourceFile("file.tsx", sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX));
1086
+ };
1087
+ const checkJsxExtension = async ({ root }) => {
1088
+ const glob = new Glob("**/*.ts");
1089
+ const ignoreGlobs = DEFAULT_SHARED_IGNORE_PATTERNS.map((p) => new Glob(p));
1090
+ const isIgnored = (p) => p.includes("node_modules") || p.endsWith(".d.ts") || ignoreGlobs.some((g) => g.match(p));
1091
+ const diagnostics = [];
1092
+ for await (const path of glob.scan({
1093
+ absolute: false,
1094
+ cwd: root,
1095
+ dot: false
1096
+ })) {
1097
+ if (isIgnored(path)) continue;
1098
+ if (hasJsx(await file(`${root}/${path}`).text())) diagnostics.push({
1099
+ file: `${root}/${path}`,
1100
+ line: 1,
1101
+ linter: "lintmax",
1102
+ rule: "jsx-requires-tsx-extension"
1103
+ });
1104
+ }
1105
+ return diagnostics;
1106
+ };
1107
+ //#endregion
1108
+ //#region src/pipeline.ts
1109
+ const createStepExecutor = ({ env, failures, root }) => {
1110
+ const runContinue = (opts) => {
1111
+ try {
1112
+ run(opts);
1113
+ } catch (error) {
1114
+ if (error instanceof CliExitError) {
1115
+ failures.push({
1116
+ code: error.code,
1117
+ label: opts.label,
1118
+ message: error.message.length > 0 ? error.message : void 0
1119
+ });
1120
+ return;
1121
+ }
1122
+ throw error;
1123
+ }
1124
+ };
1125
+ const runCompactContinue = async ({ human = false, mode }) => {
1126
+ try {
1127
+ await runCompact({
1128
+ env,
1129
+ human,
1130
+ mode,
1131
+ root
1132
+ });
1133
+ } catch (error) {
1134
+ if (error instanceof CliExitError) {
1135
+ failures.push({
1136
+ code: error.code,
1137
+ label: "compact",
1138
+ message: error.message.length > 0 ? error.message : void 0
1139
+ });
1140
+ return;
1141
+ }
1142
+ throw error;
1143
+ }
1144
+ };
1145
+ const runSteps = ({ steps }) => {
1146
+ for (const step of steps) runContinue({
1147
+ args: step.args,
1148
+ command: step.command ?? "bun",
1149
+ env,
1150
+ label: step.label,
1151
+ silent: step.silent
1152
+ });
1153
+ };
1154
+ const runStepsSilent = ({ steps }) => {
1155
+ for (const step of steps) {
1156
+ const result = runCapture({
1157
+ args: step.args,
1158
+ command: step.command ?? "bun",
1159
+ env,
1160
+ label: step.label
1161
+ });
1162
+ if (result.exitCode !== 0) failures.push({
1163
+ code: result.exitCode,
1164
+ label: step.label,
1165
+ message: result.stderr.length > 0 ? result.stderr.trim() : void 0
1166
+ });
1167
+ }
1168
+ };
1169
+ const clearFailures = () => {
1170
+ failures.length = 0;
1171
+ };
1172
+ const throwIfFailures = () => {
1173
+ if (failures.length === 0) return;
1174
+ const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
1175
+ throw new CliExitError({
1176
+ code: failures[0]?.code ?? 1,
1177
+ message: `One or more steps failed:\n${details}`
1178
+ });
1179
+ };
1180
+ return {
1181
+ clearFailures,
1182
+ runCompactContinue,
1183
+ runSteps,
1184
+ runStepsSilent,
1185
+ throwIfFailures
1186
+ };
1187
+ };
1188
+ const createCheckSteps = ({ biomeBin, dir, eslintArgs, eslintBin, oxlintBin, prettierBin, prettierMarkdownTargets, sortPkgJson }) => {
1189
+ const oxlintCliAllow = OXLINT_CLI_ALLOW.flatMap((r) => ["--allow", r]);
1190
+ const steps = [
1191
+ {
1192
+ args: [
1193
+ sortPkgJson,
1194
+ "--check",
1195
+ "**/package.json",
1196
+ "--ignore",
1197
+ "**/node_modules/**"
1198
+ ],
1199
+ label: "sort-package-json"
1200
+ },
1201
+ {
1202
+ args: [
1203
+ biomeBin,
1204
+ "ci",
1205
+ "--config-path",
1206
+ dir,
1207
+ "--diagnostic-level=error"
1208
+ ],
1209
+ label: "biome"
1210
+ },
1211
+ {
1212
+ args: [
1213
+ oxlintBin,
1214
+ "-c",
1215
+ joinPath(dir, ".oxlintrc.json"),
1216
+ "--quiet",
1217
+ ...oxlintCliAllow
1218
+ ],
1219
+ label: "oxlint"
1220
+ },
1221
+ {
1222
+ args: [
1223
+ eslintBin,
1224
+ "--no-error-on-unmatched-pattern",
1225
+ ...eslintArgs
1226
+ ],
1227
+ label: "eslint"
1228
+ }
1229
+ ];
1230
+ if (prettierMarkdownTargets.length > 0) steps.push({
1231
+ args: [
1232
+ prettierBin,
1233
+ ...PRETTIER_MD_ARGS,
1234
+ "--check",
1235
+ "--no-error-on-unmatched-pattern",
1236
+ ...prettierMarkdownTargets
1237
+ ],
1238
+ label: "prettier"
1239
+ });
1240
+ return steps;
1241
+ };
1242
+ const createFixSteps = ({ biomeBin, dir, eslintArgs, eslintBin, hasFlowmark, oxlintBin, prettierBin, prettierMarkdownTargets, sortPkgJson }) => {
1243
+ const oxlintCliAllow = OXLINT_CLI_ALLOW.flatMap((r) => ["--allow", r]);
1244
+ const steps = [
1245
+ {
1246
+ args: [
1247
+ sortPkgJson,
1248
+ "**/package.json",
1249
+ "--ignore",
1250
+ "**/node_modules/**"
1251
+ ],
1252
+ label: "sort-package-json",
1253
+ silent: true
1254
+ },
1255
+ {
1256
+ args: [
1257
+ biomeBin,
1258
+ "check",
1259
+ "--config-path",
1260
+ dir,
1261
+ "--fix",
1262
+ "--diagnostic-level=error"
1263
+ ],
1264
+ label: "biome",
1265
+ silent: true
1266
+ },
1267
+ {
1268
+ args: [
1269
+ oxlintBin,
1270
+ "-c",
1271
+ joinPath(dir, ".oxlintrc.json"),
1272
+ "--fix",
1273
+ "--fix-suggestions",
1274
+ "--quiet",
1275
+ ...oxlintCliAllow
1276
+ ],
1277
+ label: "oxlint",
1278
+ silent: true
1279
+ },
1280
+ {
1281
+ args: [
1282
+ eslintBin,
1283
+ ...eslintArgs,
1284
+ "--fix"
1285
+ ],
1286
+ label: "eslint",
1287
+ silent: true
1288
+ },
1289
+ {
1290
+ args: [
1291
+ biomeBin,
1292
+ "check",
1293
+ "--config-path",
1294
+ dir,
1295
+ "--fix",
1296
+ "--diagnostic-level=error"
1297
+ ],
1298
+ label: "biome",
1299
+ silent: true
1300
+ }
1301
+ ];
1302
+ if (hasFlowmark) steps.push({
1303
+ args: [
1304
+ "-w",
1305
+ "0",
1306
+ "--auto",
1307
+ "."
1308
+ ],
1309
+ command: "flowmark",
1310
+ label: "flowmark",
1311
+ silent: true
1312
+ });
1313
+ if (prettierMarkdownTargets.length > 0) steps.push({
1314
+ args: [
1315
+ prettierBin,
1316
+ ...PRETTIER_MD_ARGS,
1317
+ "--write",
1318
+ "--no-error-on-unmatched-pattern",
1319
+ ...prettierMarkdownTargets
1320
+ ],
1321
+ label: "prettier",
1322
+ silent: true
1323
+ });
1324
+ return steps;
1325
+ };
1326
+ const captureAndParse = ({ env, failures, label, opts, parser }) => {
1327
+ const result = runCapture({
1328
+ args: opts.args,
1329
+ command: opts.command,
1330
+ env,
1331
+ label
1332
+ });
1333
+ if (result.exitCode === 0) return [];
1334
+ const diagnostics = parser(result);
1335
+ if (diagnostics.length > 0) return diagnostics;
1336
+ const stderr = result.stderr.trim();
1337
+ const stdout = result.stdout.trim();
1338
+ const stdoutFirst = stdout.split("\n", 1)[0]?.trimStart() ?? "";
1339
+ const stdoutIsJson = stdoutFirst.startsWith("[") || stdoutFirst.startsWith("{");
1340
+ const message = stderr.length > 0 ? stderr : stdoutIsJson ? void 0 : stdout || void 0;
1341
+ failures.push({
1342
+ code: result.exitCode,
1343
+ label,
1344
+ message
1345
+ });
1346
+ return [];
1347
+ };
1348
+ const runAgentCheck = ({ biomeBin, dir, env, eslintArgs, eslintBin, failures, oxlintBin, prettierBin, prettierMarkdownTargets, sortPkgJson }) => {
1349
+ const oxlintCliAllow = OXLINT_CLI_ALLOW.flatMap((r) => ["--allow", r]);
1350
+ const allDiagnostics = [];
1351
+ const push = (d) => {
1352
+ if (d.length > 0) allDiagnostics.push(...d);
1353
+ };
1354
+ push(captureAndParse({
1355
+ env,
1356
+ failures,
1357
+ label: "sort-package-json",
1358
+ opts: {
1359
+ args: [
1360
+ sortPkgJson,
1361
+ "--check",
1362
+ "**/package.json",
1363
+ "--ignore",
1364
+ "**/node_modules/**"
1365
+ ],
1366
+ command: "bun"
1367
+ },
1368
+ parser: parseSortPackageJsonOutput
1369
+ }));
1370
+ push(captureAndParse({
1371
+ env,
1372
+ failures,
1373
+ label: "biome",
1374
+ opts: {
1375
+ args: [
1376
+ biomeBin,
1377
+ "check",
1378
+ "--config-path",
1379
+ dir,
1380
+ "--reporter=json"
1381
+ ],
1382
+ command: "bun"
1383
+ },
1384
+ parser: ({ stdout }) => parseBiomeDiagnostics({ stdout })
1385
+ }));
1386
+ push(captureAndParse({
1387
+ env,
1388
+ failures,
1389
+ label: "oxlint",
1390
+ opts: {
1391
+ args: [
1392
+ oxlintBin,
1393
+ "-c",
1394
+ joinPath(dir, ".oxlintrc.json"),
1395
+ "--quiet",
1396
+ "-f",
1397
+ "json",
1398
+ ...oxlintCliAllow
1399
+ ],
1400
+ command: "bun"
1401
+ },
1402
+ parser: ({ stdout }) => parseOxlintDiagnostics({ stdout })
1403
+ }));
1404
+ push(captureAndParse({
1405
+ env,
1406
+ failures,
1407
+ label: "eslint",
1408
+ opts: {
1409
+ args: [
1410
+ eslintBin,
1411
+ "--no-error-on-unmatched-pattern",
1412
+ ...eslintArgs,
1413
+ "-f",
1414
+ "json"
1415
+ ],
1416
+ command: "bun"
1417
+ },
1418
+ parser: ({ stdout }) => parseEslintDiagnostics({ stdout })
1419
+ }));
1420
+ if (prettierMarkdownTargets.length > 0) push(captureAndParse({
1421
+ env,
1422
+ failures,
1423
+ label: "prettier",
1424
+ opts: {
1425
+ args: [
1426
+ prettierBin,
1427
+ ...PRETTIER_MD_ARGS,
1428
+ "--list-different",
1429
+ "--no-error-on-unmatched-pattern",
1430
+ ...prettierMarkdownTargets
1431
+ ],
1432
+ command: "bun"
1433
+ },
1434
+ parser: ({ stdout }) => parsePrettierOutput({ stdout })
1435
+ }));
1436
+ return allDiagnostics;
1437
+ };
1438
+ const isGitWorkTree = ({ env, root }) => spawnSync({
1439
+ cmd: [
1440
+ "git",
1441
+ "-C",
1442
+ root,
1443
+ "rev-parse",
1444
+ "--is-inside-work-tree"
1445
+ ],
1446
+ env,
1447
+ stderr: "pipe",
1448
+ stdout: "pipe"
1449
+ }).exitCode === 0;
1450
+ const createPrettierMarkdownTargets = ({ gitFiles, gitWorkTree }) => {
1451
+ if (!gitWorkTree) return ["**/*.md"];
1452
+ return gitFiles.filter((filePath) => filePath.endsWith(".md"));
1453
+ };
1454
+ const throwAgentResults = ({ diagnostics, failures }) => {
1455
+ if (diagnostics.length === 0 && failures.length === 0) return;
1456
+ const output = formatGrouped({ files: aggregate({ diagnostics }) });
1457
+ if (output.length > 0) process.stdout.write(`${output}\n`);
1458
+ if (failures.length > 0) {
1459
+ const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
1460
+ process.stderr.write(`${details}\n`);
1461
+ }
1462
+ throw new CliExitError({ code: 1 });
1463
+ };
1464
+ const runLint = async ({ command, human = false }) => {
1465
+ const dir = joinPath(cwd, cacheDir);
1466
+ ensureDirectory({ directory: dir });
1467
+ const configPath = joinPath(cwd, "lintmax.config.ts");
1468
+ const hasConfig = await pathExists({ path: configPath });
1469
+ const bundledBinA = joinPath(lintmaxRoot, "node_modules", ".bin");
1470
+ const bundledBinB = joinPath(dirnamePath(lintmaxRoot), ".bin");
1471
+ const cwdBinDir = joinPath(cwd, "node_modules", ".bin");
1472
+ const runtimePath = joinPath(dir, "lintmax.json");
1473
+ const env$1 = {
1474
+ ...env,
1475
+ PATH: `${bundledBinA}:${bundledBinB}:${cwdBinDir}:${env.PATH ?? ""}`
1476
+ };
1477
+ if (hasConfig) run({
1478
+ args: ["-e", `const m = await import('${configPath}'); const { sync: s } = await import('lintmax'); await s(m.default);`],
1479
+ command: "bun",
1480
+ env: env$1,
1481
+ label: "config",
1482
+ silent: true
1483
+ });
1484
+ else await sync();
1485
+ const runtime = await readJson({ path: runtimePath });
1486
+ const failures = [];
1487
+ const { clearFailures, runCompactContinue, runSteps, runStepsSilent, throwIfFailures } = createStepExecutor({
1488
+ env: env$1,
1489
+ failures,
1490
+ root: cwd
1491
+ });
1492
+ if (command === "fix" && runtime.compact === true) await runCompactContinue({
1493
+ human,
1494
+ mode: "fix"
1495
+ });
1496
+ const eslintArgs = ["--config", joinPath(dir, "eslint.generated.mjs")];
1497
+ const [sortPkgJson, biomeBin, oxlintBin, eslintBin, prettierBin] = await Promise.all([
1498
+ resolveBin({
1499
+ bin: "sort-package-json",
1500
+ pkg: "sort-package-json"
1501
+ }),
1502
+ resolveBin({
1503
+ bin: "biome",
1504
+ pkg: "@biomejs/biome"
1505
+ }),
1506
+ resolveBin({
1507
+ bin: "oxlint",
1508
+ pkg: "oxlint"
1509
+ }),
1510
+ resolveBin({
1511
+ bin: "eslint",
1512
+ pkg: "eslint"
1513
+ }),
1514
+ resolveBin({
1515
+ bin: "prettier",
1516
+ pkg: "prettier"
1517
+ })
1518
+ ]);
1519
+ const hasFlowmark = spawnSync({
1520
+ cmd: ["which", "flowmark"],
1521
+ env: env$1,
1522
+ stderr: "pipe",
1523
+ stdout: "pipe"
1524
+ }).exitCode === 0;
1525
+ const gitWorkTree = isGitWorkTree({
1526
+ env: env$1,
1527
+ root: cwd
1528
+ });
1529
+ const allGitFiles = gitWorkTree ? listCompactFiles({
1530
+ env: env$1,
1531
+ root: cwd
1532
+ }) : [];
1533
+ const prettierMarkdownTargets = createPrettierMarkdownTargets({
1534
+ gitFiles: allGitFiles,
1535
+ gitWorkTree
1536
+ });
1537
+ const checkSteps = createCheckSteps({
1538
+ biomeBin,
1539
+ dir,
1540
+ eslintArgs,
1541
+ eslintBin,
1542
+ oxlintBin,
1543
+ prettierBin,
1544
+ prettierMarkdownTargets,
1545
+ sortPkgJson
1546
+ });
1547
+ const shouldComments = runtime.comments !== false;
1548
+ const ignoreGlobs = DEFAULT_SHARED_IGNORE_PATTERNS.map((p) => new Glob(p));
1549
+ const isIgnored = (filePath) => ignoreGlobs.some((g) => g.match(filePath));
1550
+ const sourceFiles = allGitFiles.filter((f) => !isIgnored(f));
1551
+ if (command === "fix") {
1552
+ if (shouldComments) await fixComments({ files: allGitFiles });
1553
+ if (sourceFiles.length > 0) await cleanIgnores(sourceFiles.map((f) => joinPath(cwd, f)));
1554
+ const fixSteps = createFixSteps({
1555
+ biomeBin,
1556
+ dir,
1557
+ eslintArgs,
1558
+ eslintBin,
1559
+ hasFlowmark,
1560
+ oxlintBin,
1561
+ prettierBin,
1562
+ prettierMarkdownTargets,
1563
+ sortPkgJson
1564
+ });
1565
+ if (human) runSteps({ steps: fixSteps });
1566
+ else runStepsSilent({ steps: fixSteps });
1567
+ clearFailures();
1568
+ if (human) {
1569
+ runSteps({ steps: checkSteps });
1570
+ throwIfFailures();
1571
+ return;
1572
+ }
1573
+ const allDiagnostics = runAgentCheck({
1574
+ biomeBin,
1575
+ dir,
1576
+ env: env$1,
1577
+ eslintArgs,
1578
+ eslintBin,
1579
+ failures,
1580
+ oxlintBin,
1581
+ prettierBin,
1582
+ prettierMarkdownTargets,
1583
+ sortPkgJson
1584
+ });
1585
+ if (shouldComments) {
1586
+ const commentDiags = await checkComments({ files: allGitFiles });
1587
+ allDiagnostics.push(...commentDiags);
1588
+ }
1589
+ const cnDiags = await checkClassName({ root: cwd });
1590
+ const jsxDiags = await checkJsxExtension({ root: cwd });
1591
+ allDiagnostics.push(...cnDiags, ...jsxDiags);
1592
+ throwAgentResults({
1593
+ diagnostics: allDiagnostics,
1594
+ failures
1595
+ });
1596
+ return;
1597
+ }
1598
+ if (human) {
1599
+ runSteps({ steps: checkSteps });
1600
+ const cnDiagsHuman = await checkClassName({ root: cwd });
1601
+ const jsxDiagsHuman = await checkJsxExtension({ root: cwd });
1602
+ const humanCustomDiags = [...cnDiagsHuman, ...jsxDiagsHuman];
1603
+ if (humanCustomDiags.length > 0) {
1604
+ const output = formatGrouped({ files: aggregate({ diagnostics: humanCustomDiags }) });
1605
+ if (output.length > 0) process.stdout.write(`${output}\n`);
1606
+ failures.push({
1607
+ code: 1,
1608
+ label: "cn"
1609
+ });
1610
+ }
1611
+ throwIfFailures();
1612
+ return;
1613
+ }
1614
+ const allDiagnostics = runAgentCheck({
1615
+ biomeBin,
1616
+ dir,
1617
+ env: env$1,
1618
+ eslintArgs,
1619
+ eslintBin,
1620
+ failures,
1621
+ oxlintBin,
1622
+ prettierBin,
1623
+ prettierMarkdownTargets,
1624
+ sortPkgJson
1625
+ });
1626
+ if (shouldComments) {
1627
+ const commentDiags = await checkComments({ files: allGitFiles });
1628
+ allDiagnostics.push(...commentDiags);
1629
+ }
1630
+ const cnDiags = await checkClassName({ root: cwd });
1631
+ const jsxDiags = await checkJsxExtension({ root: cwd });
1632
+ allDiagnostics.push(...cnDiags, ...jsxDiags);
1633
+ if (allDiagnostics.length > 0 || failures.length > 0) {
1634
+ const output = formatGrouped({ files: aggregate({ diagnostics: allDiagnostics }) });
1635
+ if (output.length > 0) process.stdout.write(`${output}\n`);
1636
+ if (failures.length > 0) {
1637
+ const details = failures.map((item) => `- ${item.label} (exit ${item.code})${item.message ? `\n${item.message}` : ""}`).join("\n");
1638
+ process.stderr.write(`${details}\n`);
1639
+ }
1640
+ throw new CliExitError({ code: 1 });
1641
+ }
1642
+ };
1643
+ //#endregion
1644
+ //#region src/cli.ts
1645
+ const command = process.argv[2];
1646
+ const main = async () => {
1647
+ const version = await readVersion();
1648
+ if (command === "init") {
1649
+ await runInit();
1650
+ return;
1651
+ }
1652
+ if (command === "--version" || command === "-v") {
1653
+ process.stdout.write(`${version}\n`);
1654
+ return;
1655
+ }
1656
+ if (command === "rules") {
1657
+ const human = process.argv.includes("--human");
1658
+ const rules = await extractAllRules();
1659
+ const output = human ? formatRulesHuman(rules) : formatRulesCompact(rules);
1660
+ process.stdout.write(`${output}\n`);
1661
+ return;
1662
+ }
1663
+ if (command === "ignores") {
1664
+ const { runIgnores } = await import("./ignores-B1QoL99d.mjs");
1665
+ await runIgnores(process.argv.includes("--verbose"));
1666
+ return;
1667
+ }
1668
+ if (command !== "fix" && command !== "check") {
1669
+ usage({ version });
1670
+ if (command === "--help" || command === "-h") return;
1671
+ throw new CliExitError({ code: 1 });
1672
+ }
1673
+ await runLint({
1674
+ command,
1675
+ human: process.argv.includes("--human")
1676
+ });
1677
+ };
1678
+ try {
1679
+ await main();
1680
+ } catch (error) {
1681
+ if (error instanceof CliExitError) {
1682
+ if (error.message.length > 0) process.stderr.write(`${error.message}\n`);
1683
+ process.exitCode = error.code;
1684
+ } else throw error;
1685
+ }
1686
+ //#endregion
1687
+ export {};