aislop 0.2.1 → 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,
@@ -1842,10 +1860,10 @@ const codeQualityEngine = {
1842
1860
 
1843
1861
  //#endregion
1844
1862
  //#region src/engines/format/biome.ts
1845
- const esmRequire$2 = createRequire(import.meta.url);
1863
+ const esmRequire$3 = createRequire(import.meta.url);
1846
1864
  const resolveLocalBiomeScript = () => {
1847
1865
  try {
1848
- const packageJsonPath = esmRequire$2.resolve("@biomejs/biome/package.json");
1866
+ const packageJsonPath = esmRequire$3.resolve("@biomejs/biome/package.json");
1849
1867
  return path.join(path.dirname(packageJsonPath), "bin", "biome");
1850
1868
  } catch {
1851
1869
  return null;
@@ -2092,7 +2110,7 @@ const fixGofmt = async (rootDirectory) => {
2092
2110
  //#endregion
2093
2111
  //#region src/utils/tooling.ts
2094
2112
  const THIS_FILE = fileURLToPath(import.meta.url);
2095
- const esmRequire$1 = createRequire(import.meta.url);
2113
+ const esmRequire$2 = createRequire(import.meta.url);
2096
2114
  const resolvePackageRoot = (startFile) => {
2097
2115
  let current = path.dirname(startFile);
2098
2116
  while (true) {
@@ -2123,7 +2141,7 @@ const isToolAvailable = async (toolName) => {
2123
2141
  };
2124
2142
  const isNodePackageAvailable = (packageName) => {
2125
2143
  try {
2126
- esmRequire$1.resolve(`${packageName}/package.json`);
2144
+ esmRequire$2.resolve(`${packageName}/package.json`);
2127
2145
  return true;
2128
2146
  } catch {
2129
2147
  return false;
@@ -2406,10 +2424,10 @@ const createOxlintConfig = (options) => {
2406
2424
 
2407
2425
  //#endregion
2408
2426
  //#region src/engines/lint/oxlint.ts
2409
- const esmRequire = createRequire(import.meta.url);
2427
+ const esmRequire$1 = createRequire(import.meta.url);
2410
2428
  const resolveOxlintBinary = () => {
2411
2429
  try {
2412
- const oxlintMainPath = esmRequire.resolve("oxlint");
2430
+ const oxlintMainPath = esmRequire$1.resolve("oxlint");
2413
2431
  const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
2414
2432
  return path.join(oxlintDir, "bin", "oxlint");
2415
2433
  } catch {
@@ -2444,6 +2462,144 @@ const detectTestFramework = (rootDir) => {
2444
2462
  } catch {}
2445
2463
  return null;
2446
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
+ };
2447
2603
  const runOxlint = async (context) => {
2448
2604
  const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
2449
2605
  const config = createOxlintConfig({
@@ -2506,6 +2662,7 @@ const fixOxlint = async (context) => {
2506
2662
  configPath,
2507
2663
  "--fix",
2508
2664
  "--fix-suggestions",
2665
+ "--fix-dangerously",
2509
2666
  "."
2510
2667
  ];
2511
2668
  const result = await runSubprocess(process.execPath, args, {
@@ -2513,10 +2670,18 @@ const fixOxlint = async (context) => {
2513
2670
  timeout: 12e4
2514
2671
  });
2515
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);
2516
2678
  } finally {
2517
2679
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
2518
2680
  }
2519
2681
  };
2682
+ const fixOxlintForce = async (context) => {
2683
+ return fixOxlint(context);
2684
+ };
2520
2685
 
2521
2686
  //#endregion
2522
2687
  //#region src/engines/lint/ruff.ts
@@ -2559,6 +2724,18 @@ const fixRuffLint = async (rootDirectory) => {
2559
2724
  });
2560
2725
  if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `ruff check --fix exited with code ${result.exitCode}`);
2561
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
+ };
2562
2739
 
2563
2740
  //#endregion
2564
2741
  //#region src/engines/lint/index.ts
@@ -2569,7 +2746,7 @@ const lintEngine = {
2569
2746
  const { languages, installedTools } = context;
2570
2747
  const promises = [];
2571
2748
  if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runOxlint(context));
2572
- 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)));
2573
2750
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
2574
2751
  if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
2575
2752
  if (languages.includes("rust") && installedTools["cargo"]) promises.push(runGenericLinter(context, "rust"));
@@ -3194,11 +3371,11 @@ const logger = {
3194
3371
  * Application version — injected at build time by tsdown from package.json.
3195
3372
  * The fallback should always match the "version" field in package.json.
3196
3373
  */
3197
- const APP_VERSION = "0.2.1";
3374
+ const APP_VERSION = "0.3.0";
3198
3375
 
3199
3376
  //#endregion
3200
3377
  //#region src/output/layout.ts
3201
- 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`;
3202
3379
  const printCommandHeader = (commandName) => {
3203
3380
  logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
3204
3381
  logger.break();
@@ -3213,7 +3390,7 @@ const printProjectMetadata = (project) => {
3213
3390
 
3214
3391
  //#endregion
3215
3392
  //#region src/output/scan-progress.ts
3216
- const SPINNER_FRAMES = [
3393
+ const SPINNER_FRAMES$1 = [
3217
3394
  "⠋",
3218
3395
  "⠙",
3219
3396
  "⠹",
@@ -3226,20 +3403,20 @@ const SPINNER_FRAMES = [
3226
3403
  "⠏"
3227
3404
  ];
3228
3405
  const shouldRenderLiveScanProgress = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
3229
- 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`;
3230
3407
  const truncateText = (text, maxLength = 52) => text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}…`;
3231
3408
  const getIssueSummary = (result) => {
3232
3409
  const errors = result.diagnostics.filter((d) => d.severity === "error").length;
3233
3410
  const warnings = result.diagnostics.filter((d) => d.severity === "warning").length;
3234
- 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)})`;
3235
3412
  const parts = [];
3236
3413
  if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
3237
3414
  if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
3238
- return `Done (${parts.join(", ")}, ${formatElapsed(result.elapsed)})`;
3415
+ return `Done (${parts.join(", ")}, ${formatElapsed$1(result.elapsed)})`;
3239
3416
  };
3240
- const getStatusParts = (state, frameIndex) => {
3417
+ const getStatusParts$1 = (state, frameIndex) => {
3241
3418
  if (state.status === "running") return {
3242
- icon: highlighter.info(SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]),
3419
+ icon: highlighter.info(SPINNER_FRAMES$1[frameIndex % SPINNER_FRAMES$1.length]),
3243
3420
  detail: highlighter.info("Running")
3244
3421
  };
3245
3422
  if (state.status === "done") {
@@ -3278,11 +3455,11 @@ const renderScanProgressBlock = (states, frameIndex) => {
3278
3455
  const headingStatus = completedCount === states.length ? highlighter.dim("complete") : runningCount > 0 ? highlighter.dim(`${runningCount} running`) : highlighter.dim("starting");
3279
3456
  return `${[` ${highlighter.bold(`Engines ${completedCount}/${states.length}`)} ${headingStatus}`, ...states.map((state) => {
3280
3457
  const label = getEngineLabel(state.engine).padEnd(labelWidth, " ");
3281
- const { icon, detail } = getStatusParts(state, frameIndex);
3458
+ const { icon, detail } = getStatusParts$1(state, frameIndex);
3282
3459
  return ` ${icon} ${label} ${detail}`;
3283
3460
  })].join("\n")}\n`;
3284
3461
  };
3285
- const clearRenderedLines$1 = (lineCount) => {
3462
+ const clearRenderedLines$2 = (lineCount) => {
3286
3463
  if (lineCount === 0) return;
3287
3464
  process.stderr.write(`\u001B[${lineCount}F`);
3288
3465
  for (let index = 0; index < lineCount; index += 1) {
@@ -3334,7 +3511,7 @@ var ScanProgressRenderer = class {
3334
3511
  }
3335
3512
  render() {
3336
3513
  if (!shouldRenderLiveScanProgress()) return;
3337
- if (this.previousLineCount > 0) clearRenderedLines$1(this.previousLineCount);
3514
+ if (this.previousLineCount > 0) clearRenderedLines$2(this.previousLineCount);
3338
3515
  const output = renderScanProgressBlock(this.states, this.frameIndex);
3339
3516
  process.stderr.write(output);
3340
3517
  this.previousLineCount = output.split("\n").length - 1;
@@ -3976,6 +4153,42 @@ const doctorCommand = async (directory) => {
3976
4153
  printDoctorConclusion(isAllGood());
3977
4154
  };
3978
4155
 
4156
+ //#endregion
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
+
3979
4192
  //#endregion
3980
4193
  //#region src/engines/ai-slop/unused-imports-fix.ts
3981
4194
  const fixUnusedImports = async (context) => {
@@ -3997,7 +4210,7 @@ const fixUnusedImports = async (context) => {
3997
4210
  for (const [lineNo, syms] of symbolsByLine) {
3998
4211
  const lineIdx = lineNo - 1;
3999
4212
  const allUnused = syms.every((s) => unusedNames.has(s.name));
4000
- const importSpan = getImportSpan(lineIdx, analysis.importLines);
4213
+ const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
4001
4214
  if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
4002
4215
  else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
4003
4216
  else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
@@ -4010,11 +4223,14 @@ const fixUnusedImports = async (context) => {
4010
4223
  fs.writeFileSync(filePath, filtered.join("\n"));
4011
4224
  }
4012
4225
  };
4013
- const getImportSpan = (startIdx, importLines) => {
4226
+ const getJsImportSpan = (lines, startIdx) => {
4014
4227
  const span = [startIdx];
4228
+ let fullImport = lines[startIdx]?.trim() ?? "";
4229
+ if (!fullImport.startsWith("import ")) return span;
4015
4230
  let idx = startIdx + 1;
4016
- while (importLines.has(idx)) {
4231
+ while (!fullImport.includes("from") && idx < lines.length) {
4017
4232
  span.push(idx);
4233
+ fullImport += ` ${lines[idx].trim()}`;
4018
4234
  idx++;
4019
4235
  }
4020
4236
  return span;
@@ -4024,23 +4240,31 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4024
4240
  const namedMatch = fullImport.match(/\{([^}]+)\}/s);
4025
4241
  if (!namedMatch) return;
4026
4242
  const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
4027
- if (unusedNamed.length === 0) return;
4243
+ const defaultUnused = syms.some((s) => s.isDefault && unusedNames.has(s.name));
4244
+ if (unusedNamed.length === 0 && !defaultUnused) return;
4028
4245
  const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
4029
4246
  const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
4030
4247
  const parts = spec.split(/\s+as\s+/);
4031
4248
  const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
4032
4249
  return !unusedNamedSet.has(localName);
4033
4250
  });
4251
+ const fromMatch = fullImport.match(/from\s+["']([^"']+)["'];?/);
4252
+ const fromClause = fromMatch ? `from "${fromMatch[1]}"` : "";
4034
4253
  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, " ");
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};`;
4038
4259
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4039
- }
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;
4040
4266
  return;
4041
4267
  }
4042
- const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
4043
- const fromClause = fromMatch ? fromMatch[1].trim() : "";
4044
4268
  const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
4045
4269
  const prefix = importPrefix ? importPrefix[1] : "import ";
4046
4270
  const wasMultiLine = span.length > 1;
@@ -4048,8 +4272,8 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4048
4272
  if (wasMultiLine && keptSpecifiers.length > 2) {
4049
4273
  const indentMatch = lines[span[1]]?.match(/^(\s+)/);
4050
4274
  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}`;
4275
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause};`;
4276
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause};`;
4053
4277
  lines[span[0]] = newImport;
4054
4278
  for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4055
4279
  };
@@ -4070,7 +4294,289 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
4070
4294
  };
4071
4295
 
4072
4296
  //#endregion
4073
- //#region src/commands/fix.ts
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
4074
4580
  const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
4075
4581
  const uniqueFileCount = (diagnostics) => uniqueFiles(diagnostics).length;
4076
4582
  const getFilePreviewLines = (title, files, verbose) => {
@@ -4094,7 +4600,8 @@ const getStepStatusLine = (result, name, elapsedLabel) => {
4094
4600
  if (result.resolvedIssues > 0) return highlighter.warn(` ! ${name}: done (${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${elapsedLabel})`);
4095
4601
  return highlighter.warn(` ! ${name}: done (no auto-fix changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${elapsedLabel})`);
4096
4602
  };
4097
- const runFixStep = async (name, detect, applyFix, options) => {
4603
+ const runFixStep = async (name, detect, applyFix, options, progress) => {
4604
+ progress.markStarted(name);
4098
4605
  const stepStart = performance.now();
4099
4606
  const before = await detect();
4100
4607
  let applyError = null;
@@ -4114,28 +4621,21 @@ const runFixStep = async (name, detect, applyFix, options) => {
4114
4621
  failed: applyError !== null && before.length === after.length,
4115
4622
  elapsedMs
4116
4623
  };
4117
- const lines = [getStepStatusLine(result, name, formatElapsed$1(result.elapsedMs))];
4118
- if (applyError) {
4119
- const reasonLines = getReasonLines(applyError instanceof Error ? applyError.message : String(applyError));
4120
- const reasonToPrint = options.verbose ? reasonLines.printable : reasonLines.firstLine;
4121
- for (const line of reasonToPrint.split("\n")) lines.push(highlighter.dim(` ${line}`));
4122
- if (!options.verbose && reasonLines.printable !== reasonToPrint) lines.push(highlighter.dim(" Re-run with -d for full tool output."));
4123
- }
4124
- lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4125
- if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4126
- process.stdout.write(`${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
+ }
4127
4637
  return result;
4128
4638
  };
4129
- const createEngineContext = (rootDirectory, projectInfo, config) => ({
4130
- rootDirectory,
4131
- languages: projectInfo.languages,
4132
- frameworks: projectInfo.frameworks,
4133
- installedTools: projectInfo.installedTools,
4134
- config: {
4135
- quality: config.quality,
4136
- security: config.security
4137
- }
4138
- });
4139
4639
  const summarizeFixRun = (steps) => {
4140
4640
  const totals = steps.reduce((acc, step) => {
4141
4641
  acc.beforeIssues += step.beforeIssues;
@@ -4155,6 +4655,19 @@ const summarizeFixRun = (steps) => {
4155
4655
  } else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
4156
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.");
4157
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
+ });
4158
4671
  const fixCommand = async (directory, config, options = {
4159
4672
  verbose: false,
4160
4673
  showHeader: true
@@ -4166,38 +4679,98 @@ const fixCommand = async (directory, config, options = {
4166
4679
  printProjectMetadata(projectInfo);
4167
4680
  const context = createEngineContext(resolvedDir, projectInfo, config);
4168
4681
  const steps = [];
4169
- if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
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
+ }
4694
+ if (config.engines.format) {
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));
4712
+ }
4170
4713
  if (config.engines.lint) {
4171
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4172
- 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));
4173
4716
  else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
4174
4717
  }
4175
4718
  if (config.engines["code-quality"]) {
4176
- 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));
4177
4720
  }
4178
4721
  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));
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));
4181
4724
  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));
4725
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options, progress));
4183
4726
  else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4184
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);
4185
4734
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
4186
4735
  else {
4187
4736
  logger.break();
4188
4737
  summarizeFixRun(steps);
4189
4738
  }
4190
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
4191
- const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
4192
- trackEvent({
4193
- command: "fix",
4194
- languages: projectInfo.languages,
4195
- fixSteps: steps.length,
4196
- fixResolved: totalResolved
4197
- });
4198
- }
4739
+ if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
4740
+ command: "fix",
4741
+ languages: projectInfo.languages,
4742
+ fixSteps: steps.length,
4743
+ fixResolved: totalResolved
4744
+ });
4199
4745
  logger.break();
4200
- 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("------------------------------------------------------------"));
4201
4774
  logger.break();
4202
4775
  };
4203
4776
 
@@ -4558,7 +5131,7 @@ const program = new Command().name("aislop").description("The unified code quali
4558
5131
  }).addHelpText("after", `
4559
5132
  ${highlighter.dim("Commands:")}
4560
5133
  aislop scan [dir] Full code quality scan
4561
- aislop fix [dir] Auto-fix formatting and lint issues
5134
+ aislop fix [dir] Auto-fix ai slop in codebase
4562
5135
  aislop init [dir] Initialize aislop config
4563
5136
  aislop doctor [dir] Check installed tools
4564
5137
  aislop ci [dir] CI-friendly JSON output
@@ -4570,7 +5143,8 @@ ${highlighter.dim("Examples:")}
4570
5143
  aislop scan -d Scan with file/line details
4571
5144
  aislop scan --changes Scan only changed files
4572
5145
  aislop scan --staged Scan only staged files (for hooks)
4573
- 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)
4574
5148
  aislop ci JSON output for CI pipelines
4575
5149
  `);
4576
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) => {
@@ -4586,9 +5160,12 @@ program.command("scan [directory]").description("Run full code quality scan").op
4586
5160
  process.exit(exitCode);
4587
5161
  }
4588
5162
  });
4589
- 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) => {
4590
5164
  const flags = command.optsWithGlobals();
4591
- 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
+ });
4592
5169
  });
4593
5170
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
4594
5171
  await initCommand(directory);
@@ -4613,4 +5190,4 @@ const main = async () => {
4613
5190
  main();
4614
5191
 
4615
5192
  //#endregion
4616
- export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
5193
+ export { ENGINE_INFO as n, APP_VERSION as t };