aislop 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DpU0WTTj.js";
1
+ import { n as getEngineLabel, r as APP_VERSION, t as ENGINE_INFO } from "./engine-info-DFze-2GQ.js";
2
2
  import { n as runSubprocess, t as isToolInstalled } from "./subprocess-99puEEGl.js";
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import pc from "picocolors";
7
- import { spawn, spawnSync } from "node:child_process";
7
+ import { spawnSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { performance } from "node:perf_hooks";
10
10
  import os from "node:os";
@@ -52,8 +52,7 @@ const logger = {
52
52
  //#region src/output/layout.ts
53
53
  const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
54
54
  const printCommandHeader = (commandName) => {
55
- logger.log(highlighter.bold(`aislop ${commandName}`));
56
- logger.log(highlighter.dim(`v${APP_VERSION}`));
55
+ logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
57
56
  logger.break();
58
57
  };
59
58
  const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
@@ -530,193 +529,272 @@ const doctorCommand = async (directory) => {
530
529
  };
531
530
 
532
531
  //#endregion
533
- //#region src/engines/format/biome.ts
534
- const esmRequire$1 = createRequire(import.meta.url);
535
- const resolveLocalBiomeScript = () => {
536
- try {
537
- const packageJsonPath = esmRequire$1.resolve("@biomejs/biome/package.json");
538
- return path.join(path.dirname(packageJsonPath), "bin", "biome");
539
- } catch {
540
- return null;
541
- }
542
- };
543
- const runBiome = async (args, rootDirectory, timeout) => {
544
- const localScript = resolveLocalBiomeScript();
545
- if (localScript) return runSubprocess(process.execPath, [localScript, ...args], {
546
- cwd: rootDirectory,
547
- timeout
548
- });
549
- return runSubprocess("biome", args, {
550
- cwd: rootDirectory,
551
- timeout
552
- });
553
- };
554
- const BIOME_EXTENSIONS = new Set([
555
- ".js",
556
- ".jsx",
532
+ //#region src/engines/ai-slop/unused-imports.ts
533
+ const JS_EXTENSIONS$1 = new Set([
557
534
  ".ts",
558
535
  ".tsx",
536
+ ".js",
537
+ ".jsx",
559
538
  ".mjs",
560
539
  ".cjs"
561
540
  ]);
562
- const getBiomeTargets = (context) => getSourceFiles(context).filter((filePath) => BIOME_EXTENSIONS.has(path.extname(filePath))).map((filePath) => path.relative(context.rootDirectory, filePath));
563
- const projectUsesDecorators = (rootDir) => {
564
- try {
565
- const tsconfigPath = path.join(rootDir, "tsconfig.json");
566
- if (!fs.existsSync(tsconfigPath)) return false;
567
- const content = fs.readFileSync(tsconfigPath, "utf-8");
568
- return /experimentalDecorators.*true/i.test(content);
569
- } catch {
570
- return false;
571
- }
572
- };
573
- const runBiomeFormat = async (context) => {
574
- const targets = getBiomeTargets(context);
575
- if (targets.length === 0) return [];
576
- const args = [
577
- "format",
578
- "--reporter=json",
579
- ...targets
580
- ];
581
- try {
582
- const result = await runBiome(args, context.rootDirectory, 6e4);
583
- const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
584
- if (!output) return [];
585
- let diagnostics = parseBiomeJsonOutput(output, context.rootDirectory);
586
- if (projectUsesDecorators(context.rootDirectory)) diagnostics = diagnostics.filter((d) => {
587
- const msg = d.message.toLowerCase();
588
- return !msg.includes("decorator") && !msg.includes("parsing error");
541
+ const PY_EXTENSIONS = new Set([".py"]);
542
+ const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
543
+ const extractJsImportedSymbols = (lines) => {
544
+ const symbols = [];
545
+ const importLines = /* @__PURE__ */ new Set();
546
+ for (let i = 0; i < lines.length; i++) {
547
+ const trimmed = lines[i].trim();
548
+ if (!trimmed.startsWith("import ")) continue;
549
+ importLines.add(i);
550
+ if (/^import\s+["']/.test(trimmed)) continue;
551
+ if (/^import\s+type\s/.test(trimmed)) continue;
552
+ let fullImport = trimmed;
553
+ let endLine = i;
554
+ while (!fullImport.includes("from") && endLine < lines.length - 1) {
555
+ endLine++;
556
+ fullImport += ` ${lines[endLine].trim()}`;
557
+ importLines.add(endLine);
558
+ }
559
+ const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
560
+ if (namespaceMatch) {
561
+ symbols.push({
562
+ name: namespaceMatch[1],
563
+ line: i + 1,
564
+ isDefault: false,
565
+ isNamespace: true
566
+ });
567
+ continue;
568
+ }
569
+ const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
570
+ if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
571
+ name: defaultMatch[1],
572
+ line: i + 1,
573
+ isDefault: true,
574
+ isNamespace: false
589
575
  });
590
- return diagnostics;
591
- } catch {
592
- return [];
576
+ const namedMatch = fullImport.match(/\{([^}]+)\}/);
577
+ if (namedMatch) {
578
+ const namedImports = namedMatch[1].split(",");
579
+ for (const ni of namedImports) {
580
+ const parts = ni.trim().split(/\s+as\s+/);
581
+ if (parts.length === 0 || !parts[0]) continue;
582
+ const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
583
+ const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
584
+ if (localName && /^\w+$/.test(localName)) symbols.push({
585
+ name: localName,
586
+ line: i + 1,
587
+ isDefault: false,
588
+ isNamespace: false
589
+ });
590
+ }
591
+ }
593
592
  }
593
+ return {
594
+ symbols,
595
+ importLines
596
+ };
594
597
  };
595
- const parseBiomeJsonOutput = (output, rootDir) => {
596
- const diagnostics = [];
597
- for (const line of output.split("\n")) {
598
- const trimmed = line.trim();
599
- if (!trimmed.startsWith("{")) continue;
600
- let parsed = null;
601
- try {
602
- parsed = JSON.parse(trimmed);
603
- } catch {
604
- parsed = null;
598
+ const extractPyImportedSymbols = (lines) => {
599
+ const symbols = [];
600
+ const importLines = /* @__PURE__ */ new Set();
601
+ for (let i = 0; i < lines.length; i++) {
602
+ const trimmed = lines[i].trim();
603
+ const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
604
+ if (fromMatch) {
605
+ importLines.add(i);
606
+ const importPart = fromMatch[1].replace(/#.*$/, "").trim();
607
+ if (importPart === "*") continue;
608
+ const cleaned = importPart.replace(/[()]/g, "");
609
+ for (const item of cleaned.split(",")) {
610
+ const parts = item.trim().split(/\s+as\s+/);
611
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
612
+ if (localName && /^\w+$/.test(localName)) symbols.push({
613
+ name: localName,
614
+ line: i + 1,
615
+ isDefault: false,
616
+ isNamespace: false
617
+ });
618
+ }
619
+ continue;
605
620
  }
606
- if (!parsed || !Array.isArray(parsed.diagnostics)) continue;
607
- for (const entry of parsed.diagnostics) {
608
- const rawPath = entry.location?.path;
609
- if (!rawPath) continue;
610
- const severity = entry.severity === "error" ? "error" : "warning";
611
- diagnostics.push({
612
- filePath: path.isAbsolute(rawPath) ? path.relative(rootDir, rawPath) : rawPath,
613
- engine: "format",
614
- rule: "formatting",
615
- severity,
616
- message: entry.message ?? "File is not formatted correctly",
617
- help: "Run `aislop fix` to auto-format",
618
- line: entry.location?.start?.line ?? 0,
619
- column: entry.location?.start?.column ?? 0,
620
- category: "Format",
621
- fixable: true
621
+ const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
622
+ if (importMatch) {
623
+ importLines.add(i);
624
+ const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
625
+ if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
626
+ name: simpleName,
627
+ line: i + 1,
628
+ isDefault: false,
629
+ isNamespace: true
622
630
  });
623
631
  }
624
632
  }
625
- return diagnostics;
626
- };
627
- const fixBiomeFormat = async (context) => {
628
- const targets = getBiomeTargets(context);
629
- if (targets.length === 0) return;
630
- const result = await runBiome([
631
- "check",
632
- "--write",
633
- "--formatter-enabled=true",
634
- "--linter-enabled=false",
635
- ...targets
636
- ], context.rootDirectory, 6e4);
637
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
633
+ return {
634
+ symbols,
635
+ importLines
636
+ };
638
637
  };
639
-
640
- //#endregion
641
- //#region src/engines/format/gofmt.ts
642
- const runGofmt = async (context) => {
643
- try {
644
- const result = await runSubprocess("gofmt", ["-l", context.rootDirectory], {
645
- cwd: context.rootDirectory,
646
- timeout: 6e4
647
- });
648
- if (!result.stdout) return [];
649
- return result.stdout.split("\n").filter((f) => f.length > 0).map((file) => ({
650
- filePath: path.relative(context.rootDirectory, file),
651
- engine: "format",
652
- rule: "go-formatting",
653
- severity: "warning",
654
- message: "Go file is not formatted correctly",
655
- help: "Run `aislop fix` to auto-format with gofmt",
656
- line: 0,
657
- column: 0,
658
- category: "Format",
659
- fixable: true
660
- }));
661
- } catch {
662
- return [];
638
+ const isSymbolUsed = (name, content, importLines, lines) => {
639
+ const pattern = new RegExp(`\\b${name}\\b`, "g");
640
+ let match;
641
+ while ((match = pattern.exec(content)) !== null) {
642
+ const lineIndex = content.slice(0, match.index).split("\n").length - 1;
643
+ if (!importLines.has(lineIndex)) return true;
663
644
  }
645
+ for (let i = 0; i < lines.length; i++) {
646
+ if (importLines.has(i)) continue;
647
+ if (lines[i].includes(name)) return true;
648
+ }
649
+ return false;
664
650
  };
665
- const fixGofmt = async (rootDirectory) => {
666
- const result = await runSubprocess("gofmt", ["-w", rootDirectory], {
667
- cwd: rootDirectory,
668
- timeout: 6e4
669
- });
670
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
671
- };
672
-
673
- //#endregion
674
- //#region src/engines/format/ruff-format.ts
675
- const runRuffFormat = async (context) => {
676
- const ruffBinary = resolveToolBinary("ruff");
651
+ const analyzeFile = (filePath) => {
652
+ if (isAutoGenerated(filePath)) return null;
653
+ let content;
677
654
  try {
678
- const result = await runSubprocess(ruffBinary, [
679
- "format",
680
- "--check",
681
- "--diff",
682
- context.rootDirectory
683
- ], {
684
- cwd: context.rootDirectory,
685
- timeout: 6e4
686
- });
687
- if (result.exitCode === 0) return [];
688
- return parseRuffFormatOutput(result.stdout || result.stderr, context.rootDirectory);
655
+ content = fs.readFileSync(filePath, "utf-8");
689
656
  } catch {
690
- return [];
657
+ return null;
691
658
  }
659
+ const ext = path.extname(filePath);
660
+ const lines = content.split("\n");
661
+ let symbols;
662
+ let importLines;
663
+ if (JS_EXTENSIONS$1.has(ext)) {
664
+ const result = extractJsImportedSymbols(lines);
665
+ symbols = result.symbols;
666
+ importLines = result.importLines;
667
+ } else if (PY_EXTENSIONS.has(ext)) {
668
+ const result = extractPyImportedSymbols(lines);
669
+ symbols = result.symbols;
670
+ importLines = result.importLines;
671
+ } else return null;
672
+ return {
673
+ lines,
674
+ symbols,
675
+ importLines,
676
+ ext
677
+ };
692
678
  };
693
- const parseRuffFormatOutput = (output, rootDir) => {
679
+ const getUnusedSymbols = (lines, symbols, importLines) => {
680
+ const content = lines.join("\n");
681
+ return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
682
+ };
683
+ const detectUnusedImports = async (context) => {
684
+ const files = getSourceFiles(context);
694
685
  const diagnostics = [];
695
- const filePattern = /^--- (.+)$/gm;
696
- let match;
697
- while ((match = filePattern.exec(output)) !== null) {
698
- const filePath = match[1].replace(/^a\//, "");
699
- diagnostics.push({
700
- filePath: path.relative(rootDir, filePath),
701
- engine: "format",
702
- rule: "python-formatting",
686
+ for (const filePath of files) {
687
+ const analysis = analyzeFile(filePath);
688
+ if (!analysis) continue;
689
+ const relativePath = path.relative(context.rootDirectory, filePath);
690
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
691
+ for (const symbol of unused) diagnostics.push({
692
+ filePath: relativePath,
693
+ engine: "ai-slop",
694
+ rule: "ai-slop/unused-import",
703
695
  severity: "warning",
704
- message: "Python file is not formatted correctly",
705
- help: "Run `aislop fix` to auto-format with ruff",
706
- line: 0,
696
+ message: `Imported symbol '${symbol.name}' is never used`,
697
+ help: "Remove unused imports to keep the code clean",
698
+ line: symbol.line,
707
699
  column: 0,
708
- category: "Format",
700
+ category: "AI Slop",
709
701
  fixable: true
710
702
  });
711
703
  }
712
704
  return diagnostics;
713
705
  };
714
- const fixRuffFormat = async (rootDirectory) => {
715
- const result = await runSubprocess(resolveToolBinary("ruff"), ["format", rootDirectory], {
716
- cwd: rootDirectory,
717
- timeout: 6e4
706
+
707
+ //#endregion
708
+ //#region src/engines/ai-slop/unused-imports-fix.ts
709
+ const fixUnusedImports = async (context) => {
710
+ const files = getSourceFiles(context);
711
+ for (const filePath of files) {
712
+ const analysis = analyzeFile(filePath);
713
+ if (!analysis) continue;
714
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
715
+ if (unused.length === 0) continue;
716
+ const unusedNames = new Set(unused.map((u) => u.name));
717
+ const lines = [...analysis.lines];
718
+ const symbolsByLine = /* @__PURE__ */ new Map();
719
+ for (const sym of analysis.symbols) {
720
+ const arr = symbolsByLine.get(sym.line) ?? [];
721
+ arr.push(sym);
722
+ symbolsByLine.set(sym.line, arr);
723
+ }
724
+ const linesToRemove = /* @__PURE__ */ new Set();
725
+ for (const [lineNo, syms] of symbolsByLine) {
726
+ const lineIdx = lineNo - 1;
727
+ const allUnused = syms.every((s) => unusedNames.has(s.name));
728
+ const importSpan = getImportSpan(lineIdx, analysis.importLines);
729
+ if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
730
+ else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
731
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
732
+ }
733
+ if (linesToRemove.size === 0 && unused.length === 0) continue;
734
+ const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
735
+ for (const idx of sortedRemove) lines.splice(idx, 1);
736
+ const filtered = lines.filter((l) => l !== REMOVE_MARKER);
737
+ while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
738
+ fs.writeFileSync(filePath, filtered.join("\n"));
739
+ }
740
+ };
741
+ const getImportSpan = (startIdx, importLines) => {
742
+ const span = [startIdx];
743
+ let idx = startIdx + 1;
744
+ while (importLines.has(idx)) {
745
+ span.push(idx);
746
+ idx++;
747
+ }
748
+ return span;
749
+ };
750
+ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
751
+ const fullImport = span.map((i) => lines[i]).join("\n");
752
+ const namedMatch = fullImport.match(/\{([^}]+)\}/s);
753
+ if (!namedMatch) return;
754
+ const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
755
+ if (unusedNamed.length === 0) return;
756
+ const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
757
+ const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
758
+ const parts = spec.split(/\s+as\s+/);
759
+ const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
760
+ return !unusedNamedSet.has(localName);
718
761
  });
719
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
762
+ if (keptSpecifiers.length === 0) {
763
+ if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
764
+ const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
765
+ lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
766
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
767
+ }
768
+ return;
769
+ }
770
+ const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
771
+ const fromClause = fromMatch ? fromMatch[1].trim() : "";
772
+ const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
773
+ const prefix = importPrefix ? importPrefix[1] : "import ";
774
+ const wasMultiLine = span.length > 1;
775
+ let newImport;
776
+ if (wasMultiLine && keptSpecifiers.length > 2) {
777
+ const indentMatch = lines[span[1]]?.match(/^(\s+)/);
778
+ const indent = indentMatch ? indentMatch[1] : " ";
779
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
780
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
781
+ lines[span[0]] = newImport;
782
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
783
+ };
784
+ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
785
+ const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
786
+ if (!fromMatch) return;
787
+ const prefix = fromMatch[1];
788
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
789
+ const hasParen = importPart.startsWith("(");
790
+ const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
791
+ const parts = spec.split(/\s+as\s+/);
792
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
793
+ return !unusedNames.has(localName);
794
+ });
795
+ if (keptSpecifiers.length === 0) return;
796
+ const joined = keptSpecifiers.join(", ");
797
+ lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
720
798
  };
721
799
 
722
800
  //#endregion
@@ -862,26 +940,213 @@ const runKnip = async (rootDirectory) => {
862
940
  engine: "code-quality",
863
941
  rule: "knip/files",
864
942
  severity: "warning",
865
- message: KNIP_MESSAGE_MAP.files,
866
- help: "This file is not imported by any other file in the project.",
943
+ message: KNIP_MESSAGE_MAP.files,
944
+ help: "This file is not imported by any other file in the project.",
945
+ line: 0,
946
+ column: 0,
947
+ category: "Dead Code",
948
+ fixable: false
949
+ });
950
+ const issues = parsed.issues ?? [];
951
+ const issueTypes = [
952
+ ...DEPENDENCY_TYPES,
953
+ "exports",
954
+ "types",
955
+ "duplicates"
956
+ ];
957
+ for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
958
+ return diagnostics;
959
+ } catch {
960
+ return [];
961
+ }
962
+ };
963
+
964
+ //#endregion
965
+ //#region src/engines/format/biome.ts
966
+ const esmRequire$1 = createRequire(import.meta.url);
967
+ const resolveLocalBiomeScript = () => {
968
+ try {
969
+ const packageJsonPath = esmRequire$1.resolve("@biomejs/biome/package.json");
970
+ return path.join(path.dirname(packageJsonPath), "bin", "biome");
971
+ } catch {
972
+ return null;
973
+ }
974
+ };
975
+ const runBiome = async (args, rootDirectory, timeout) => {
976
+ const localScript = resolveLocalBiomeScript();
977
+ if (localScript) return runSubprocess(process.execPath, [localScript, ...args], {
978
+ cwd: rootDirectory,
979
+ timeout
980
+ });
981
+ return runSubprocess("biome", args, {
982
+ cwd: rootDirectory,
983
+ timeout
984
+ });
985
+ };
986
+ const BIOME_EXTENSIONS = new Set([
987
+ ".js",
988
+ ".jsx",
989
+ ".ts",
990
+ ".tsx",
991
+ ".mjs",
992
+ ".cjs"
993
+ ]);
994
+ const getBiomeTargets = (context) => getSourceFiles(context).filter((filePath) => BIOME_EXTENSIONS.has(path.extname(filePath))).map((filePath) => path.relative(context.rootDirectory, filePath));
995
+ const projectUsesDecorators = (rootDir) => {
996
+ try {
997
+ const tsconfigPath = path.join(rootDir, "tsconfig.json");
998
+ if (!fs.existsSync(tsconfigPath)) return false;
999
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1000
+ return /experimentalDecorators.*true/i.test(content);
1001
+ } catch {
1002
+ return false;
1003
+ }
1004
+ };
1005
+ const runBiomeFormat = async (context) => {
1006
+ const targets = getBiomeTargets(context);
1007
+ if (targets.length === 0) return [];
1008
+ const args = [
1009
+ "format",
1010
+ "--reporter=json",
1011
+ ...targets
1012
+ ];
1013
+ try {
1014
+ const result = await runBiome(args, context.rootDirectory, 6e4);
1015
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
1016
+ if (!output) return [];
1017
+ let diagnostics = parseBiomeJsonOutput(output, context.rootDirectory);
1018
+ if (projectUsesDecorators(context.rootDirectory)) diagnostics = diagnostics.filter((d) => {
1019
+ const msg = d.message.toLowerCase();
1020
+ return !msg.includes("decorator") && !msg.includes("parsing error");
1021
+ });
1022
+ return diagnostics;
1023
+ } catch {
1024
+ return [];
1025
+ }
1026
+ };
1027
+ const parseBiomeJsonOutput = (output, rootDir) => {
1028
+ const diagnostics = [];
1029
+ for (const line of output.split("\n")) {
1030
+ const trimmed = line.trim();
1031
+ if (!trimmed.startsWith("{")) continue;
1032
+ let parsed = null;
1033
+ try {
1034
+ parsed = JSON.parse(trimmed);
1035
+ } catch {
1036
+ parsed = null;
1037
+ }
1038
+ if (!parsed || !Array.isArray(parsed.diagnostics)) continue;
1039
+ for (const entry of parsed.diagnostics) {
1040
+ const rawPath = entry.location?.path;
1041
+ if (!rawPath) continue;
1042
+ const severity = entry.severity === "error" ? "error" : "warning";
1043
+ diagnostics.push({
1044
+ filePath: path.isAbsolute(rawPath) ? path.relative(rootDir, rawPath) : rawPath,
1045
+ engine: "format",
1046
+ rule: "formatting",
1047
+ severity,
1048
+ message: entry.message ?? "File is not formatted correctly",
1049
+ help: "Run `aislop fix` to auto-format",
1050
+ line: entry.location?.start?.line ?? 0,
1051
+ column: entry.location?.start?.column ?? 0,
1052
+ category: "Format",
1053
+ fixable: true
1054
+ });
1055
+ }
1056
+ }
1057
+ return diagnostics;
1058
+ };
1059
+ const fixBiomeFormat = async (context) => {
1060
+ const targets = getBiomeTargets(context);
1061
+ if (targets.length === 0) return;
1062
+ await runBiome([
1063
+ "format",
1064
+ "--write",
1065
+ ...targets
1066
+ ], context.rootDirectory, 6e4);
1067
+ };
1068
+
1069
+ //#endregion
1070
+ //#region src/engines/format/gofmt.ts
1071
+ const runGofmt = async (context) => {
1072
+ try {
1073
+ const result = await runSubprocess("gofmt", ["-l", context.rootDirectory], {
1074
+ cwd: context.rootDirectory,
1075
+ timeout: 6e4
1076
+ });
1077
+ if (!result.stdout) return [];
1078
+ return result.stdout.split("\n").filter((f) => f.length > 0).map((file) => ({
1079
+ filePath: path.relative(context.rootDirectory, file),
1080
+ engine: "format",
1081
+ rule: "go-formatting",
1082
+ severity: "warning",
1083
+ message: "Go file is not formatted correctly",
1084
+ help: "Run `aislop fix` to auto-format with gofmt",
867
1085
  line: 0,
868
1086
  column: 0,
869
- category: "Dead Code",
870
- fixable: false
1087
+ category: "Format",
1088
+ fixable: true
1089
+ }));
1090
+ } catch {
1091
+ return [];
1092
+ }
1093
+ };
1094
+ const fixGofmt = async (rootDirectory) => {
1095
+ const result = await runSubprocess("gofmt", ["-w", rootDirectory], {
1096
+ cwd: rootDirectory,
1097
+ timeout: 6e4
1098
+ });
1099
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `gofmt exited with code ${result.exitCode}`);
1100
+ };
1101
+
1102
+ //#endregion
1103
+ //#region src/engines/format/ruff-format.ts
1104
+ const runRuffFormat = async (context) => {
1105
+ const ruffBinary = resolveToolBinary("ruff");
1106
+ try {
1107
+ const result = await runSubprocess(ruffBinary, [
1108
+ "format",
1109
+ "--check",
1110
+ "--diff",
1111
+ context.rootDirectory
1112
+ ], {
1113
+ cwd: context.rootDirectory,
1114
+ timeout: 6e4
871
1115
  });
872
- const issues = parsed.issues ?? [];
873
- const issueTypes = [
874
- ...DEPENDENCY_TYPES,
875
- "exports",
876
- "types",
877
- "duplicates"
878
- ];
879
- for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
880
- return diagnostics;
1116
+ if (result.exitCode === 0) return [];
1117
+ return parseRuffFormatOutput(result.stdout || result.stderr, context.rootDirectory);
881
1118
  } catch {
882
1119
  return [];
883
1120
  }
884
1121
  };
1122
+ const parseRuffFormatOutput = (output, rootDir) => {
1123
+ const diagnostics = [];
1124
+ const filePattern = /^--- (.+)$/gm;
1125
+ let match;
1126
+ while ((match = filePattern.exec(output)) !== null) {
1127
+ const filePath = match[1].replace(/^a\//, "");
1128
+ diagnostics.push({
1129
+ filePath: path.relative(rootDir, filePath),
1130
+ engine: "format",
1131
+ rule: "python-formatting",
1132
+ severity: "warning",
1133
+ message: "Python file is not formatted correctly",
1134
+ help: "Run `aislop fix` to auto-format with ruff",
1135
+ line: 0,
1136
+ column: 0,
1137
+ category: "Format",
1138
+ fixable: true
1139
+ });
1140
+ }
1141
+ return diagnostics;
1142
+ };
1143
+ const fixRuffFormat = async (rootDirectory) => {
1144
+ const result = await runSubprocess(resolveToolBinary("ruff"), ["format", rootDirectory], {
1145
+ cwd: rootDirectory,
1146
+ timeout: 6e4
1147
+ });
1148
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff format exited with code ${result.exitCode}`);
1149
+ };
885
1150
 
886
1151
  //#endregion
887
1152
  //#region src/engines/lint/oxlint-config.ts
@@ -1057,6 +1322,7 @@ const fixOxlint = async (context) => {
1057
1322
  "-c",
1058
1323
  configPath,
1059
1324
  "--fix",
1325
+ "--fix-suggestions",
1060
1326
  "."
1061
1327
  ];
1062
1328
  const result = await runSubprocess(process.execPath, args, {
@@ -1111,81 +1377,6 @@ const fixRuffLint = async (rootDirectory) => {
1111
1377
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
1112
1378
  };
1113
1379
 
1114
- //#endregion
1115
- //#region src/output/pager.ts
1116
- const DEFAULT_COLUMNS = 80;
1117
- const DEFAULT_ROWS = 24;
1118
- const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
1119
- const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
1120
- const resolvePagerCommand = () => {
1121
- const pager = process.env.PAGER?.trim();
1122
- if (pager) {
1123
- const [command, ...args] = pager.split(/\s+/);
1124
- if (command) return {
1125
- command,
1126
- args
1127
- };
1128
- }
1129
- return {
1130
- command: "less",
1131
- args: [
1132
- "-R",
1133
- "-F",
1134
- "-X"
1135
- ]
1136
- };
1137
- };
1138
- const writeToStdout = (text) => {
1139
- process.stdout.write(text);
1140
- };
1141
- const pipeToPager = async (command, args, text) => new Promise((resolve) => {
1142
- let settled = false;
1143
- const finish = (success) => {
1144
- if (settled) return;
1145
- settled = true;
1146
- resolve(success);
1147
- };
1148
- try {
1149
- const child = spawn(command, args, {
1150
- stdio: [
1151
- "pipe",
1152
- "inherit",
1153
- "inherit"
1154
- ],
1155
- windowsHide: true
1156
- });
1157
- child.once("error", () => finish(false));
1158
- child.once("close", (code) => finish(code === 0));
1159
- child.stdin?.on("error", () => void 0);
1160
- child.stdin?.end(text);
1161
- } catch {
1162
- finish(false);
1163
- }
1164
- });
1165
- const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
1166
- const width = Math.max(1, columns);
1167
- return text.split("\n").reduce((count, line) => {
1168
- const visibleLine = stripAnsi(line).replaceAll(" ", " ");
1169
- return count + Math.max(1, Math.ceil(visibleLine.length / width));
1170
- }, 0);
1171
- };
1172
- const shouldPageOutput = (text, options = {}) => {
1173
- if (text.trim().length === 0) return false;
1174
- const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
1175
- const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
1176
- if (!stdinIsTTY || !stdoutIsTTY) return false;
1177
- const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
1178
- return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
1179
- };
1180
- const printMaybePaged = async (text) => {
1181
- if (!shouldPageOutput(text)) {
1182
- writeToStdout(text);
1183
- return;
1184
- }
1185
- const pager = resolvePagerCommand();
1186
- if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
1187
- };
1188
-
1189
1380
  //#endregion
1190
1381
  //#region src/utils/telemetry.ts
1191
1382
  /**
@@ -1333,7 +1524,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
1333
1524
  }
1334
1525
  lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
1335
1526
  if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
1336
- await printMaybePaged(`${lines.join("\n")}\n\n`);
1527
+ process.stdout.write(`${lines.join("\n")}\n\n`);
1337
1528
  return result;
1338
1529
  };
1339
1530
  const createEngineContext = (rootDirectory, projectInfo, config) => ({
@@ -1376,13 +1567,7 @@ const fixCommand = async (directory, config, options = {
1376
1567
  printProjectMetadata(projectInfo);
1377
1568
  const context = createEngineContext(resolvedDir, projectInfo, config);
1378
1569
  const steps = [];
1379
- if (config.engines.format) {
1380
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
1381
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
1382
- else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
1383
- if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
1384
- else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
1385
- }
1570
+ if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
1386
1571
  if (config.engines.lint) {
1387
1572
  if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
1388
1573
  if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
@@ -1391,6 +1576,13 @@ const fixCommand = async (directory, config, options = {
1391
1576
  if (config.engines["code-quality"]) {
1392
1577
  if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
1393
1578
  }
1579
+ if (config.engines.format) {
1580
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
1581
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
1582
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
1583
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
1584
+ else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
1585
+ }
1394
1586
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
1395
1587
  else {
1396
1588
  logger.break();
@@ -1873,7 +2065,7 @@ const detectTrivialComments = async (context) => {
1873
2065
 
1874
2066
  //#endregion
1875
2067
  //#region src/engines/ai-slop/dead-patterns.ts
1876
- const JS_EXTENSIONS$1 = new Set([
2068
+ const JS_EXTENSIONS = new Set([
1877
2069
  ".ts",
1878
2070
  ".tsx",
1879
2071
  ".js",
@@ -1885,7 +2077,7 @@ const CONSOLE_LOG_PATTERN = /\bconsole\.(?:log|debug|info|trace|dir|table)\s*\(/
1885
2077
  const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
1886
2078
  const SCRIPT_DIR_PATTERN = /(?:^|\/)(scripts|bin)\//;
1887
2079
  const detectConsoleLeftovers = (content, relativePath, ext) => {
1888
- if (!JS_EXTENSIONS$1.has(ext)) return [];
2080
+ if (!JS_EXTENSIONS.has(ext)) return [];
1889
2081
  if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
1890
2082
  if (SCRIPT_DIR_PATTERN.test(relativePath)) return [];
1891
2083
  const diagnostics = [];
@@ -1945,7 +2137,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1945
2137
  for (let i = 0; i < lines.length; i++) {
1946
2138
  const trimmed = lines[i].trim();
1947
2139
  const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
1948
- if (JS_EXTENSIONS$1.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push({
2140
+ if (JS_EXTENSIONS.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push({
1949
2141
  filePath: relativePath,
1950
2142
  engine: "ai-slop",
1951
2143
  rule: "ai-slop/unreachable-code",
@@ -1969,7 +2161,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
1969
2161
  category: "AI Slop",
1970
2162
  fixable: false
1971
2163
  });
1972
- if (JS_EXTENSIONS$1.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push({
2164
+ if (JS_EXTENSIONS.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push({
1973
2165
  filePath: relativePath,
1974
2166
  engine: "ai-slop",
1975
2167
  rule: "ai-slop/empty-function",
@@ -2145,166 +2337,6 @@ const detectSwallowedExceptions = async (context) => {
2145
2337
  return diagnostics;
2146
2338
  };
2147
2339
 
2148
- //#endregion
2149
- //#region src/engines/ai-slop/unused-imports.ts
2150
- const JS_EXTENSIONS = new Set([
2151
- ".ts",
2152
- ".tsx",
2153
- ".js",
2154
- ".jsx",
2155
- ".mjs",
2156
- ".cjs"
2157
- ]);
2158
- const PY_EXTENSIONS = new Set([".py"]);
2159
- const extractJsImportedSymbols = (lines) => {
2160
- const symbols = [];
2161
- const importLines = /* @__PURE__ */ new Set();
2162
- for (let i = 0; i < lines.length; i++) {
2163
- const trimmed = lines[i].trim();
2164
- if (!trimmed.startsWith("import ")) continue;
2165
- importLines.add(i);
2166
- if (/^import\s+["']/.test(trimmed)) continue;
2167
- if (/^import\s+type\s/.test(trimmed)) continue;
2168
- let fullImport = trimmed;
2169
- let endLine = i;
2170
- while (!fullImport.includes("from") && endLine < lines.length - 1) {
2171
- endLine++;
2172
- fullImport += ` ${lines[endLine].trim()}`;
2173
- importLines.add(endLine);
2174
- }
2175
- const namespaceMatch = fullImport.match(/import\s+\*\s+as\s+(\w+)\s+from/);
2176
- if (namespaceMatch) {
2177
- symbols.push({
2178
- name: namespaceMatch[1],
2179
- line: i + 1,
2180
- isDefault: false,
2181
- isNamespace: true
2182
- });
2183
- continue;
2184
- }
2185
- const defaultMatch = fullImport.match(/import\s+(\w+)\s*(?:,\s*\{[^}]*\})?\s+from/);
2186
- if (defaultMatch && defaultMatch[1] !== "type") symbols.push({
2187
- name: defaultMatch[1],
2188
- line: i + 1,
2189
- isDefault: true,
2190
- isNamespace: false
2191
- });
2192
- const namedMatch = fullImport.match(/\{([^}]+)\}/);
2193
- if (namedMatch) {
2194
- const namedImports = namedMatch[1].split(",");
2195
- for (const ni of namedImports) {
2196
- const parts = ni.trim().split(/\s+as\s+/);
2197
- if (parts.length === 0 || !parts[0]) continue;
2198
- const cleanParts = parts.map((p) => p.trim().replace(/^type\s+/, ""));
2199
- const localName = cleanParts.length > 1 ? cleanParts[1] : cleanParts[0];
2200
- if (localName && /^\w+$/.test(localName)) symbols.push({
2201
- name: localName,
2202
- line: i + 1,
2203
- isDefault: false,
2204
- isNamespace: false
2205
- });
2206
- }
2207
- }
2208
- }
2209
- return {
2210
- symbols,
2211
- importLines
2212
- };
2213
- };
2214
- const extractPyImportedSymbols = (lines) => {
2215
- const symbols = [];
2216
- const importLines = /* @__PURE__ */ new Set();
2217
- for (let i = 0; i < lines.length; i++) {
2218
- const trimmed = lines[i].trim();
2219
- const fromMatch = trimmed.match(/^from\s+[\w.]+\s+import\s+(.+)/);
2220
- if (fromMatch) {
2221
- importLines.add(i);
2222
- const importPart = fromMatch[1].replace(/#.*$/, "").trim();
2223
- if (importPart === "*") continue;
2224
- const cleaned = importPart.replace(/[()]/g, "");
2225
- for (const item of cleaned.split(",")) {
2226
- const parts = item.trim().split(/\s+as\s+/);
2227
- const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
2228
- if (localName && /^\w+$/.test(localName)) symbols.push({
2229
- name: localName,
2230
- line: i + 1,
2231
- isDefault: false,
2232
- isNamespace: false
2233
- });
2234
- }
2235
- continue;
2236
- }
2237
- const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
2238
- if (importMatch) {
2239
- importLines.add(i);
2240
- const simpleName = (importMatch[2] ?? importMatch[1]).split(".")[0];
2241
- if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
2242
- name: simpleName,
2243
- line: i + 1,
2244
- isDefault: false,
2245
- isNamespace: true
2246
- });
2247
- }
2248
- }
2249
- return {
2250
- symbols,
2251
- importLines
2252
- };
2253
- };
2254
- const isSymbolUsed = (name, content, importLines, lines) => {
2255
- const pattern = new RegExp(`\\b${name}\\b`, "g");
2256
- let match;
2257
- while ((match = pattern.exec(content)) !== null) {
2258
- const lineIndex = content.slice(0, match.index).split("\n").length - 1;
2259
- if (!importLines.has(lineIndex)) return true;
2260
- }
2261
- for (let i = 0; i < lines.length; i++) {
2262
- if (importLines.has(i)) continue;
2263
- if (lines[i].includes(name)) return true;
2264
- }
2265
- return false;
2266
- };
2267
- const detectUnusedImports = async (context) => {
2268
- const files = getSourceFiles(context);
2269
- const diagnostics = [];
2270
- for (const filePath of files) {
2271
- if (isAutoGenerated(filePath)) continue;
2272
- let content;
2273
- try {
2274
- content = fs.readFileSync(filePath, "utf-8");
2275
- } catch {
2276
- continue;
2277
- }
2278
- const ext = path.extname(filePath);
2279
- const relativePath = path.relative(context.rootDirectory, filePath);
2280
- const lines = content.split("\n");
2281
- let symbols;
2282
- let importLinesSet;
2283
- if (JS_EXTENSIONS.has(ext)) {
2284
- const result = extractJsImportedSymbols(lines);
2285
- symbols = result.symbols;
2286
- importLinesSet = result.importLines;
2287
- } else if (PY_EXTENSIONS.has(ext)) {
2288
- const result = extractPyImportedSymbols(lines);
2289
- symbols = result.symbols;
2290
- importLinesSet = result.importLines;
2291
- } else continue;
2292
- for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
2293
- filePath: relativePath,
2294
- engine: "ai-slop",
2295
- rule: "ai-slop/unused-import",
2296
- severity: "warning",
2297
- message: `Imported symbol '${symbol.name}' is never used`,
2298
- help: "Remove unused imports to keep the code clean",
2299
- line: symbol.line,
2300
- column: 0,
2301
- category: "AI Slop",
2302
- fixable: true
2303
- });
2304
- }
2305
- return diagnostics;
2306
- };
2307
-
2308
2340
  //#endregion
2309
2341
  //#region src/engines/ai-slop/index.ts
2310
2342
  const aiSlopEngine = {
@@ -4069,17 +4101,18 @@ const scanCommand = async (directory, config, options) => {
4069
4101
  });
4070
4102
  }
4071
4103
  if (options.json) {
4072
- const { buildJsonOutput } = await import("./json-UG8l_sLC.js");
4104
+ const { buildJsonOutput } = await import("./json-Ci_gvHLS.js");
4073
4105
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
4074
4106
  console.log(JSON.stringify(jsonOut, null, 2));
4075
4107
  return { exitCode };
4076
4108
  }
4077
- await printMaybePaged([
4109
+ const output = [
4078
4110
  "",
4079
4111
  allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
4080
4112
  renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
4081
4113
  ""
4082
- ].join("\n"));
4114
+ ].join("\n");
4115
+ process.stdout.write(output);
4083
4116
  return { exitCode };
4084
4117
  };
4085
4118