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/README.md CHANGED
@@ -10,25 +10,87 @@
10
10
 
11
11
  `aislop` is a unified code-quality CLI that catches the lazy patterns AI coding tools leave behind. One command, one score out of 100.
12
12
 
13
+ `aislop` helps teams review AI-assisted code faster by combining formatting, linting, maintainability, AI-pattern detection, architecture checks, and security checks into a single report.
14
+
15
+ ## See it in action
16
+
17
+ ### Scan
18
+
19
+ ![aislop scan demo](assets/scan.gif)
20
+
21
+ ### Fix
22
+
23
+ ![aislop fix demo](assets/fix.gif)
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ # scan your project
29
+ npx aislop scan
30
+
31
+ # auto-fix what can be fixed safely
32
+ npx aislop fix
33
+
34
+ # CI mode (JSON output + quality gate)
35
+ npx aislop ci
13
36
  ```
14
- $ npx aislop scan
15
37
 
16
- aislop Scan v0.2.0
38
+ Sample output:
39
+
40
+ ```text
41
+ aislop scan v0.2.0
17
42
 
18
43
  ✓ Project my-app (typescript)
19
44
  Source files: 142
20
45
 
21
- ✓ Formatting: done (0 issues)
22
- Linting: done (2 warnings)
23
- Code Quality: done (1 warning)
24
- ! Maintainability: done (4 warnings)
25
- ✓ Security: done (0 issues)
26
-
27
- Score: 80/100 (Healthy)
46
+ ✓ Formatting: done (0 issues)
47
+ ! Linting: done (2 warnings)
48
+ ! Code Quality: done (1 warning)
49
+ Maintainability: done (0 issues)
50
+ ✓ Security: done (0 issues)
51
+
52
+ ------------------------------------------------------------
53
+ Summary
54
+ Score: 89/100 (Healthy)
55
+ Issues: 0 errors, 3 warnings
56
+ Auto-fixable: 2
57
+ Files: 142
58
+ Time: 2.3s
59
+ ------------------------------------------------------------
28
60
  ```
29
61
 
30
62
  ---
31
63
 
64
+ ## Why aislop
65
+
66
+ AI-generated changes often pass review because problems are spread across many files and many categories.
67
+ `aislop` gives you one view and one score.
68
+
69
+ - **One command, full picture**: formatting + lint + maintainability + AI slop + security (+ architecture)
70
+ - **Score-based quality gate**: use a single 0-100 score in CI and PR checks
71
+ - **Auto-fix support**: remove unused imports, apply lint suggestions, and format in one pass
72
+ - **Duplication visibility**: flag repeated blocks and encourage extraction into shared modules
73
+ - **Software engineering best practices**: enforce function/file size limits, nesting limits, dead code cleanup, and safer patterns
74
+ - **Works across stacks**: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo/React Native
75
+ - **Zero-config start**: run `npx aislop scan` and get useful output immediately
76
+
77
+ ## What it catches
78
+
79
+ Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
80
+
81
+ | Engine | Examples |
82
+ |---|---|
83
+ | Formatting | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
84
+ | Linting | oxlint, ruff, golangci-lint, clippy, expo-doctor |
85
+ | Code Quality | Function/file size limits, deep nesting, duplication, dead code, unused dependencies (knip) |
86
+ | AI Slop | Trivial comments, swallowed exceptions, unused imports, console leftovers, type assertion abuse, TODO stubs |
87
+ | Security | Hardcoded secrets, eval, innerHTML, SQL/shell injection, dependency audits |
88
+ | Architecture | Custom import bans, layering rules, required patterns |
89
+
90
+ See the full [rules reference](docs/rules.md).
91
+
92
+ ---
93
+
32
94
  ## Installation
33
95
 
34
96
  ```bash
@@ -67,7 +129,7 @@ aislop scan --json # output JSON
67
129
  ### Fix issues automatically
68
130
 
69
131
  ```bash
70
- aislop fix # auto-fix formatting + lint issues
132
+ aislop fix # auto-fix unused imports, formatting, and lint fixes
71
133
  ```
72
134
 
73
135
  ### Use in CI pipelines
@@ -76,6 +138,19 @@ aislop fix # auto-fix formatting + lint issues
76
138
  aislop ci # JSON output, exits 1 if score < threshold
77
139
  ```
78
140
 
141
+ ### Common workflow
142
+
143
+ ```bash
144
+ # before commit
145
+ aislop scan --staged
146
+
147
+ # during local cleanup
148
+ aislop fix
149
+
150
+ # full project check
151
+ aislop scan
152
+ ```
153
+
79
154
  ### Other commands
80
155
 
81
156
  ```bash
@@ -103,6 +178,7 @@ npx aislop scan --staged
103
178
  - uses: actions/setup-node@v6
104
179
  with:
105
180
  node-version: 20
181
+ - run: npm ci
106
182
  - run: npx aislop ci
107
183
  ```
108
184
 
@@ -119,34 +195,6 @@ ci:
119
195
 
120
196
  ---
121
197
 
122
- ## Why aislop?
123
-
124
- AI-generated code passes review because issues are spread across dozens of files. No single linter catches all of them. `aislop` does:
125
-
126
- - **AI-specific pattern detection** — trivial comments, thin wrappers, generic names, swallowed exceptions, `as any` casts
127
- - **Multi-language** — TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo/React Native
128
- - **Single score** — one number to gate PRs, track in CI, and trend over time
129
- - **Zero config** — run `npx aislop scan` and get results immediately
130
- - **Framework-aware** — auto-detects Next.js, React, Expo, Vite, Remix, Django, Flask, FastAPI
131
- - **Batteries included** — ships with oxlint, biome, knip; downloads ruff and golangci-lint on install
132
-
133
- ## What it catches
134
-
135
- Six engines run in parallel: **Formatting**, **Linting**, **Code Quality**, **AI Slop Detection**, **Security**, and **Architecture** (opt-in).
136
-
137
- | Engine | Examples |
138
- |---|---|
139
- | Formatting | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
140
- | Linting | oxlint, ruff, golangci-lint, clippy, expo-doctor |
141
- | Code Quality | Function/file size limits, deep nesting, duplication, dead code, unused dependencies (knip) |
142
- | AI Slop | Trivial comments, swallowed exceptions, unused imports, console leftovers, type assertion abuse, TODO stubs |
143
- | Security | Hardcoded secrets, eval, innerHTML, SQL/shell injection, dependency audits |
144
- | Architecture | Custom import bans, layering rules, required patterns |
145
-
146
- See the full [rules reference](docs/rules.md) for all 30+ built-in rules.
147
-
148
- ---
149
-
150
198
  ## Documentation
151
199
 
152
200
  | Topic | Link |
package/dist/cli.js CHANGED
@@ -872,6 +872,7 @@ const JS_EXTENSIONS = new Set([
872
872
  ".cjs"
873
873
  ]);
874
874
  const PY_EXTENSIONS = new Set([".py"]);
875
+ const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
875
876
  const extractJsImportedSymbols = (lines) => {
876
877
  const symbols = [];
877
878
  const importLines = /* @__PURE__ */ new Set();
@@ -980,32 +981,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
980
981
  }
981
982
  return false;
982
983
  };
984
+ const analyzeFile = (filePath) => {
985
+ if (isAutoGenerated(filePath)) return null;
986
+ let content;
987
+ try {
988
+ content = fs.readFileSync(filePath, "utf-8");
989
+ } catch {
990
+ return null;
991
+ }
992
+ const ext = path.extname(filePath);
993
+ const lines = content.split("\n");
994
+ let symbols;
995
+ let importLines;
996
+ if (JS_EXTENSIONS.has(ext)) {
997
+ const result = extractJsImportedSymbols(lines);
998
+ symbols = result.symbols;
999
+ importLines = result.importLines;
1000
+ } else if (PY_EXTENSIONS.has(ext)) {
1001
+ const result = extractPyImportedSymbols(lines);
1002
+ symbols = result.symbols;
1003
+ importLines = result.importLines;
1004
+ } else return null;
1005
+ return {
1006
+ lines,
1007
+ symbols,
1008
+ importLines,
1009
+ ext
1010
+ };
1011
+ };
1012
+ const getUnusedSymbols = (lines, symbols, importLines) => {
1013
+ const content = lines.join("\n");
1014
+ return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
1015
+ };
983
1016
  const detectUnusedImports = async (context) => {
984
1017
  const files = getSourceFiles(context);
985
1018
  const diagnostics = [];
986
1019
  for (const filePath of files) {
987
- if (isAutoGenerated(filePath)) continue;
988
- let content;
989
- try {
990
- content = fs.readFileSync(filePath, "utf-8");
991
- } catch {
992
- continue;
993
- }
994
- const ext = path.extname(filePath);
1020
+ const analysis = analyzeFile(filePath);
1021
+ if (!analysis) continue;
995
1022
  const relativePath = path.relative(context.rootDirectory, filePath);
996
- const lines = content.split("\n");
997
- let symbols;
998
- let importLinesSet;
999
- if (JS_EXTENSIONS.has(ext)) {
1000
- const result = extractJsImportedSymbols(lines);
1001
- symbols = result.symbols;
1002
- importLinesSet = result.importLines;
1003
- } else if (PY_EXTENSIONS.has(ext)) {
1004
- const result = extractPyImportedSymbols(lines);
1005
- symbols = result.symbols;
1006
- importLinesSet = result.importLines;
1007
- } else continue;
1008
- for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
1023
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
1024
+ for (const symbol of unused) diagnostics.push({
1009
1025
  filePath: relativePath,
1010
1026
  engine: "ai-slop",
1011
1027
  rule: "ai-slop/unused-import",
@@ -1922,14 +1938,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
1922
1938
  const fixBiomeFormat = async (context) => {
1923
1939
  const targets = getBiomeTargets(context);
1924
1940
  if (targets.length === 0) return;
1925
- const result = await runBiome([
1926
- "check",
1941
+ await runBiome([
1942
+ "format",
1927
1943
  "--write",
1928
- "--formatter-enabled=true",
1929
- "--linter-enabled=false",
1930
1944
  ...targets
1931
1945
  ], context.rootDirectory, 6e4);
1932
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
1933
1946
  };
1934
1947
 
1935
1948
  //#endregion
@@ -2492,6 +2505,7 @@ const fixOxlint = async (context) => {
2492
2505
  "-c",
2493
2506
  configPath,
2494
2507
  "--fix",
2508
+ "--fix-suggestions",
2495
2509
  "."
2496
2510
  ];
2497
2511
  const result = await runSubprocess(process.execPath, args, {
@@ -3180,14 +3194,13 @@ const logger = {
3180
3194
  * Application version — injected at build time by tsdown from package.json.
3181
3195
  * The fallback should always match the "version" field in package.json.
3182
3196
  */
3183
- const APP_VERSION = "0.2.0";
3197
+ const APP_VERSION = "0.2.1";
3184
3198
 
3185
3199
  //#endregion
3186
3200
  //#region src/output/layout.ts
3187
3201
  const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3188
3202
  const printCommandHeader = (commandName) => {
3189
- logger.log(highlighter.bold(`aislop ${commandName}`));
3190
- logger.log(highlighter.dim(`v${APP_VERSION}`));
3203
+ logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
3191
3204
  logger.break();
3192
3205
  };
3193
3206
  const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
@@ -3198,81 +3211,6 @@ const printProjectMetadata = (project) => {
3198
3211
  logger.break();
3199
3212
  };
3200
3213
 
3201
- //#endregion
3202
- //#region src/output/pager.ts
3203
- const DEFAULT_COLUMNS = 80;
3204
- const DEFAULT_ROWS = 24;
3205
- const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
3206
- const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
3207
- const resolvePagerCommand = () => {
3208
- const pager = process.env.PAGER?.trim();
3209
- if (pager) {
3210
- const [command, ...args] = pager.split(/\s+/);
3211
- if (command) return {
3212
- command,
3213
- args
3214
- };
3215
- }
3216
- return {
3217
- command: "less",
3218
- args: [
3219
- "-R",
3220
- "-F",
3221
- "-X"
3222
- ]
3223
- };
3224
- };
3225
- const writeToStdout = (text) => {
3226
- process.stdout.write(text);
3227
- };
3228
- const pipeToPager = async (command, args, text) => new Promise((resolve) => {
3229
- let settled = false;
3230
- const finish = (success) => {
3231
- if (settled) return;
3232
- settled = true;
3233
- resolve(success);
3234
- };
3235
- try {
3236
- const child = spawn(command, args, {
3237
- stdio: [
3238
- "pipe",
3239
- "inherit",
3240
- "inherit"
3241
- ],
3242
- windowsHide: true
3243
- });
3244
- child.once("error", () => finish(false));
3245
- child.once("close", (code) => finish(code === 0));
3246
- child.stdin?.on("error", () => void 0);
3247
- child.stdin?.end(text);
3248
- } catch {
3249
- finish(false);
3250
- }
3251
- });
3252
- const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
3253
- const width = Math.max(1, columns);
3254
- return text.split("\n").reduce((count, line) => {
3255
- const visibleLine = stripAnsi(line).replaceAll(" ", " ");
3256
- return count + Math.max(1, Math.ceil(visibleLine.length / width));
3257
- }, 0);
3258
- };
3259
- const shouldPageOutput = (text, options = {}) => {
3260
- if (text.trim().length === 0) return false;
3261
- const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
3262
- const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
3263
- if (!stdinIsTTY || !stdoutIsTTY) return false;
3264
- const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
3265
- return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
3266
- };
3267
- const printMaybePaged = async (text) => {
3268
- if (!shouldPageOutput(text)) {
3269
- writeToStdout(text);
3270
- return;
3271
- }
3272
- const pager = resolvePagerCommand();
3273
- if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
3274
- };
3275
-
3276
3214
  //#endregion
3277
3215
  //#region src/output/scan-progress.ts
3278
3216
  const SPINNER_FRAMES = [
@@ -3882,12 +3820,13 @@ const scanCommand = async (directory, config, options) => {
3882
3820
  console.log(JSON.stringify(jsonOut, null, 2));
3883
3821
  return { exitCode };
3884
3822
  }
3885
- await printMaybePaged([
3823
+ const output = [
3886
3824
  "",
3887
3825
  allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
3888
3826
  renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
3889
3827
  ""
3890
- ].join("\n"));
3828
+ ].join("\n");
3829
+ process.stdout.write(output);
3891
3830
  return { exitCode };
3892
3831
  };
3893
3832
 
@@ -4037,6 +3976,99 @@ const doctorCommand = async (directory) => {
4037
3976
  printDoctorConclusion(isAllGood());
4038
3977
  };
4039
3978
 
3979
+ //#endregion
3980
+ //#region src/engines/ai-slop/unused-imports-fix.ts
3981
+ const fixUnusedImports = async (context) => {
3982
+ const files = getSourceFiles(context);
3983
+ for (const filePath of files) {
3984
+ const analysis = analyzeFile(filePath);
3985
+ if (!analysis) continue;
3986
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
3987
+ if (unused.length === 0) continue;
3988
+ const unusedNames = new Set(unused.map((u) => u.name));
3989
+ const lines = [...analysis.lines];
3990
+ const symbolsByLine = /* @__PURE__ */ new Map();
3991
+ for (const sym of analysis.symbols) {
3992
+ const arr = symbolsByLine.get(sym.line) ?? [];
3993
+ arr.push(sym);
3994
+ symbolsByLine.set(sym.line, arr);
3995
+ }
3996
+ const linesToRemove = /* @__PURE__ */ new Set();
3997
+ for (const [lineNo, syms] of symbolsByLine) {
3998
+ const lineIdx = lineNo - 1;
3999
+ const allUnused = syms.every((s) => unusedNames.has(s.name));
4000
+ const importSpan = getImportSpan(lineIdx, analysis.importLines);
4001
+ if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
4002
+ else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
4003
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
4004
+ }
4005
+ if (linesToRemove.size === 0 && unused.length === 0) continue;
4006
+ const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
4007
+ for (const idx of sortedRemove) lines.splice(idx, 1);
4008
+ const filtered = lines.filter((l) => l !== REMOVE_MARKER);
4009
+ while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
4010
+ fs.writeFileSync(filePath, filtered.join("\n"));
4011
+ }
4012
+ };
4013
+ const getImportSpan = (startIdx, importLines) => {
4014
+ const span = [startIdx];
4015
+ let idx = startIdx + 1;
4016
+ while (importLines.has(idx)) {
4017
+ span.push(idx);
4018
+ idx++;
4019
+ }
4020
+ return span;
4021
+ };
4022
+ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4023
+ const fullImport = span.map((i) => lines[i]).join("\n");
4024
+ const namedMatch = fullImport.match(/\{([^}]+)\}/s);
4025
+ if (!namedMatch) return;
4026
+ const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
4027
+ if (unusedNamed.length === 0) return;
4028
+ const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
4029
+ const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
4030
+ const parts = spec.split(/\s+as\s+/);
4031
+ const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
4032
+ return !unusedNamedSet.has(localName);
4033
+ });
4034
+ if (keptSpecifiers.length === 0) {
4035
+ if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
4036
+ const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
4037
+ lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
4038
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4039
+ }
4040
+ return;
4041
+ }
4042
+ const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
4043
+ const fromClause = fromMatch ? fromMatch[1].trim() : "";
4044
+ const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
4045
+ const prefix = importPrefix ? importPrefix[1] : "import ";
4046
+ const wasMultiLine = span.length > 1;
4047
+ let newImport;
4048
+ if (wasMultiLine && keptSpecifiers.length > 2) {
4049
+ const indentMatch = lines[span[1]]?.match(/^(\s+)/);
4050
+ const indent = indentMatch ? indentMatch[1] : " ";
4051
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
4052
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
4053
+ lines[span[0]] = newImport;
4054
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4055
+ };
4056
+ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
4057
+ const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
4058
+ if (!fromMatch) return;
4059
+ const prefix = fromMatch[1];
4060
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
4061
+ const hasParen = importPart.startsWith("(");
4062
+ const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
4063
+ const parts = spec.split(/\s+as\s+/);
4064
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
4065
+ return !unusedNames.has(localName);
4066
+ });
4067
+ if (keptSpecifiers.length === 0) return;
4068
+ const joined = keptSpecifiers.join(", ");
4069
+ lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
4070
+ };
4071
+
4040
4072
  //#endregion
4041
4073
  //#region src/commands/fix.ts
4042
4074
  const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
@@ -4091,7 +4123,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
4091
4123
  }
4092
4124
  lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4093
4125
  if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4094
- await printMaybePaged(`${lines.join("\n")}\n\n`);
4126
+ process.stdout.write(`${lines.join("\n")}\n\n`);
4095
4127
  return result;
4096
4128
  };
4097
4129
  const createEngineContext = (rootDirectory, projectInfo, config) => ({
@@ -4134,13 +4166,7 @@ const fixCommand = async (directory, config, options = {
4134
4166
  printProjectMetadata(projectInfo);
4135
4167
  const context = createEngineContext(resolvedDir, projectInfo, config);
4136
4168
  const steps = [];
4137
- if (config.engines.format) {
4138
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
4139
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
4140
- else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
4141
- if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
4142
- else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4143
- }
4169
+ if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
4144
4170
  if (config.engines.lint) {
4145
4171
  if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4146
4172
  if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
@@ -4149,6 +4175,13 @@ const fixCommand = async (directory, config, options = {
4149
4175
  if (config.engines["code-quality"]) {
4150
4176
  if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
4151
4177
  }
4178
+ if (config.engines.format) {
4179
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
4180
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
4181
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
4182
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
4183
+ else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4184
+ }
4152
4185
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
4153
4186
  else {
4154
4187
  logger.break();
@@ -4289,7 +4322,7 @@ const rulesCommand = async (directory) => {
4289
4322
  lines.push("");
4290
4323
  }
4291
4324
  }
4292
- await printMaybePaged(`${lines.join("\n")}\n`);
4325
+ process.stdout.write(`${lines.join("\n")}\n`);
4293
4326
  };
4294
4327
 
4295
4328
  //#endregion
@@ -3,7 +3,7 @@
3
3
  * Application version — injected at build time by tsdown from package.json.
4
4
  * The fallback should always match the "version" field in package.json.
5
5
  */
6
- const APP_VERSION = "0.2.0";
6
+ const APP_VERSION = "0.2.1";
7
7
 
8
8
  //#endregion
9
9
  //#region src/output/engine-info.ts