aislop 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,6 +13,23 @@ import pc from "picocolors";
13
13
  import ora from "ora";
14
14
  import { emitKeypressEvents } from "node:readline";
15
15
 
16
+ //#region \0rolldown/runtime.js
17
+ var __defProp = Object.defineProperty;
18
+ var __exportAll = (all, no_symbols) => {
19
+ let target = {};
20
+ for (var name in all) {
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true
24
+ });
25
+ }
26
+ if (!no_symbols) {
27
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
28
+ }
29
+ return target;
30
+ };
31
+
32
+ //#endregion
16
33
  //#region src/config/defaults.ts
17
34
  const DEFAULT_CONFIG = {
18
35
  version: 1,
@@ -36,18 +53,18 @@ const DEFAULT_CONFIG = {
36
53
  },
37
54
  scoring: {
38
55
  weights: {
39
- format: .5,
40
- lint: 1,
41
- "code-quality": 1.5,
42
- "ai-slop": 1,
56
+ format: .3,
57
+ lint: .6,
58
+ "code-quality": .8,
59
+ "ai-slop": 2.5,
43
60
  architecture: 1,
44
- security: 2
61
+ security: 1.5
45
62
  },
46
63
  thresholds: {
47
64
  good: 75,
48
65
  ok: 50
49
66
  },
50
- smoothing: 10
67
+ smoothing: 20
51
68
  },
52
69
  ci: {
53
70
  failBelow: 0,
@@ -77,15 +94,16 @@ security:
77
94
 
78
95
  scoring:
79
96
  weights:
80
- format: 0.5
81
- lint: 1.0
82
- code-quality: 1.5
83
- ai-slop: 1.0
97
+ format: 0.3
98
+ lint: 0.6
99
+ code-quality: 0.8
100
+ ai-slop: 2.5
84
101
  architecture: 1.0
85
- security: 2.0
102
+ security: 1.5
86
103
  thresholds:
87
104
  good: 75
88
105
  ok: 50
106
+ smoothing: 20
89
107
 
90
108
  ci:
91
109
  failBelow: 0
@@ -113,12 +131,12 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
113
131
  //#endregion
114
132
  //#region src/config/schema.ts
115
133
  const DEFAULT_WEIGHTS = {
116
- format: .5,
117
- lint: 1,
118
- "code-quality": 1.5,
119
- "ai-slop": 1,
134
+ format: .3,
135
+ lint: .6,
136
+ "code-quality": .8,
137
+ "ai-slop": 2.5,
120
138
  architecture: 1,
121
- security: 2
139
+ security: 1.5
122
140
  };
123
141
  const EnginesSchema = z.object({
124
142
  format: z.boolean().default(true),
@@ -148,7 +166,7 @@ const ScoringSchema = z.object({
148
166
  good: 75,
149
167
  ok: 50
150
168
  })),
151
- smoothing: z.number().nonnegative().default(10)
169
+ smoothing: z.number().nonnegative().default(20)
152
170
  });
153
171
  const CiSchema = z.object({
154
172
  failBelow: z.number().default(0),
@@ -181,7 +199,7 @@ const AislopConfigSchema = z.object({
181
199
  good: 75,
182
200
  ok: 50
183
201
  },
184
- smoothing: 10
202
+ smoothing: 20
185
203
  })),
186
204
  ci: CiSchema.default(() => ({
187
205
  failBelow: 0,
@@ -872,6 +890,7 @@ const JS_EXTENSIONS = new Set([
872
890
  ".cjs"
873
891
  ]);
874
892
  const PY_EXTENSIONS = new Set([".py"]);
893
+ const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
875
894
  const extractJsImportedSymbols = (lines) => {
876
895
  const symbols = [];
877
896
  const importLines = /* @__PURE__ */ new Set();
@@ -980,32 +999,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
980
999
  }
981
1000
  return false;
982
1001
  };
1002
+ const analyzeFile = (filePath) => {
1003
+ if (isAutoGenerated(filePath)) return null;
1004
+ let content;
1005
+ try {
1006
+ content = fs.readFileSync(filePath, "utf-8");
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ const ext = path.extname(filePath);
1011
+ const lines = content.split("\n");
1012
+ let symbols;
1013
+ let importLines;
1014
+ if (JS_EXTENSIONS.has(ext)) {
1015
+ const result = extractJsImportedSymbols(lines);
1016
+ symbols = result.symbols;
1017
+ importLines = result.importLines;
1018
+ } else if (PY_EXTENSIONS.has(ext)) {
1019
+ const result = extractPyImportedSymbols(lines);
1020
+ symbols = result.symbols;
1021
+ importLines = result.importLines;
1022
+ } else return null;
1023
+ return {
1024
+ lines,
1025
+ symbols,
1026
+ importLines,
1027
+ ext
1028
+ };
1029
+ };
1030
+ const getUnusedSymbols = (lines, symbols, importLines) => {
1031
+ const content = lines.join("\n");
1032
+ return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
1033
+ };
983
1034
  const detectUnusedImports = async (context) => {
984
1035
  const files = getSourceFiles(context);
985
1036
  const diagnostics = [];
986
1037
  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);
1038
+ const analysis = analyzeFile(filePath);
1039
+ if (!analysis) continue;
995
1040
  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({
1041
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
1042
+ for (const symbol of unused) diagnostics.push({
1009
1043
  filePath: relativePath,
1010
1044
  engine: "ai-slop",
1011
1045
  rule: "ai-slop/unused-import",
@@ -1826,10 +1860,10 @@ const codeQualityEngine = {
1826
1860
 
1827
1861
  //#endregion
1828
1862
  //#region src/engines/format/biome.ts
1829
- const esmRequire$2 = createRequire(import.meta.url);
1863
+ const esmRequire$3 = createRequire(import.meta.url);
1830
1864
  const resolveLocalBiomeScript = () => {
1831
1865
  try {
1832
- const packageJsonPath = esmRequire$2.resolve("@biomejs/biome/package.json");
1866
+ const packageJsonPath = esmRequire$3.resolve("@biomejs/biome/package.json");
1833
1867
  return path.join(path.dirname(packageJsonPath), "bin", "biome");
1834
1868
  } catch {
1835
1869
  return null;
@@ -1922,14 +1956,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
1922
1956
  const fixBiomeFormat = async (context) => {
1923
1957
  const targets = getBiomeTargets(context);
1924
1958
  if (targets.length === 0) return;
1925
- const result = await runBiome([
1926
- "check",
1959
+ await runBiome([
1960
+ "format",
1927
1961
  "--write",
1928
- "--formatter-enabled=true",
1929
- "--linter-enabled=false",
1930
1962
  ...targets
1931
1963
  ], context.rootDirectory, 6e4);
1932
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
1933
1964
  };
1934
1965
 
1935
1966
  //#endregion
@@ -2079,7 +2110,7 @@ const fixGofmt = async (rootDirectory) => {
2079
2110
  //#endregion
2080
2111
  //#region src/utils/tooling.ts
2081
2112
  const THIS_FILE = fileURLToPath(import.meta.url);
2082
- const esmRequire$1 = createRequire(import.meta.url);
2113
+ const esmRequire$2 = createRequire(import.meta.url);
2083
2114
  const resolvePackageRoot = (startFile) => {
2084
2115
  let current = path.dirname(startFile);
2085
2116
  while (true) {
@@ -2110,7 +2141,7 @@ const isToolAvailable = async (toolName) => {
2110
2141
  };
2111
2142
  const isNodePackageAvailable = (packageName) => {
2112
2143
  try {
2113
- esmRequire$1.resolve(`${packageName}/package.json`);
2144
+ esmRequire$2.resolve(`${packageName}/package.json`);
2114
2145
  return true;
2115
2146
  } catch {
2116
2147
  return false;
@@ -2393,10 +2424,10 @@ const createOxlintConfig = (options) => {
2393
2424
 
2394
2425
  //#endregion
2395
2426
  //#region src/engines/lint/oxlint.ts
2396
- const esmRequire = createRequire(import.meta.url);
2427
+ const esmRequire$1 = createRequire(import.meta.url);
2397
2428
  const resolveOxlintBinary = () => {
2398
2429
  try {
2399
- const oxlintMainPath = esmRequire.resolve("oxlint");
2430
+ const oxlintMainPath = esmRequire$1.resolve("oxlint");
2400
2431
  const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
2401
2432
  return path.join(oxlintDir, "bin", "oxlint");
2402
2433
  } catch {
@@ -2431,6 +2462,144 @@ const detectTestFramework = (rootDir) => {
2431
2462
  } catch {}
2432
2463
  return null;
2433
2464
  };
2465
+ const extractUnusedVarName = (message) => {
2466
+ const variableMatch = message.match(/Variable '([^']+)' is declared but never used/);
2467
+ if (variableMatch?.[1]) return {
2468
+ name: variableMatch[1],
2469
+ type: "variable"
2470
+ };
2471
+ const paramMatch = message.match(/Parameter '([^']+)' is declared but never used/);
2472
+ if (paramMatch?.[1]) return {
2473
+ name: paramMatch[1],
2474
+ type: "parameter"
2475
+ };
2476
+ const catchMatch = message.match(/Catch parameter '([^']+)' is caught but never used/);
2477
+ if (catchMatch?.[1]) return {
2478
+ name: catchMatch[1],
2479
+ type: "parameter"
2480
+ };
2481
+ return null;
2482
+ };
2483
+ const collectUnusedVarCandidates = (diagnostics) => diagnostics.filter((d) => d.rule === "eslint/no-unused-vars").map((d) => {
2484
+ const extracted = extractUnusedVarName(d.message);
2485
+ if (!extracted || extracted.name.startsWith("_")) return null;
2486
+ return {
2487
+ filePath: d.filePath,
2488
+ line: d.line,
2489
+ column: d.column,
2490
+ name: extracted.name,
2491
+ type: extracted.type
2492
+ };
2493
+ }).filter((candidate) => candidate !== null);
2494
+ const prefixIdentifierOnLine = (line, name, column, type) => {
2495
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2496
+ if (type === "parameter") {
2497
+ const paramPattern = new RegExp(`\\b${escaped}\\b`);
2498
+ if (paramPattern.test(line)) return line.replace(paramPattern, `_${name}`);
2499
+ return line;
2500
+ }
2501
+ const assignPattern = new RegExp(`(\\s*)(const|let|var)\\s+${escaped}\\s*=\\s*(.+)$`);
2502
+ const assignMatch = line.match(assignPattern);
2503
+ if (assignMatch) {
2504
+ const indent = assignMatch[1];
2505
+ const expression = assignMatch[3];
2506
+ if (/await\s/.test(expression) || /\w+\s*\(/.test(expression)) return `${indent}${expression}`;
2507
+ return "";
2508
+ }
2509
+ const destructureMatch = line.match(/\{[^}]*\}/);
2510
+ if (destructureMatch) {
2511
+ const destructureContent = destructureMatch[0];
2512
+ const destructureStart = destructureMatch.index;
2513
+ if (new RegExp(`\\b${escaped}\\b`).test(destructureContent)) {
2514
+ let updated = destructureContent;
2515
+ const propPattern = new RegExp(`\\b${escaped}\\b\\s*,?`);
2516
+ updated = updated.replace(propPattern, (match) => {
2517
+ if (match.endsWith(",")) return "";
2518
+ return "";
2519
+ });
2520
+ updated = updated.replace(/,\s*\},/, "}");
2521
+ updated = updated.replace(/\{,\s*/, "{");
2522
+ updated = updated.replace(/\s*,\s*\}/, "}");
2523
+ if (updated !== destructureContent) return line.slice(0, destructureStart) + updated + line.slice(destructureStart + destructureContent.length);
2524
+ }
2525
+ }
2526
+ let bestStart = -1;
2527
+ let bestEnd = -1;
2528
+ let bestDistance = Number.POSITIVE_INFINITY;
2529
+ const target = Math.max(0, column - 1);
2530
+ for (const match of line.matchAll(new RegExp(`\\b${escaped}\\b`, "g"))) {
2531
+ if (match.index === void 0) continue;
2532
+ const start = match.index;
2533
+ const end = start + name.length;
2534
+ if (start > 0 && line[start - 1] === "_") continue;
2535
+ const distance = target >= start && target <= end ? 0 : Math.abs(start - target);
2536
+ if (distance < bestDistance) {
2537
+ bestDistance = distance;
2538
+ bestStart = start;
2539
+ bestEnd = end;
2540
+ }
2541
+ }
2542
+ if (bestStart < 0 || bestEnd < 0) return line;
2543
+ return `${line.slice(0, bestStart)}_${name}${line.slice(bestEnd)}`;
2544
+ };
2545
+ const applyUnusedVarPrefixFixes = (rootDirectory, candidates) => {
2546
+ const byFile = /* @__PURE__ */ new Map();
2547
+ for (const candidate of candidates) {
2548
+ const absolute = path.isAbsolute(candidate.filePath) ? candidate.filePath : path.join(rootDirectory, candidate.filePath);
2549
+ const entries = byFile.get(absolute) ?? [];
2550
+ entries.push(candidate);
2551
+ byFile.set(absolute, entries);
2552
+ }
2553
+ for (const [filePath, fileCandidates] of byFile.entries()) {
2554
+ if (!fs.existsSync(filePath)) continue;
2555
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
2556
+ const ordered = [...fileCandidates].sort((a, b) => {
2557
+ if (a.line !== b.line) return a.line - b.line;
2558
+ return a.column - b.column;
2559
+ });
2560
+ let changed = false;
2561
+ for (const candidate of ordered) {
2562
+ const lineIndex = candidate.line - 1;
2563
+ if (lineIndex < 0 || lineIndex >= lines.length) continue;
2564
+ const current = lines[lineIndex];
2565
+ const updated = prefixIdentifierOnLine(current, candidate.name, candidate.column, candidate.type);
2566
+ if (updated !== current) {
2567
+ lines[lineIndex] = updated;
2568
+ changed = true;
2569
+ }
2570
+ }
2571
+ if (changed) fs.writeFileSync(filePath, lines.join("\n"));
2572
+ }
2573
+ };
2574
+ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
2575
+ const byFile = /* @__PURE__ */ new Map();
2576
+ for (const d of diagnostics) {
2577
+ const keyMatch = d.message.match(/Duplicate key '([^']+)'/);
2578
+ if (!keyMatch) continue;
2579
+ const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(rootDirectory, d.filePath);
2580
+ const entries = byFile.get(absolute) ?? [];
2581
+ entries.push({
2582
+ key: keyMatch[1],
2583
+ line: d.line
2584
+ });
2585
+ byFile.set(absolute, entries);
2586
+ }
2587
+ for (const [filePath, dupes] of byFile) {
2588
+ if (!fs.existsSync(filePath)) continue;
2589
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
2590
+ const toRemove = /* @__PURE__ */ new Set();
2591
+ for (const { key } of dupes) {
2592
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2593
+ const keyPattern = new RegExp(`^\\s*['"]?${escaped}['"]?\\s*:|^\\s*${escaped}\\s*:`);
2594
+ const matches = [];
2595
+ for (let i = 0; i < lines.length; i++) if (keyPattern.test(lines[i])) matches.push(i);
2596
+ for (let j = 1; j < matches.length; j++) toRemove.add(matches[j]);
2597
+ }
2598
+ if (toRemove.size === 0) continue;
2599
+ const filtered = lines.filter((_, i) => !toRemove.has(i));
2600
+ fs.writeFileSync(filePath, filtered.join("\n"));
2601
+ }
2602
+ };
2434
2603
  const runOxlint = async (context) => {
2435
2604
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
2436
2605
  const config = createOxlintConfig({
@@ -2492,6 +2661,8 @@ const fixOxlint = async (context) => {
2492
2661
  "-c",
2493
2662
  configPath,
2494
2663
  "--fix",
2664
+ "--fix-suggestions",
2665
+ "--fix-dangerously",
2495
2666
  "."
2496
2667
  ];
2497
2668
  const result = await runSubprocess(process.execPath, args, {
@@ -2499,10 +2670,18 @@ const fixOxlint = async (context) => {
2499
2670
  timeout: 12e4
2500
2671
  });
2501
2672
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Oxlint exited with code ${result.exitCode}`);
2673
+ const remaining = await runOxlint(context);
2674
+ const candidates = collectUnusedVarCandidates(remaining);
2675
+ if (candidates.length > 0) applyUnusedVarPrefixFixes(context.rootDirectory, candidates);
2676
+ const duplicateKeys = remaining.filter((d) => d.message.startsWith("Duplicate key"));
2677
+ if (duplicateKeys.length > 0) removeDuplicateKeyLines(context.rootDirectory, duplicateKeys);
2502
2678
  } finally {
2503
2679
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
2504
2680
  }
2505
2681
  };
2682
+ const fixOxlintForce = async (context) => {
2683
+ return fixOxlint(context);
2684
+ };
2506
2685
 
2507
2686
  //#endregion
2508
2687
  //#region src/engines/lint/ruff.ts
@@ -2545,6 +2724,18 @@ const fixRuffLint = async (rootDirectory) => {
2545
2724
  });
2546
2725
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
2547
2726
  };
2727
+ const fixRuffLintForce = async (rootDirectory) => {
2728
+ const result = await runSubprocess(resolveToolBinary("ruff"), [
2729
+ "check",
2730
+ "--fix",
2731
+ "--unsafe-fixes",
2732
+ rootDirectory
2733
+ ], {
2734
+ cwd: rootDirectory,
2735
+ timeout: 6e4
2736
+ });
2737
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
2738
+ };
2548
2739
 
2549
2740
  //#endregion
2550
2741
  //#region src/engines/lint/index.ts
@@ -2555,7 +2746,7 @@ const lintEngine = {
2555
2746
  const { languages, installedTools } = context;
2556
2747
  const promises = [];
2557
2748
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
2558
- if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-vDz4kh9-.js").then((mod) => mod.runExpoDoctor(context)));
2749
+ if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
2559
2750
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
2560
2751
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
2561
2752
  if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
@@ -3180,14 +3371,13 @@ const logger = {
3180
3371
  * Application version — injected at build time by tsdown from package.json.
3181
3372
  * The fallback should always match the "version" field in package.json.
3182
3373
  */
3183
- const APP_VERSION = "0.2.0";
3374
+ const APP_VERSION = "0.3.0";
3184
3375
 
3185
3376
  //#endregion
3186
3377
  //#region src/output/layout.ts
3187
- const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3378
+ const formatElapsed$2 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3188
3379
  const printCommandHeader = (commandName) => {
3189
- logger.log(highlighter.bold(`aislop ${commandName}`));
3190
- logger.log(highlighter.dim(`v${APP_VERSION}`));
3380
+ logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
3191
3381
  logger.break();
3192
3382
  };
3193
3383
  const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
@@ -3198,84 +3388,9 @@ const printProjectMetadata = (project) => {
3198
3388
  logger.break();
3199
3389
  };
3200
3390
 
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
3391
  //#endregion
3277
3392
  //#region src/output/scan-progress.ts
3278
- const SPINNER_FRAMES = [
3393
+ const SPINNER_FRAMES$1 = [
3279
3394
  "⠋",
3280
3395
  "⠙",
3281
3396
  "⠹",
@@ -3288,20 +3403,20 @@ const SPINNER_FRAMES = [
3288
3403
  "⠏"
3289
3404
  ];
3290
3405
  const shouldRenderLiveScanProgress = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
3291
- const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3406
+ const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3292
3407
  const truncateText = (text, maxLength = 52) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
3293
3408
  const getIssueSummary = (result) => {
3294
3409
  const errors = result.diagnostics.filter((d) => d.severity === "error").length;
3295
3410
  const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
3296
- if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed(result.elapsed)})`;
3411
+ if (errors === 0 && warnings === 0) return `Done (0 issues, ${formatElapsed$1(result.elapsed)})`;
3297
3412
  const parts = [];
3298
3413
  if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
3299
3414
  if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
3300
- return `Done (${parts.join(", ")}, ${formatElapsed(result.elapsed)})`;
3415
+ return `Done (${parts.join(", ")}, ${formatElapsed$1(result.elapsed)})`;
3301
3416
  };
3302
- const getStatusParts = (state, frameIndex) => {
3417
+ const getStatusParts$1 = (state, frameIndex) => {
3303
3418
  if (state.status === "running") return {
3304
- icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
3419
+ icon: highlighter.info(SPINNER_FRAMES$1[frameIndex % SPINNER_FRAMES$1.length]),
3305
3420
  detail: highlighter.info("Running")
3306
3421
  };
3307
3422
  if (state.status === "done") {
@@ -3340,11 +3455,11 @@ const renderScanProgressBlock = (states, frameIndex) => {
3340
3455
  const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
3341
3456
  return `${[` ${highlighter.bold(`Engines ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
3342
3457
  const label = getEngineLabel(state.engine).padEnd(labelWidth, " ");
3343
- const { icon, detail } = getStatusParts(state, frameIndex);
3458
+ const { icon, detail } = getStatusParts$1(state, frameIndex);
3344
3459
  return ` ${icon} ${label} ${detail}`;
3345
3460
  })].join("\n")}\n`;
3346
3461
  };
3347
- const clearRenderedLines$1 = (lineCount) => {
3462
+ const clearRenderedLines$2 = (lineCount) => {
3348
3463
  if (lineCount === 0) return;
3349
3464
  process.stderr.write(`\u001B[${lineCount}F`);
3350
3465
  for (let index = 0; index < lineCount; index += 1) {
@@ -3396,7 +3511,7 @@ var ScanProgressRenderer = class {
3396
3511
  }
3397
3512
  render() {
3398
3513
  if (!shouldRenderLiveScanProgress()) return;
3399
- if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
3514
+ if (this.previousLineCount > 0) clearRenderedLines$2(this.previousLineCount);
3400
3515
  const output = renderScanProgressBlock(this.states, this.frameIndex);
3401
3516
  process.stderr.write(output);
3402
3517
  this.previousLineCount = output.split("\n").length - 1;
@@ -3882,12 +3997,13 @@ const scanCommand = async (directory, config, options) => {
3882
3997
  console.log(JSON.stringify(jsonOut, null, 2));
3883
3998
  return { exitCode };
3884
3999
  }
3885
- await printMaybePaged([
4000
+ const output = [
3886
4001
  "",
3887
4002
  allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
3888
4003
  renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
3889
4004
  ""
3890
- ].join("\n"));
4005
+ ].join("\n");
4006
+ process.stdout.write(output);
3891
4007
  return { exitCode };
3892
4008
  };
3893
4009
 
@@ -4038,7 +4154,429 @@ const doctorCommand = async (directory) => {
4038
4154
  };
4039
4155
 
4040
4156
  //#endregion
4041
- //#region src/commands/fix.ts
4157
+ //#region src/engines/ai-slop/dead-patterns-fix.ts
4158
+ /**
4159
+ * Removes lines flagged as fixable by the trivial-comment and dead-pattern detectors.
4160
+ * Specifically handles:
4161
+ * - ai-slop/trivial-comment (trivial comments that restate the code)
4162
+ * - ai-slop/console-leftover (console.log/debug/info left in production)
4163
+ */
4164
+ const fixDeadPatterns = async (context) => {
4165
+ const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
4166
+ if (fixable.length === 0) return;
4167
+ const byFile = /* @__PURE__ */ new Map();
4168
+ for (const d of fixable) {
4169
+ const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(context.rootDirectory, d.filePath);
4170
+ const lines = byFile.get(absolute) ?? /* @__PURE__ */ new Set();
4171
+ lines.add(d.line);
4172
+ byFile.set(absolute, lines);
4173
+ }
4174
+ for (const [filePath, lineNumbers] of byFile) {
4175
+ if (!fs.existsSync(filePath)) continue;
4176
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
4177
+ const filtered = [];
4178
+ for (let i = 0; i < lines.length; i++) {
4179
+ const lineNo = i + 1;
4180
+ if (lineNumbers.has(lineNo)) continue;
4181
+ filtered.push(lines[i]);
4182
+ }
4183
+ const collapsed = [];
4184
+ for (const line of filtered) {
4185
+ if (line.trim() === "" && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "") continue;
4186
+ collapsed.push(line);
4187
+ }
4188
+ fs.writeFileSync(filePath, collapsed.join("\n"));
4189
+ }
4190
+ };
4191
+
4192
+ //#endregion
4193
+ //#region src/engines/ai-slop/unused-imports-fix.ts
4194
+ const fixUnusedImports = async (context) => {
4195
+ const files = getSourceFiles(context);
4196
+ for (const filePath of files) {
4197
+ const analysis = analyzeFile(filePath);
4198
+ if (!analysis) continue;
4199
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
4200
+ if (unused.length === 0) continue;
4201
+ const unusedNames = new Set(unused.map((u) => u.name));
4202
+ const lines = [...analysis.lines];
4203
+ const symbolsByLine = /* @__PURE__ */ new Map();
4204
+ for (const sym of analysis.symbols) {
4205
+ const arr = symbolsByLine.get(sym.line) ?? [];
4206
+ arr.push(sym);
4207
+ symbolsByLine.set(sym.line, arr);
4208
+ }
4209
+ const linesToRemove = /* @__PURE__ */ new Set();
4210
+ for (const [lineNo, syms] of symbolsByLine) {
4211
+ const lineIdx = lineNo - 1;
4212
+ const allUnused = syms.every((s) => unusedNames.has(s.name));
4213
+ const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
4214
+ if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
4215
+ else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
4216
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
4217
+ }
4218
+ if (linesToRemove.size === 0 && unused.length === 0) continue;
4219
+ const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
4220
+ for (const idx of sortedRemove) lines.splice(idx, 1);
4221
+ const filtered = lines.filter((l) => l !== REMOVE_MARKER);
4222
+ while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
4223
+ fs.writeFileSync(filePath, filtered.join("\n"));
4224
+ }
4225
+ };
4226
+ const getJsImportSpan = (lines, startIdx) => {
4227
+ const span = [startIdx];
4228
+ let fullImport = lines[startIdx]?.trim() ?? "";
4229
+ if (!fullImport.startsWith("import ")) return span;
4230
+ let idx = startIdx + 1;
4231
+ while (!fullImport.includes("from") && idx < lines.length) {
4232
+ span.push(idx);
4233
+ fullImport += ` ${lines[idx].trim()}`;
4234
+ idx++;
4235
+ }
4236
+ return span;
4237
+ };
4238
+ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4239
+ const fullImport = span.map((i) => lines[i]).join("\n");
4240
+ const namedMatch = fullImport.match(/\{([^}]+)\}/s);
4241
+ if (!namedMatch) return;
4242
+ const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
4243
+ const defaultUnused = syms.some((s) => s.isDefault && unusedNames.has(s.name));
4244
+ if (unusedNamed.length === 0 && !defaultUnused) return;
4245
+ const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
4246
+ const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
4247
+ const parts = spec.split(/\s+as\s+/);
4248
+ const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
4249
+ return !unusedNamedSet.has(localName);
4250
+ });
4251
+ const fromMatch = fullImport.match(/from\s+["']([^"']+)["'];?/);
4252
+ const fromClause = fromMatch ? `from "${fromMatch[1]}"` : "";
4253
+ if (keptSpecifiers.length === 0) {
4254
+ const usedDefault = syms.find((s) => s.isDefault && !unusedNames.has(s.name));
4255
+ if (usedDefault) {
4256
+ const defaultMatch = fullImport.match(/^import\s+(\w+)/);
4257
+ const defaultName = defaultMatch ? defaultMatch[1] : usedDefault.name;
4258
+ lines[span[0]] = `import ${defaultName} ${fromClause};`;
4259
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4260
+ } else for (const idx of span) lines[idx] = REMOVE_MARKER;
4261
+ return;
4262
+ }
4263
+ if (defaultUnused) {
4264
+ lines[span[0]] = `import { ${keptSpecifiers.join(", ")} } ${fromClause};`;
4265
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4266
+ return;
4267
+ }
4268
+ const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
4269
+ const prefix = importPrefix ? importPrefix[1] : "import ";
4270
+ const wasMultiLine = span.length > 1;
4271
+ let newImport;
4272
+ if (wasMultiLine && keptSpecifiers.length > 2) {
4273
+ const indentMatch = lines[span[1]]?.match(/^(\s+)/);
4274
+ const indent = indentMatch ? indentMatch[1] : " ";
4275
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause};`;
4276
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause};`;
4277
+ lines[span[0]] = newImport;
4278
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4279
+ };
4280
+ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
4281
+ const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
4282
+ if (!fromMatch) return;
4283
+ const prefix = fromMatch[1];
4284
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
4285
+ const hasParen = importPart.startsWith("(");
4286
+ const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
4287
+ const parts = spec.split(/\s+as\s+/);
4288
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
4289
+ return !unusedNames.has(localName);
4290
+ });
4291
+ if (keptSpecifiers.length === 0) return;
4292
+ const joined = keptSpecifiers.join(", ");
4293
+ lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
4294
+ };
4295
+
4296
+ //#endregion
4297
+ //#region src/engines/lint/expo-doctor.ts
4298
+ var expo_doctor_exports = /* @__PURE__ */ __exportAll({ runExpoDoctor: () => runExpoDoctor });
4299
+ const esmRequire = createRequire(import.meta.url);
4300
+ const ISSUE_PREFIX = "✖ ";
4301
+ const resolveExpoDoctorScript = () => {
4302
+ try {
4303
+ const packageJsonPath = esmRequire.resolve("expo-doctor/package.json");
4304
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
4305
+ const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["expo-doctor"];
4306
+ if (!binRelativePath) return null;
4307
+ return path.join(path.dirname(packageJsonPath), binRelativePath);
4308
+ } catch {
4309
+ return null;
4310
+ }
4311
+ };
4312
+ const toRuleSuffix = (title) => {
4313
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4314
+ return slug.length > 0 ? slug : "issue";
4315
+ };
4316
+ const parseIssues = (output) => {
4317
+ const lines = output.split("\n").map((line) => line.trimEnd());
4318
+ const startIndex = lines.findIndex((line) => line.includes("Possible issues detected:"));
4319
+ if (startIndex < 0) return [];
4320
+ const issues = [];
4321
+ let current = null;
4322
+ let inAdvice = false;
4323
+ for (let i = startIndex + 1; i < lines.length; i += 1) {
4324
+ const line = lines[i].trim();
4325
+ if (/^\d+\s+checks failed/.test(line)) break;
4326
+ if (line.length === 0) continue;
4327
+ if (line.startsWith(ISSUE_PREFIX)) {
4328
+ if (current) issues.push(current);
4329
+ current = {
4330
+ title: line.slice(2).trim(),
4331
+ details: [],
4332
+ advice: []
4333
+ };
4334
+ inAdvice = false;
4335
+ continue;
4336
+ }
4337
+ if (!current) continue;
4338
+ if (line === "Advice:") {
4339
+ inAdvice = true;
4340
+ continue;
4341
+ }
4342
+ if (inAdvice) current.advice.push(line);
4343
+ else current.details.push(line);
4344
+ }
4345
+ if (current) issues.push(current);
4346
+ return issues;
4347
+ };
4348
+ const parseConfigError = (output) => {
4349
+ const line = output.split("\n").find((candidate) => candidate.trim().startsWith("ConfigError:"));
4350
+ return line ? line.trim() : null;
4351
+ };
4352
+ const toDiagnostics = (issues) => issues.map((issue) => {
4353
+ const helpParts = [issue.details.join(" ").trim(), issue.advice.join(" ").trim()].filter((part) => part.length > 0);
4354
+ return {
4355
+ filePath: "package.json",
4356
+ engine: "lint",
4357
+ rule: `expo-doctor/${toRuleSuffix(issue.title)}`,
4358
+ severity: "warning",
4359
+ message: `Expo Doctor: ${issue.title}`,
4360
+ help: helpParts.join(" "),
4361
+ line: 0,
4362
+ column: 0,
4363
+ category: "Expo",
4364
+ fixable: false
4365
+ };
4366
+ });
4367
+ const runExpoDoctor = async (context) => {
4368
+ const scriptPath = resolveExpoDoctorScript();
4369
+ let stdout = "";
4370
+ let stderr = "";
4371
+ try {
4372
+ if (scriptPath) {
4373
+ const result = await runSubprocess(process.execPath, [
4374
+ scriptPath,
4375
+ context.rootDirectory,
4376
+ "--verbose"
4377
+ ], {
4378
+ cwd: context.rootDirectory,
4379
+ timeout: 12e4
4380
+ });
4381
+ stdout = result.stdout;
4382
+ stderr = result.stderr;
4383
+ } else {
4384
+ const result = await runSubprocess("npx", [
4385
+ "--yes",
4386
+ "expo-doctor",
4387
+ context.rootDirectory,
4388
+ "--verbose"
4389
+ ], {
4390
+ cwd: context.rootDirectory,
4391
+ timeout: 12e4
4392
+ });
4393
+ stdout = result.stdout;
4394
+ stderr = result.stderr;
4395
+ }
4396
+ } catch {
4397
+ return [];
4398
+ }
4399
+ const output = [stdout, stderr].filter(Boolean).join("\n");
4400
+ if (!output) return [];
4401
+ const configError = parseConfigError(output);
4402
+ if (configError) return [{
4403
+ filePath: "package.json",
4404
+ engine: "lint",
4405
+ rule: "expo-doctor/config-error",
4406
+ severity: "warning",
4407
+ message: configError,
4408
+ help: "Install project dependencies, then re-run `aislop scan`.",
4409
+ line: 0,
4410
+ column: 0,
4411
+ category: "Expo",
4412
+ fixable: false
4413
+ }];
4414
+ return toDiagnostics(parseIssues(output));
4415
+ };
4416
+
4417
+ //#endregion
4418
+ //#region src/output/fix-progress.ts
4419
+ const SPINNER_FRAMES = [
4420
+ "⠋",
4421
+ "⠙",
4422
+ "⠹",
4423
+ "⠸",
4424
+ "⠼",
4425
+ "⠴",
4426
+ "⠦",
4427
+ "⠧",
4428
+ "⠇",
4429
+ "⠏"
4430
+ ];
4431
+ const shouldRenderLive = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
4432
+ const formatElapsed = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
4433
+ const getStepSummary = (result) => {
4434
+ if (result.failed) return `Failed (${result.afterIssues} remain)`;
4435
+ if (result.beforeIssues === 0) return `0 issues, ${formatElapsed(result.elapsedMs)}`;
4436
+ if (result.afterIssues === 0) return `${result.resolvedIssues} resolved, ${formatElapsed(result.elapsedMs)}`;
4437
+ if (result.resolvedIssues > 0) return `${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${formatElapsed(result.elapsedMs)}`;
4438
+ return `no changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${formatElapsed(result.elapsedMs)}`;
4439
+ };
4440
+ const getStatusParts = (state, frameIndex) => {
4441
+ if (state.status === "running") return {
4442
+ icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
4443
+ detail: highlighter.info("Running")
4444
+ };
4445
+ if (state.status === "done" && state.result) return {
4446
+ icon: state.result.afterIssues > 0 ? highlighter.warn("!") : highlighter.success("✓"),
4447
+ detail: highlighter.dim(`Done (${getStepSummary(state.result)})`)
4448
+ };
4449
+ if (state.status === "failed" && state.result) return {
4450
+ icon: highlighter.error("✗"),
4451
+ detail: highlighter.dim(`Failed (${getStepSummary(state.result)})`)
4452
+ };
4453
+ return {
4454
+ icon: highlighter.dim("○"),
4455
+ detail: highlighter.dim("Waiting")
4456
+ };
4457
+ };
4458
+ const clearRenderedLines$1 = (lineCount) => {
4459
+ if (lineCount === 0) return;
4460
+ process.stderr.write(`\u001B[${lineCount}F`);
4461
+ for (let index = 0; index < lineCount; index += 1) {
4462
+ process.stderr.write("\x1B[2K");
4463
+ if (index < lineCount - 1) process.stderr.write("\x1B[1E");
4464
+ }
4465
+ if (lineCount > 1) process.stderr.write(`\u001B[${lineCount - 1}F`);
4466
+ };
4467
+ const renderFixProgressBlock = (states, frameIndex) => {
4468
+ if (states.length === 0) return ` ${highlighter.bold("Fixes 0/0")} ${highlighter.dim("nothing to run")}\n`;
4469
+ const completedCount = states.filter((s) => s.status === "done" || s.status === "failed").length;
4470
+ const runningCount = states.filter((s) => s.status === "running").length;
4471
+ const labelWidth = Math.max(...states.map((s) => s.name.length));
4472
+ const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
4473
+ return `${[` ${highlighter.bold(`Fixes ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
4474
+ const label = state.name.padEnd(labelWidth, " ");
4475
+ const { icon, detail } = getStatusParts(state, frameIndex);
4476
+ return ` ${icon} ${label} ${detail}`;
4477
+ })].join("\n")}\n`;
4478
+ };
4479
+ var FixProgressRenderer = class {
4480
+ states;
4481
+ previousLineCount = 0;
4482
+ frameIndex = 0;
4483
+ timer;
4484
+ live;
4485
+ constructor(stepNames) {
4486
+ this.states = stepNames.map((name) => ({
4487
+ name,
4488
+ status: "pending"
4489
+ }));
4490
+ this.live = shouldRenderLive();
4491
+ }
4492
+ isLive() {
4493
+ return this.live;
4494
+ }
4495
+ start() {
4496
+ if (!this.live) return;
4497
+ this.render();
4498
+ this.timer = setInterval(() => {
4499
+ this.frameIndex += 1;
4500
+ this.render();
4501
+ }, 100);
4502
+ this.timer.unref();
4503
+ }
4504
+ markStarted(name) {
4505
+ const state = this.states.find((s) => s.name === name);
4506
+ if (!state) return;
4507
+ state.status = "running";
4508
+ this.render();
4509
+ }
4510
+ markComplete(name, result) {
4511
+ const state = this.states.find((s) => s.name === name);
4512
+ if (!state) return;
4513
+ state.status = result.failed ? "failed" : "done";
4514
+ state.result = result;
4515
+ this.render();
4516
+ }
4517
+ stop() {
4518
+ if (this.timer) {
4519
+ clearInterval(this.timer);
4520
+ this.timer = void 0;
4521
+ }
4522
+ if (!this.live) return;
4523
+ this.render();
4524
+ }
4525
+ render() {
4526
+ if (!this.live) return;
4527
+ if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
4528
+ const output = renderFixProgressBlock(this.states, this.frameIndex);
4529
+ process.stderr.write(output);
4530
+ this.previousLineCount = output.split("\n").length - 1;
4531
+ }
4532
+ };
4533
+
4534
+ //#endregion
4535
+ //#region src/commands/fix-force.ts
4536
+ const getJsAuditFixCommand = (rootDirectory) => {
4537
+ if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return {
4538
+ command: "pnpm",
4539
+ args: ["audit", "--fix"]
4540
+ };
4541
+ if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
4542
+ command: "npm",
4543
+ args: ["audit", "fix"]
4544
+ };
4545
+ return null;
4546
+ };
4547
+ const fixDependencyAudit = async (context) => {
4548
+ const auditFix = getJsAuditFixCommand(context.rootDirectory);
4549
+ if (!auditFix) return;
4550
+ const result = await runSubprocess(auditFix.command, auditFix.args, {
4551
+ cwd: context.rootDirectory,
4552
+ timeout: 18e4
4553
+ });
4554
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `${auditFix.command} audit fix failed`);
4555
+ };
4556
+ const fixExpoDependencies = async (context) => {
4557
+ if ((await runSubprocess("npx", [
4558
+ "--yes",
4559
+ "expo",
4560
+ "install",
4561
+ "--fix"
4562
+ ], {
4563
+ cwd: context.rootDirectory,
4564
+ timeout: 18e4
4565
+ })).exitCode === 0) return;
4566
+ const checkResult = await runSubprocess("npx", [
4567
+ "--yes",
4568
+ "expo",
4569
+ "install",
4570
+ "--check"
4571
+ ], {
4572
+ cwd: context.rootDirectory,
4573
+ timeout: 18e4
4574
+ });
4575
+ if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
4576
+ };
4577
+
4578
+ //#endregion
4579
+ //#region src/commands/fix-step.ts
4042
4580
  const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
4043
4581
  const uniqueFileCount = (diagnostics) => uniqueFiles(diagnostics).length;
4044
4582
  const getFilePreviewLines = (title, files, verbose) => {
@@ -4062,7 +4600,8 @@ const getStepStatusLine = (result, name, elapsedLabel) => {
4062
4600
  if (result.resolvedIssues > 0) return highlighter.warn(` ! ${name}: done (${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${elapsedLabel})`);
4063
4601
  return highlighter.warn(` ! ${name}: done (no auto-fix changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${elapsedLabel})`);
4064
4602
  };
4065
- const runFixStep = async (name, detect, applyFix, options) => {
4603
+ const runFixStep = async (name, detect, applyFix, options, progress) => {
4604
+ progress.markStarted(name);
4066
4605
  const stepStart = performance.now();
4067
4606
  const before = await detect();
4068
4607
  let applyError = null;
@@ -4082,28 +4621,21 @@ const runFixStep = async (name, detect, applyFix, options) => {
4082
4621
  failed: applyError !== null && before.length === after.length,
4083
4622
  elapsedMs
4084
4623
  };
4085
- const lines = [getStepStatusLine(result, name, formatElapsed$1(result.elapsedMs))];
4086
- if (applyError) {
4087
- const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
4088
- const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
4089
- for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
4090
- if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
4091
- }
4092
- lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4093
- if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4094
- await printMaybePaged(`${lines.join("\n")}\n\n`);
4624
+ progress.markComplete(name, result);
4625
+ if (!progress.isLive()) {
4626
+ const lines = [getStepStatusLine(result, name, formatElapsed$2(result.elapsedMs))];
4627
+ if (applyError) {
4628
+ const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
4629
+ const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
4630
+ for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
4631
+ if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
4632
+ }
4633
+ lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4634
+ if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4635
+ process.stdout.write(`${lines.join("\n")}\n\n`);
4636
+ }
4095
4637
  return result;
4096
4638
  };
4097
- const createEngineContext = (rootDirectory, projectInfo, config) => ({
4098
- rootDirectory,
4099
- languages: projectInfo.languages,
4100
- frameworks: projectInfo.frameworks,
4101
- installedTools: projectInfo.installedTools,
4102
- config: {
4103
- quality: config.quality,
4104
- security: config.security
4105
- }
4106
- });
4107
4639
  const summarizeFixRun = (steps) => {
4108
4640
  const totals = steps.reduce((acc, step) => {
4109
4641
  acc.beforeIssues += step.beforeIssues;
@@ -4123,6 +4655,19 @@ const summarizeFixRun = (steps) => {
4123
4655
  } else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
4124
4656
  if (totals.failedSteps === 0 && totals.beforeIssues > 0 && totals.resolvedIssues === 0) logger.dim(" No auto-fixable changes were applied. Current findings are likely manual-fix categories.");
4125
4657
  };
4658
+
4659
+ //#endregion
4660
+ //#region src/commands/fix.ts
4661
+ const createEngineContext = (rootDirectory, projectInfo, config) => ({
4662
+ rootDirectory,
4663
+ languages: projectInfo.languages,
4664
+ frameworks: projectInfo.frameworks,
4665
+ installedTools: projectInfo.installedTools,
4666
+ config: {
4667
+ quality: config.quality,
4668
+ security: config.security
4669
+ }
4670
+ });
4126
4671
  const fixCommand = async (directory, config, options = {
4127
4672
  verbose: false,
4128
4673
  showHeader: true
@@ -4134,37 +4679,98 @@ const fixCommand = async (directory, config, options = {
4134
4679
  printProjectMetadata(projectInfo);
4135
4680
  const context = createEngineContext(resolvedDir, projectInfo, config);
4136
4681
  const steps = [];
4682
+ const stepNames = [];
4683
+ if (config.engines["ai-slop"]) {
4684
+ stepNames.push("Unused imports");
4685
+ stepNames.push("Dead code & comments");
4686
+ }
4687
+ if (config.engines.lint) {
4688
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS lint fixes");
4689
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python lint fixes");
4690
+ }
4691
+ if (config.engines["code-quality"]) {
4692
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("Unused dependencies");
4693
+ }
4137
4694
  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.");
4695
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS formatting");
4696
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python formatting");
4697
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) stepNames.push("Go formatting");
4698
+ }
4699
+ if (options.force) {
4700
+ if (config.engines.security) stepNames.push("Dependency audit fixes");
4701
+ if (projectInfo.frameworks.includes("expo")) stepNames.push("Expo dependency alignment");
4702
+ }
4703
+ const progress = new FixProgressRenderer(stepNames);
4704
+ progress.start();
4705
+ if (config.engines["ai-slop"]) {
4706
+ steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options, progress));
4707
+ const detectFixableSlop = async () => {
4708
+ const [comments, dead] = await Promise.all([detectTrivialComments(context), detectDeadPatterns(context)]);
4709
+ return [...comments, ...dead].filter((d) => d.fixable);
4710
+ };
4711
+ steps.push(await runFixStep("Dead code & comments", detectFixableSlop, () => fixDeadPatterns(context), options, progress));
4143
4712
  }
4144
4713
  if (config.engines.lint) {
4145
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4146
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
4714
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => options.force ? fixOxlintForce(context) : fixOxlint(context), options, progress));
4715
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => options.force ? fixRuffLintForce(resolvedDir) : fixRuffLint(resolvedDir), options, progress));
4147
4716
  else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
4148
4717
  }
4149
4718
  if (config.engines["code-quality"]) {
4150
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
4719
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options, progress));
4720
+ }
4721
+ if (config.engines.format) {
4722
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options, progress));
4723
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options, progress));
4724
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python formatting fixes.");
4725
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options, progress));
4726
+ else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4151
4727
  }
4728
+ if (options.force) {
4729
+ if (config.engines.security) steps.push(await runFixStep("Dependency audit fixes", () => runDependencyAudit(context), () => fixDependencyAudit(context), options, progress));
4730
+ if (projectInfo.frameworks.includes("expo")) steps.push(await runFixStep("Expo dependency alignment", () => runExpoDoctor(context), () => fixExpoDependencies(context), options, progress));
4731
+ }
4732
+ progress.stop();
4733
+ const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
4152
4734
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
4153
4735
  else {
4154
4736
  logger.break();
4155
4737
  summarizeFixRun(steps);
4156
4738
  }
4157
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
4158
- const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
4159
- trackEvent({
4160
- command: "fix",
4161
- languages: projectInfo.languages,
4162
- fixSteps: steps.length,
4163
- fixResolved: totalResolved
4164
- });
4165
- }
4739
+ if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
4740
+ command: "fix",
4741
+ languages: projectInfo.languages,
4742
+ fixSteps: steps.length,
4743
+ fixResolved: totalResolved
4744
+ });
4166
4745
  logger.break();
4167
- logger.success(" ✓ Done. Run `aislop scan` to verify.");
4746
+ const configDir = findConfigDir(resolvedDir);
4747
+ const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
4748
+ const engineConfig = {
4749
+ quality: config.quality,
4750
+ security: config.security,
4751
+ architectureRulesPath: config.engines.architecture ? rulesPath : void 0
4752
+ };
4753
+ const allDiagnostics = (await runEngines({
4754
+ rootDirectory: resolvedDir,
4755
+ languages: projectInfo.languages,
4756
+ frameworks: projectInfo.frameworks,
4757
+ installedTools: projectInfo.installedTools,
4758
+ config: engineConfig
4759
+ }, config.engines, () => {}, () => {})).flatMap((r) => r.diagnostics);
4760
+ const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
4761
+ const errors = allDiagnostics.filter((d) => d.severity === "error").length;
4762
+ const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
4763
+ const fixable = allDiagnostics.filter((d) => d.fixable).length;
4764
+ const manual = errors + warnings - fixable;
4765
+ const scoreColor = scoreResult.score >= config.scoring.thresholds.good ? highlighter.success : scoreResult.score >= config.scoring.thresholds.ok ? highlighter.warn : highlighter.error;
4766
+ logger.log(highlighter.dim("------------------------------------------------------------"));
4767
+ logger.log(highlighter.bold("Result"));
4768
+ logger.log(` Score: ${scoreColor(`${scoreResult.score}/100`)} ${scoreColor(`(${scoreResult.label})`)}`);
4769
+ logger.log(` Resolved: ${highlighter.success(String(totalResolved))} issue${totalResolved === 1 ? "" : "s"}`);
4770
+ logger.log(` Remaining: ${errors + warnings > 0 ? highlighter.warn(String(errors + warnings)) : highlighter.success("0")} (${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"})`);
4771
+ if (fixable > 0) logger.log(` Auto-fixable: ${highlighter.info(String(fixable))}`);
4772
+ if (manual > 0) logger.log(` Manual effort: ${highlighter.dim(String(manual))}`);
4773
+ logger.log(highlighter.dim("------------------------------------------------------------"));
4168
4774
  logger.break();
4169
4775
  };
4170
4776
 
@@ -4289,7 +4895,7 @@ const rulesCommand = async (directory) => {
4289
4895
  lines.push("");
4290
4896
  }
4291
4897
  }
4292
- await printMaybePaged(`${lines.join("\n")}\n`);
4898
+ process.stdout.write(`${lines.join("\n")}\n`);
4293
4899
  };
4294
4900
 
4295
4901
  //#endregion
@@ -4525,7 +5131,7 @@ const program = new Command().name("aislop").description("The unified code quali
4525
5131
  }).addHelpText("after", `
4526
5132
  ${highlighter.dim("Commands:")}
4527
5133
  aislop scan [dir] Full code quality scan
4528
- aislop fix [dir] Auto-fix formatting and lint issues
5134
+ aislop fix [dir] Auto-fix ai slop in codebase
4529
5135
  aislop init [dir] Initialize aislop config
4530
5136
  aislop doctor [dir] Check installed tools
4531
5137
  aislop ci [dir] CI-friendly JSON output
@@ -4537,7 +5143,8 @@ ${highlighter.dim("Examples:")}
4537
5143
  aislop scan -d Scan with file/line details
4538
5144
  aislop scan --changes Scan only changed files
4539
5145
  aislop scan --staged Scan only staged files (for hooks)
4540
- aislop fix Auto-fix issues
5146
+ aislop fix Auto-fix ai slop in codebase
5147
+ aislop fix --force Run aggressive fixes (includes audit and dependency alignment)
4541
5148
  aislop ci JSON output for CI pipelines
4542
5149
  `);
4543
5150
  program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").action(async (directory = ".", _flags, command) => {
@@ -4553,9 +5160,12 @@ program.command("scan [directory]").description("Run full code quality scan").op
4553
5160
  process.exit(exitCode);
4554
5161
  }
4555
5162
  });
4556
- program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
5163
+ program.command("fix [directory]").description("Auto-fix ai slop in codebase").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").action(async (directory = ".", _flags, command) => {
4557
5164
  const flags = command.optsWithGlobals();
4558
- await fixCommand(directory, loadConfig(directory), { verbose: Boolean(flags.verbose) });
5165
+ await fixCommand(directory, loadConfig(directory), {
5166
+ verbose: Boolean(flags.verbose),
5167
+ force: Boolean(flags.force)
5168
+ });
4559
5169
  });
4560
5170
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
4561
5171
  await initCommand(directory);
@@ -4580,4 +5190,4 @@ const main = async () => {
4580
5190
  main();
4581
5191
 
4582
5192
  //#endregion
4583
- export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
5193
+ export { ENGINE_INFO as n, APP_VERSION as t };