lintmax 0.1.15

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.

Potentially problematic release.


This version of lintmax might be problematic. Click here for more details.

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