aislop 0.3.2 → 0.4.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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "commander";
4
+ import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { performance } from "node:perf_hooks";
6
- import fs from "node:fs";
7
7
  import YAML from "yaml";
8
8
  import { z } from "zod/v4";
9
9
  import { spawn, spawnSync } from "node:child_process";
@@ -257,7 +257,9 @@ const loadConfig = (directory) => {
257
257
  try {
258
258
  const raw = fs.readFileSync(configPath, "utf-8");
259
259
  return parseConfig(YAML.parse(raw));
260
- } catch {
260
+ } catch (error) {
261
+ const msg = error instanceof Error ? error.message : String(error);
262
+ process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
261
263
  return DEFAULT_CONFIG;
262
264
  }
263
265
  };
@@ -406,15 +408,19 @@ const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
406
408
  }).filter(({ relativePath }) => isWithinProject(relativePath) && hasAllowedExtension(relativePath, extraSet)).map(({ absolutePath }) => absolutePath);
407
409
  };
408
410
  const isAutoGenerated = (filePath) => {
411
+ let fd;
409
412
  try {
410
- const fd = fs.openSync(filePath, "r");
413
+ fd = fs.openSync(filePath, "r");
411
414
  const buf = Buffer.alloc(512);
412
415
  const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
413
- fs.closeSync(fd);
414
416
  const header = buf.toString("utf-8", 0, bytesRead);
415
417
  return AUTO_GENERATED_PATTERNS.some((pattern) => pattern.test(header));
416
418
  } catch {
417
419
  return false;
420
+ } finally {
421
+ if (fd !== void 0) try {
422
+ fs.closeSync(fd);
423
+ } catch {}
418
424
  }
419
425
  };
420
426
  const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
@@ -651,7 +657,11 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
651
657
  "FIXME",
652
658
  "HACK",
653
659
  "XXX"
654
- ].join("|")}|TEMP|PLACEHOLDER|STUB)\\b[:\\s]`, "i");
660
+ ].join("|")})\\b[:\\s]|\\b(?:${[
661
+ "TEMP",
662
+ "PLACEHOLDER",
663
+ "STUB"
664
+ ].join("|")})[:\\s]`);
655
665
  const detectTodoStubs = (content, relativePath) => {
656
666
  const diagnostics = [];
657
667
  const lines = content.split("\n");
@@ -753,18 +763,20 @@ const detectUnsafeTypePatterns = (content, relativePath, ext) => {
753
763
  category: "AI Slop",
754
764
  fixable: false
755
765
  });
756
- if (doubleAssertPattern.test(trimmed)) diagnostics.push({
757
- filePath: relativePath,
758
- engine: "ai-slop",
759
- rule: "ai-slop/double-type-assertion",
760
- severity: "warning",
761
- message: `Double type assertion (as unknown as X) bypasses type checking`,
762
- help: "Refactor to avoid needing a double assertion it usually indicates a design issue",
763
- line: i + 1,
764
- column: 0,
765
- category: "AI Slop",
766
- fixable: false
767
- });
766
+ if (doubleAssertPattern.test(trimmed)) {
767
+ if (!(/\.query[(<]/.test(trimmed) || /result\[0\]/.test(trimmed) || /rows\s/.test(trimmed))) diagnostics.push({
768
+ filePath: relativePath,
769
+ engine: "ai-slop",
770
+ rule: "ai-slop/double-type-assertion",
771
+ severity: "warning",
772
+ message: `Double type assertion (as unknown as X) bypasses type checking`,
773
+ help: "Refactor to avoid needing a double assertion. If this is an ORM query return, consider a typed wrapper function",
774
+ line: i + 1,
775
+ column: 0,
776
+ category: "AI Slop",
777
+ fixable: false
778
+ });
779
+ }
768
780
  }
769
781
  return diagnostics;
770
782
  };
@@ -1541,82 +1553,6 @@ const checkComplexity = async (context) => {
1541
1553
  return diagnostics;
1542
1554
  };
1543
1555
 
1544
- //#endregion
1545
- //#region src/engines/code-quality/duplication.ts
1546
- const MIN_DUPLICATE_LINES = 12;
1547
- const MIN_DUPLICATE_CHARS = 240;
1548
- const MAX_DUPLICATE_REPORTS = 50;
1549
- const isIgnorableLine = (line) => {
1550
- const trimmed = line.trim();
1551
- return trimmed.length === 0 || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#");
1552
- };
1553
- const normalizeLine = (line) => line.trim().replace(/\s+/g, " ");
1554
- const extractDuplicateBlocks = (content) => {
1555
- const blocks = [];
1556
- const lines = content.split("\n");
1557
- for (let i = 0; i <= lines.length - MIN_DUPLICATE_LINES; i++) {
1558
- const segment = lines.slice(i, i + MIN_DUPLICATE_LINES);
1559
- if (segment.some(isIgnorableLine)) continue;
1560
- const key = segment.map(normalizeLine).join("\n");
1561
- if (key.length < MIN_DUPLICATE_CHARS) continue;
1562
- blocks.push({
1563
- key,
1564
- startLine: i + 1
1565
- });
1566
- }
1567
- return blocks;
1568
- };
1569
- const checkDuplication = async (context) => {
1570
- const files = getSourceFiles(context);
1571
- const duplicates = /* @__PURE__ */ new Map();
1572
- for (const absoluteFilePath of files) {
1573
- if (isAutoGenerated(absoluteFilePath)) continue;
1574
- let content = "";
1575
- try {
1576
- content = fs.readFileSync(absoluteFilePath, "utf-8");
1577
- } catch {
1578
- continue;
1579
- }
1580
- const relativeFilePath = path.relative(context.rootDirectory, absoluteFilePath);
1581
- for (const block of extractDuplicateBlocks(content)) {
1582
- const occurrence = {
1583
- filePath: relativeFilePath,
1584
- startLine: block.startLine
1585
- };
1586
- const list = duplicates.get(block.key) ?? [];
1587
- list.push(occurrence);
1588
- duplicates.set(block.key, list);
1589
- }
1590
- }
1591
- const diagnostics = [];
1592
- const reportedPairs = /* @__PURE__ */ new Set();
1593
- for (const occurrences of duplicates.values()) {
1594
- if (occurrences.length < 2) continue;
1595
- const source = occurrences[0];
1596
- for (const occurrence of occurrences.slice(1)) {
1597
- if (diagnostics.length >= MAX_DUPLICATE_REPORTS) return diagnostics;
1598
- if (occurrence.filePath === source.filePath && occurrence.startLine === source.startLine) continue;
1599
- if (occurrence.filePath === source.filePath) continue;
1600
- const pairKey = `${source.filePath}->${occurrence.filePath}`;
1601
- if (reportedPairs.has(pairKey)) continue;
1602
- reportedPairs.add(pairKey);
1603
- diagnostics.push({
1604
- filePath: occurrence.filePath,
1605
- engine: "code-quality",
1606
- rule: "duplication/block",
1607
- severity: "warning",
1608
- message: `Possible duplicated code block (${MIN_DUPLICATE_LINES}+ lines) also found at ${source.filePath}:${source.startLine}`,
1609
- help: "Extract shared logic into a reusable function or module",
1610
- line: occurrence.startLine,
1611
- column: 0,
1612
- category: "Duplication",
1613
- fixable: false
1614
- });
1615
- }
1616
- }
1617
- return diagnostics;
1618
- };
1619
-
1620
1556
  //#endregion
1621
1557
  //#region src/utils/subprocess.ts
1622
1558
  const runSubprocess = (command, args, options = {}) => {
@@ -1731,7 +1667,7 @@ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
1731
1667
  return diagnostics;
1732
1668
  };
1733
1669
  const findMonorepoRoot = (directory) => {
1734
- let current = path.dirname(directory);
1670
+ let current = directory;
1735
1671
  while (current !== path.dirname(current)) {
1736
1672
  if (fs.existsSync(path.join(current, "pnpm-workspace.yaml")) || (() => {
1737
1673
  const pkgPath = path.join(current, "package.json");
@@ -1791,6 +1727,16 @@ const fixUnusedDependencies = async (rootDirectory) => {
1791
1727
  }
1792
1728
  if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
1793
1729
  };
1730
+ const runKnipUnusedFiles = async (rootDirectory) => {
1731
+ return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/files");
1732
+ };
1733
+ const fixUnusedFiles = async (rootDirectory) => {
1734
+ const diagnostics = await runKnipUnusedFiles(rootDirectory);
1735
+ for (const d of diagnostics) {
1736
+ const absolutePath = path.resolve(rootDirectory, d.filePath);
1737
+ if (fs.existsSync(absolutePath)) fs.unlinkSync(absolutePath);
1738
+ }
1739
+ };
1794
1740
  const runKnip = async (rootDirectory) => {
1795
1741
  const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
1796
1742
  if (!knipRuntime) return [];
@@ -1804,7 +1750,7 @@ const runKnip = async (rootDirectory) => {
1804
1750
  ];
1805
1751
  const result = await runSubprocess(process.execPath, args, {
1806
1752
  cwd: knipRuntime.cwd,
1807
- timeout: 2e4,
1753
+ timeout: 6e4,
1808
1754
  env: { FORCE_COLOR: "0" }
1809
1755
  });
1810
1756
  if (!result.stdout) return [];
@@ -1846,7 +1792,6 @@ const codeQualityEngine = {
1846
1792
  const promises = [];
1847
1793
  if (context.languages.includes("typescript") || context.languages.includes("javascript")) promises.push(runKnip(context.rootDirectory));
1848
1794
  promises.push(checkComplexity(context));
1849
- promises.push(checkDuplication(context));
1850
1795
  const results = await Promise.allSettled(promises);
1851
1796
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
1852
1797
  return {
@@ -1888,6 +1833,24 @@ const BIOME_EXTENSIONS = new Set([
1888
1833
  ".mjs",
1889
1834
  ".cjs"
1890
1835
  ]);
1836
+ const projectHasBiomeConfig = (rootDir) => {
1837
+ try {
1838
+ const biomePath = path.join(rootDir, "biome.json");
1839
+ return fs.existsSync(biomePath);
1840
+ } catch {
1841
+ return false;
1842
+ }
1843
+ };
1844
+ const getBiomeLineWidth = (rootDir) => {
1845
+ try {
1846
+ const biomePath = path.join(rootDir, "biome.json");
1847
+ if (!fs.existsSync(biomePath)) return 120;
1848
+ const content = fs.readFileSync(biomePath, "utf-8");
1849
+ return JSON.parse(content).formatter?.lineWidth ?? 120;
1850
+ } catch {
1851
+ return 120;
1852
+ }
1853
+ };
1891
1854
  const getBiomeTargets = (context) => getSourceFiles(context).filter((filePath) => BIOME_EXTENSIONS.has(path.extname(filePath))).map((filePath) => path.relative(context.rootDirectory, filePath));
1892
1855
  const projectUsesDecorators = (rootDir) => {
1893
1856
  try {
@@ -1902,9 +1865,11 @@ const projectUsesDecorators = (rootDir) => {
1902
1865
  const runBiomeFormat = async (context) => {
1903
1866
  const targets = getBiomeTargets(context);
1904
1867
  if (targets.length === 0) return [];
1868
+ if (!projectHasBiomeConfig(context.rootDirectory)) return [];
1905
1869
  const args = [
1906
1870
  "format",
1907
1871
  "--reporter=json",
1872
+ `--line-width=${getBiomeLineWidth(context.rootDirectory)}`,
1908
1873
  ...targets
1909
1874
  ];
1910
1875
  try {
@@ -1936,7 +1901,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
1936
1901
  for (const entry of parsed.diagnostics) {
1937
1902
  const rawPath = entry.location?.path;
1938
1903
  if (!rawPath) continue;
1939
- const severity = entry.severity === "error" ? "error" : "warning";
1904
+ const severity = "warning";
1940
1905
  const rawMessage = entry.message ?? "";
1941
1906
  const message = !rawMessage || rawMessage.toLowerCase().includes("would have printed") ? "File is not formatted correctly" : rawMessage;
1942
1907
  diagnostics.push({
@@ -1961,6 +1926,7 @@ const fixBiomeFormat = async (context) => {
1961
1926
  await runBiome([
1962
1927
  "format",
1963
1928
  "--write",
1929
+ `--line-width=${getBiomeLineWidth(context.rootDirectory)}`,
1964
1930
  ...targets
1965
1931
  ], context.rootDirectory, 6e4);
1966
1932
  };
@@ -2438,12 +2404,12 @@ const resolveOxlintBinary = () => {
2438
2404
  };
2439
2405
  const parseRuleCode = (code) => {
2440
2406
  if (!code) return {
2441
- plugin: "unknown",
2442
- rule: "unknown"
2407
+ plugin: "eslint",
2408
+ rule: "syntax-error"
2443
2409
  };
2444
2410
  const match = code.match(/^(.+)\((.+)\)$/);
2445
2411
  if (!match) return {
2446
- plugin: "unknown",
2412
+ plugin: "eslint",
2447
2413
  rule: code
2448
2414
  };
2449
2415
  return {
@@ -2633,6 +2599,7 @@ const runOxlint = async (context) => {
2633
2599
  } catch {
2634
2600
  return [];
2635
2601
  }
2602
+ const seen = /* @__PURE__ */ new Set();
2636
2603
  return output.diagnostics.map((d) => {
2637
2604
  const { plugin, rule } = parseRuleCode(d.code);
2638
2605
  const label = d.labels[0];
@@ -2648,6 +2615,11 @@ const runOxlint = async (context) => {
2648
2615
  category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
2649
2616
  fixable: false
2650
2617
  };
2618
+ }).filter((d) => {
2619
+ const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
2620
+ if (seen.has(key)) return false;
2621
+ seen.add(key);
2622
+ return true;
2651
2623
  });
2652
2624
  } finally {
2653
2625
  if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
@@ -2774,7 +2746,7 @@ const runDependencyAudit = async (context) => {
2774
2746
  const timeout = context.config.security.auditTimeout;
2775
2747
  const promises = [];
2776
2748
  if (context.languages.includes("typescript") || context.languages.includes("javascript")) {
2777
- if (fs.existsSync(path.join(context.rootDirectory, "pnpm-lock.yaml"))) promises.push(runPnpmAudit(context.rootDirectory, timeout));
2749
+ if (fs.existsSync(path.join(context.rootDirectory, "pnpm-lock.yaml"))) promises.push(runPnpmAuditWithFallback(context.rootDirectory, timeout));
2778
2750
  else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
2779
2751
  }
2780
2752
  if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
@@ -2794,13 +2766,20 @@ const runNpmAudit = async (rootDir, timeout) => {
2794
2766
  return [];
2795
2767
  }
2796
2768
  };
2797
- const runPnpmAudit = async (rootDir, timeout) => {
2769
+ const runPnpmAuditWithFallback = async (rootDir, timeout) => {
2770
+ const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
2798
2771
  try {
2799
- return parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
2772
+ const diagnostics = parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
2800
2773
  cwd: rootDir,
2801
2774
  timeout
2802
2775
  })).stdout, "pnpm audit");
2776
+ if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
2777
+ if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
2778
+ return [];
2779
+ }
2780
+ return diagnostics;
2803
2781
  } catch {
2782
+ if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
2804
2783
  return [];
2805
2784
  }
2806
2785
  };
@@ -2832,8 +2811,10 @@ const parseModernVulnerabilities = (vulnerabilities, source) => {
2832
2811
  for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
2833
2812
  const severity = (vulnerability.severity ?? "moderate").toLowerCase();
2834
2813
  const fixAvailable = vulnerability.fixAvailable;
2814
+ const isDirect = vulnerability.isDirect === true;
2835
2815
  let recommendation = `Run \`${defaultAuditFixCommand(source)}\` to resolve`;
2836
- if (fixAvailable === false) recommendation = "No automatic fix available.";
2816
+ if (fixAvailable === false) recommendation = isDirect ? "No automatic fix available — check for a newer major version or an alternative package." : "Transitive vulnerability with no fix. Add an override in package.json or upgrade the parent dependency.";
2817
+ else if (!isDirect && fixAvailable === true) recommendation = `Transitive dep — \`${defaultAuditFixCommand(source)}\` may not resolve this. If it persists, add an override in package.json or upgrade the parent package that depends on ${packageName}.`;
2837
2818
  else if (fixAvailable && typeof fixAvailable === "object" && "name" in fixAvailable && "version" in fixAvailable) {
2838
2819
  const target = fixAvailable;
2839
2820
  if (target.name && target.version) recommendation = `Upgrade to ${target.name}@${target.version}.`;
@@ -3376,7 +3357,7 @@ const logger = {
3376
3357
  * Application version — injected at build time by tsdown from package.json.
3377
3358
  * The fallback should always match the "version" field in package.json.
3378
3359
  */
3379
- const APP_VERSION = "0.3.2";
3360
+ const APP_VERSION = "0.4.0";
3380
3361
 
3381
3362
  //#endregion
3382
3363
  //#region src/output/layout.ts
@@ -3615,7 +3596,7 @@ const renderSummary = (diagnostics, scoreResult, elapsedMs, fileCount, threshold
3615
3596
  const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
3616
3597
  const fixableCount = diagnostics.filter((d) => d.fixable).length;
3617
3598
  const elapsed = toElapsedLabel(elapsedMs);
3618
- return `${[
3599
+ const lines = [
3619
3600
  highlighter.dim("------------------------------------------------------------"),
3620
3601
  highlighter.bold("Summary"),
3621
3602
  ` Score: ${colorByScore(`${scoreResult.score}/${PERFECT_SCORE}`, scoreResult.score, thresholds)} ${colorByScore(`(${scoreResult.label})`, scoreResult.score, thresholds)}`,
@@ -3624,7 +3605,15 @@ const renderSummary = (diagnostics, scoreResult, elapsedMs, fileCount, threshold
3624
3605
  ` Files: ${highlighter.info(String(fileCount))}`,
3625
3606
  ` Time: ${highlighter.info(elapsed)}`,
3626
3607
  highlighter.dim("------------------------------------------------------------")
3627
- ].join("\n")}\n`;
3608
+ ];
3609
+ if (errorCount + warningCount > 0) {
3610
+ lines.push("");
3611
+ lines.push(highlighter.bold("Next steps"));
3612
+ if (fixableCount > 0) lines.push(` ${highlighter.info("→")} Run ${highlighter.info("fix")} to auto-fix ${fixableCount} issue${fixableCount === 1 ? "" : "s"}`);
3613
+ lines.push(` ${highlighter.info("→")} Run ${highlighter.info("fix -f")} to apply all available fixes (includes dependency audit)`);
3614
+ lines.push(` ${highlighter.dim("→")} Run ${highlighter.dim("fix --<agent>")} to hand off to a coding agent (see ${highlighter.dim("fix --help")} for supported agents)`);
3615
+ }
3616
+ return `${lines.join("\n")}\n`;
3628
3617
  };
3629
3618
  const printEngineStatus = (result) => {
3630
3619
  const label = getEngineLabel(result.engine);
@@ -3929,6 +3918,18 @@ const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
3929
3918
  const scanCommand = async (directory, config, options) => {
3930
3919
  const startTime = performance.now();
3931
3920
  const resolvedDir = path.resolve(directory);
3921
+ if (!fs.existsSync(resolvedDir)) {
3922
+ const msg = `Path does not exist: ${resolvedDir}`;
3923
+ if (options.json) console.log(JSON.stringify({ error: msg }, null, 2));
3924
+ else logger.error(` ${msg}`);
3925
+ return { exitCode: 1 };
3926
+ }
3927
+ if (!fs.statSync(resolvedDir).isDirectory()) {
3928
+ const msg = `Not a directory: ${resolvedDir}`;
3929
+ if (options.json) console.log(JSON.stringify({ error: msg }, null, 2));
3930
+ else logger.error(` ${msg}`);
3931
+ return { exitCode: 1 };
3932
+ }
3932
3933
  const showHeader = options.showHeader !== false;
3933
3934
  const useLiveProgress = !options.json && shouldUseSpinner();
3934
3935
  if (!options.json && showHeader) printCommandHeader("Scan");
@@ -4161,10 +4162,55 @@ const doctorCommand = async (directory) => {
4161
4162
  //#endregion
4162
4163
  //#region src/engines/ai-slop/dead-patterns-fix.ts
4163
4164
  /**
4165
+ * Given a starting line that contains an opening `(`, find all lines
4166
+ * through the matching `)`. Returns the set of 1-based line numbers.
4167
+ */
4168
+ const findStatementSpan = (lines, startIndex) => {
4169
+ const span = /* @__PURE__ */ new Set();
4170
+ let depth = 0;
4171
+ let started = false;
4172
+ for (let i = startIndex; i < lines.length; i++) {
4173
+ const line = lines[i];
4174
+ span.add(i + 1);
4175
+ for (const ch of line) if (ch === "(") {
4176
+ depth++;
4177
+ started = true;
4178
+ } else if (ch === ")") depth--;
4179
+ if (started && depth <= 0) break;
4180
+ }
4181
+ return span;
4182
+ };
4183
+ /**
4184
+ * Patterns that indicate a console.log is communicating an error or important
4185
+ * status to the user — should be upgraded to console.error, not removed.
4186
+ */
4187
+ const ERROR_MESSAGE_PATTERNS = [
4188
+ /\b(?:error|err|fail|failed|failure|fatal|crash|exception)\b/i,
4189
+ /\b(?:not found|missing|invalid|unable|cannot|couldn'?t|won'?t)\b/i,
4190
+ /\b(?:denied|unauthorized|forbidden|refused|rejected|timeout|timed?\s*out)\b/i,
4191
+ /\bno\s+(?:\w+\s+)*found\b/i,
4192
+ /\bprocess\.exit\b/
4193
+ ];
4194
+ /**
4195
+ * Extracts the full text of a console statement spanning multiple lines.
4196
+ */
4197
+ const getStatementText = (lines, startIndex, span) => {
4198
+ const spanLines = [];
4199
+ for (const lineNo of span) spanLines.push(lines[lineNo - 1]);
4200
+ return spanLines.join("\n");
4201
+ };
4202
+ /**
4203
+ * Determine if a console.log should be replaced with console.error
4204
+ * rather than removed entirely.
4205
+ */
4206
+ const shouldUpgradeToError = (statementText) => {
4207
+ return ERROR_MESSAGE_PATTERNS.some((pattern) => pattern.test(statementText));
4208
+ };
4209
+ /**
4164
4210
  * Removes lines flagged as fixable by the trivial-comment and dead-pattern detectors.
4165
- * Specifically handles:
4166
- * - ai-slop/trivial-comment (trivial comments that restate the code)
4167
- * - ai-slop/console-leftover (console.log/debug/info left in production)
4211
+ * - ai-slop/trivial-comment → remove the line
4212
+ * - ai-slop/console-leftover remove the entire statement (multi-line safe),
4213
+ * OR replace with console.error if the message indicates an error/failure
4168
4214
  */
4169
4215
  const fixDeadPatterns = async (context) => {
4170
4216
  const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
@@ -4172,26 +4218,48 @@ const fixDeadPatterns = async (context) => {
4172
4218
  const byFile = /* @__PURE__ */ new Map();
4173
4219
  for (const d of fixable) {
4174
4220
  const absolute = path.isAbsolute(d.filePath) ? d.filePath : path.join(context.rootDirectory, d.filePath);
4175
- const lines = byFile.get(absolute) ?? /* @__PURE__ */ new Set();
4176
- lines.add(d.line);
4177
- byFile.set(absolute, lines);
4221
+ const entries = byFile.get(absolute) ?? [];
4222
+ entries.push({
4223
+ line: d.line,
4224
+ rule: d.rule
4225
+ });
4226
+ byFile.set(absolute, entries);
4178
4227
  }
4179
- for (const [filePath, lineNumbers] of byFile) {
4180
- if (!fs.existsSync(filePath)) continue;
4181
- const lines = fs.readFileSync(filePath, "utf-8").split("\n");
4182
- const filtered = [];
4183
- for (let i = 0; i < lines.length; i++) {
4184
- const lineNo = i + 1;
4185
- if (lineNumbers.has(lineNo)) continue;
4186
- filtered.push(lines[i]);
4187
- }
4188
- const collapsed = [];
4189
- for (const line of filtered) {
4190
- if (line.trim() === "" && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "") continue;
4191
- collapsed.push(line);
4192
- }
4193
- fs.writeFileSync(filePath, collapsed.join("\n"));
4228
+ for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries);
4229
+ };
4230
+ const fixFileDeadPatterns = (filePath, entries) => {
4231
+ if (!fs.existsSync(filePath)) return;
4232
+ const lines = fs.readFileSync(filePath, "utf-8").split("\n");
4233
+ const linesToRemove = /* @__PURE__ */ new Set();
4234
+ const lineReplacements = /* @__PURE__ */ new Map();
4235
+ for (const entry of entries) {
4236
+ const index = entry.line - 1;
4237
+ if (index < 0 || index >= lines.length) continue;
4238
+ if (entry.rule === "ai-slop/console-leftover") {
4239
+ const span = findStatementSpan(lines, index);
4240
+ if (shouldUpgradeToError(getStatementText(lines, index, span))) {
4241
+ const replaced = lines[index].replace(/console\.(?:log|debug|info|trace|dir|table)\s*\(/, "console.error(");
4242
+ lineReplacements.set(entry.line, replaced);
4243
+ } else for (const lineNo of span) linesToRemove.add(lineNo);
4244
+ } else linesToRemove.add(entry.line);
4245
+ }
4246
+ const result = applyEditsAndCollapse(lines, linesToRemove, lineReplacements);
4247
+ fs.writeFileSync(filePath, result);
4248
+ };
4249
+ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
4250
+ const result = [];
4251
+ for (let i = 0; i < lines.length; i++) {
4252
+ const lineNo = i + 1;
4253
+ if (linesToRemove.has(lineNo)) continue;
4254
+ result.push(lineReplacements.get(lineNo) ?? lines[i]);
4194
4255
  }
4256
+ const collapsed = [];
4257
+ for (const line of result) {
4258
+ const prevEmpty = collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "";
4259
+ if (line.trim() === "" && prevEmpty) continue;
4260
+ collapsed.push(line);
4261
+ }
4262
+ return collapsed.join("\n");
4195
4263
  };
4196
4264
 
4197
4265
  //#endregion
@@ -4440,7 +4508,7 @@ const getStepSummary = (result) => {
4440
4508
  if (result.beforeIssues === 0) return `0 issues, ${formatElapsed(result.elapsedMs)}`;
4441
4509
  if (result.afterIssues === 0) return `${result.resolvedIssues} resolved, ${formatElapsed(result.elapsedMs)}`;
4442
4510
  if (result.resolvedIssues > 0) return `${result.resolvedIssues} resolved, ${result.afterIssues} remaining, ${formatElapsed(result.elapsedMs)}`;
4443
- return `no changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"}, ${formatElapsed(result.elapsedMs)}`;
4511
+ return `no changes, ${result.afterIssues} issue${result.afterIssues === 1 ? "" : "s"} remain, ${formatElapsed(result.elapsedMs)}`;
4444
4512
  };
4445
4513
  const getStatusParts = (state, frameIndex) => {
4446
4514
  if (state.status === "running") return {
@@ -4536,6 +4604,241 @@ var FixProgressRenderer = class {
4536
4604
  }
4537
4605
  };
4538
4606
 
4607
+ //#endregion
4608
+ //#region src/commands/fix-code.ts
4609
+ const CONTEXT_LINES = 3;
4610
+ const MAX_DIAGNOSTICS_PER_FILE = 10;
4611
+ const MAX_FILES = 20;
4612
+ const AGENT_CONFIGS = {
4613
+ claude: {
4614
+ type: "cli",
4615
+ bin: "claude",
4616
+ args: (p) => [p]
4617
+ },
4618
+ codex: {
4619
+ type: "cli",
4620
+ bin: "codex",
4621
+ args: (p) => [p]
4622
+ },
4623
+ amp: {
4624
+ type: "cli",
4625
+ bin: "amp",
4626
+ args: (p) => [p]
4627
+ },
4628
+ antigravity: {
4629
+ type: "cli",
4630
+ bin: "antigravity",
4631
+ args: (p) => [p]
4632
+ },
4633
+ "deep-agents": {
4634
+ type: "cli",
4635
+ bin: "deep-agents",
4636
+ args: (p) => [p]
4637
+ },
4638
+ gemini: {
4639
+ type: "cli",
4640
+ bin: "gemini",
4641
+ args: (p) => [p]
4642
+ },
4643
+ kimi: {
4644
+ type: "cli",
4645
+ bin: "kimi",
4646
+ args: (p) => [p]
4647
+ },
4648
+ opencode: {
4649
+ type: "cli",
4650
+ bin: "opencode",
4651
+ args: (p) => ["run", p]
4652
+ },
4653
+ warp: {
4654
+ type: "cli",
4655
+ bin: "warp",
4656
+ args: (p) => [p]
4657
+ },
4658
+ aider: {
4659
+ type: "cli",
4660
+ bin: "aider",
4661
+ args: (p) => ["--message", p]
4662
+ },
4663
+ goose: {
4664
+ type: "cli",
4665
+ bin: "goose",
4666
+ args: (p) => ["run", p]
4667
+ },
4668
+ cursor: {
4669
+ type: "editor",
4670
+ bin: "cursor"
4671
+ },
4672
+ windsurf: {
4673
+ type: "editor",
4674
+ bin: "windsurf"
4675
+ },
4676
+ vscode: {
4677
+ type: "editor",
4678
+ bin: "code"
4679
+ }
4680
+ };
4681
+ const getCodeSnippet = (rootDirectory, diagnostic) => {
4682
+ if (diagnostic.line <= 0) return null;
4683
+ const absolutePath = path.resolve(rootDirectory, diagnostic.filePath);
4684
+ let content;
4685
+ try {
4686
+ content = fs.readFileSync(absolutePath, "utf-8");
4687
+ } catch {
4688
+ return null;
4689
+ }
4690
+ const lines = content.split("\n");
4691
+ const startLine = Math.max(0, diagnostic.line - 1 - CONTEXT_LINES);
4692
+ const endLine = Math.min(lines.length, diagnostic.line + CONTEXT_LINES);
4693
+ const snippet = [];
4694
+ for (let i = startLine; i < endLine; i++) {
4695
+ const lineNum = i + 1;
4696
+ const marker = lineNum === diagnostic.line ? "→" : " ";
4697
+ snippet.push(`${marker} ${String(lineNum).padStart(4)} │ ${lines[i]}`);
4698
+ }
4699
+ return snippet.join("\n");
4700
+ };
4701
+ const groupByFile = (diagnostics) => {
4702
+ const map = /* @__PURE__ */ new Map();
4703
+ for (const d of diagnostics) {
4704
+ const list = map.get(d.filePath) ?? [];
4705
+ list.push(d);
4706
+ map.set(d.filePath, list);
4707
+ }
4708
+ return [...map.entries()].map(([filePath, diags]) => ({
4709
+ filePath,
4710
+ diagnostics: diags
4711
+ })).sort((a, b) => {
4712
+ const aErrors = a.diagnostics.filter((d) => d.severity === "error").length;
4713
+ const bErrors = b.diagnostics.filter((d) => d.severity === "error").length;
4714
+ if (aErrors !== bErrors) return bErrors - aErrors;
4715
+ return b.diagnostics.length - a.diagnostics.length;
4716
+ });
4717
+ };
4718
+ const isInstalled = (bin) => {
4719
+ return spawnSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" }).status === 0;
4720
+ };
4721
+ const copyToClipboard = (text) => {
4722
+ const args = {
4723
+ darwin: ["pbcopy"],
4724
+ linux: [
4725
+ "xclip",
4726
+ "-selection",
4727
+ "clipboard"
4728
+ ],
4729
+ win32: ["clip"]
4730
+ }[process.platform];
4731
+ if (!args) return false;
4732
+ const [bin, ...rest] = args;
4733
+ return spawnSync(bin, rest, {
4734
+ input: text,
4735
+ encoding: "utf-8"
4736
+ }).status === 0;
4737
+ };
4738
+ const buildAgentPrompt = (rootDirectory, diagnostics, score) => {
4739
+ const groups = groupByFile(diagnostics).slice(0, MAX_FILES);
4740
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
4741
+ const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
4742
+ const lines = [
4743
+ `Fix the following ${diagnostics.length} code quality issue${diagnostics.length === 1 ? "" : "s"} found by aislop (current score: ${score}/100).`,
4744
+ "",
4745
+ `Summary: ${errorCount} error${errorCount === 1 ? "" : "s"}, ${warningCount} warning${warningCount === 1 ? "" : "s"} across ${groups.length} file${groups.length === 1 ? "" : "s"}.`,
4746
+ ""
4747
+ ];
4748
+ for (const group of groups) {
4749
+ lines.push(`## ${group.filePath}`);
4750
+ lines.push("");
4751
+ const fileDiags = group.diagnostics.slice(0, MAX_DIAGNOSTICS_PER_FILE);
4752
+ for (const d of fileDiags) {
4753
+ const severity = d.severity === "error" ? "ERROR" : d.severity === "warning" ? "WARN" : "INFO";
4754
+ const location = d.line > 0 ? ` (line ${d.line})` : "";
4755
+ lines.push(`**[${severity}]** \`${d.rule}\`${location}: ${d.message}`);
4756
+ if (d.help) lines.push(`> ${d.help}`);
4757
+ const snippet = getCodeSnippet(rootDirectory, d);
4758
+ if (snippet) {
4759
+ lines.push("```");
4760
+ lines.push(snippet);
4761
+ lines.push("```");
4762
+ }
4763
+ lines.push("");
4764
+ }
4765
+ if (group.diagnostics.length > MAX_DIAGNOSTICS_PER_FILE) {
4766
+ lines.push(`_...and ${group.diagnostics.length - MAX_DIAGNOSTICS_PER_FILE} more issue${group.diagnostics.length - MAX_DIAGNOSTICS_PER_FILE === 1 ? "" : "s"} in this file._`);
4767
+ lines.push("");
4768
+ }
4769
+ }
4770
+ const totalGroups = groupByFile(diagnostics).length;
4771
+ if (totalGroups > MAX_FILES) {
4772
+ const remaining = totalGroups - MAX_FILES;
4773
+ lines.push(`_...and ${remaining} more file${remaining === 1 ? "" : "s"} with issues._`);
4774
+ lines.push("");
4775
+ }
4776
+ lines.push("---");
4777
+ lines.push("Fix each issue following the guidance above. Prioritize errors over warnings.");
4778
+ lines.push("After making changes, run `npx aislop scan` to verify all issues are resolved and the score improves.");
4779
+ return lines.join("\n");
4780
+ };
4781
+ const SUPPORTED_AGENT_NAMES = Object.keys(AGENT_CONFIGS);
4782
+ const launchAgent = (agent, rootDirectory, diagnostics, score) => {
4783
+ if (diagnostics.length === 0) {
4784
+ logger.success(" No remaining issues — nothing to hand off.");
4785
+ return;
4786
+ }
4787
+ const config = AGENT_CONFIGS[agent];
4788
+ if (!config) {
4789
+ logger.error(` Unknown agent: ${agent}`);
4790
+ logger.dim(` Supported: ${SUPPORTED_AGENT_NAMES.join(", ")}`);
4791
+ return;
4792
+ }
4793
+ if (!isInstalled(config.bin)) {
4794
+ logger.error(` ${agent} is not installed or not in PATH.`);
4795
+ logger.dim(` Install it first, or use ${highlighter.info("fix -p")} to print the prompt manually.`);
4796
+ return;
4797
+ }
4798
+ const prompt = buildAgentPrompt(rootDirectory, diagnostics, score);
4799
+ if (config.type === "editor") {
4800
+ const copied = copyToClipboard(prompt);
4801
+ logger.break();
4802
+ if (copied) logger.log(` ${highlighter.success("✓")} Prompt copied to clipboard (${diagnostics.length} issue${diagnostics.length === 1 ? "" : "s"})`);
4803
+ else logger.warn(" Could not copy to clipboard. Use fix --prompt to print it instead.");
4804
+ logger.log(` ${highlighter.info("→")} Opening ${highlighter.bold(agent)}... paste the prompt into the agent chat.`);
4805
+ logger.break();
4806
+ spawnSync(config.bin, ["."], {
4807
+ cwd: rootDirectory,
4808
+ stdio: "inherit"
4809
+ });
4810
+ return;
4811
+ }
4812
+ logger.break();
4813
+ logger.log(` ${highlighter.info("→")} Opening ${highlighter.bold(agent)} with ${diagnostics.length} issue${diagnostics.length === 1 ? "" : "s"}...`);
4814
+ logger.break();
4815
+ spawnSync(config.bin, config.args(prompt), {
4816
+ cwd: rootDirectory,
4817
+ stdio: "inherit"
4818
+ });
4819
+ };
4820
+ const printPrompt = (rootDirectory, diagnostics, score) => {
4821
+ if (diagnostics.length === 0) {
4822
+ logger.success(" No remaining issues — nothing to generate.");
4823
+ return;
4824
+ }
4825
+ const prompt = buildAgentPrompt(rootDirectory, diagnostics, score);
4826
+ if (!process.stdout.isTTY) {
4827
+ process.stdout.write(prompt);
4828
+ return;
4829
+ }
4830
+ logger.break();
4831
+ logger.log(highlighter.bold("Agent prompt"));
4832
+ logger.log(highlighter.dim(" Copy the prompt below, or pipe it: fix -p | pbcopy"));
4833
+ logger.log(highlighter.dim(" Or launch directly: fix --claude, fix --cursor, fix --codex, etc."));
4834
+ logger.log(highlighter.dim(" Editor agents (--cursor, --windsurf, --vscode) auto-copy to clipboard."));
4835
+ logger.break();
4836
+ logger.log(highlighter.dim("╭─────────────────────────────────────────────────────────╮"));
4837
+ for (const line of prompt.split("\n")) logger.log(` ${line}`);
4838
+ logger.log(highlighter.dim("╰─────────────────────────────────────────────────────────╯"));
4839
+ logger.break();
4840
+ };
4841
+
4539
4842
  //#endregion
4540
4843
  //#region src/commands/fix-force.ts
4541
4844
  const getJsAuditFixCommand = (rootDirectory) => {
@@ -4556,9 +4859,69 @@ const fixDependencyAudit = async (context) => {
4556
4859
  cwd: context.rootDirectory,
4557
4860
  timeout: 18e4
4558
4861
  });
4559
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `${auditFix.command} audit fix failed`);
4862
+ if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error(`${auditFix.command} audit fix failed`);
4863
+ const installResult = await runSubprocess(auditFix.command, ["install"], {
4864
+ cwd: context.rootDirectory,
4865
+ timeout: 18e4
4866
+ });
4867
+ if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || `${auditFix.command} install failed after audit fix`);
4868
+ if (auditFix.command === "npm") await tryNpmOverrides(context.rootDirectory);
4869
+ };
4870
+ /**
4871
+ * For unresolvable transitive vulnerabilities, attempt to add npm overrides
4872
+ * in package.json. This forces a newer version of the vulnerable transitive dep.
4873
+ */
4874
+ const fetchLatestVersion = async (rootDir, pkgName) => {
4875
+ try {
4876
+ const result = await runSubprocess("npm", [
4877
+ "view",
4878
+ pkgName,
4879
+ "version",
4880
+ "--json"
4881
+ ], {
4882
+ cwd: rootDir,
4883
+ timeout: 1e4
4884
+ });
4885
+ return result.stdout ? JSON.parse(result.stdout) : null;
4886
+ } catch {
4887
+ return null;
4888
+ }
4889
+ };
4890
+ const collectOverrides = async (rootDir, vulnerabilities) => {
4891
+ const overrides = {};
4892
+ for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
4893
+ if (vuln.fixAvailable !== false || !vuln.range) continue;
4894
+ const latest = await fetchLatestVersion(rootDir, pkgName);
4895
+ if (latest) overrides[pkgName] = latest;
4896
+ }
4897
+ return overrides;
4898
+ };
4899
+ const tryNpmOverrides = async (rootDir) => {
4900
+ try {
4901
+ const auditResult = await runSubprocess("npm", ["audit", "--json"], {
4902
+ cwd: rootDir,
4903
+ timeout: 3e4
4904
+ });
4905
+ if (!auditResult.stdout) return;
4906
+ const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
4907
+ if (!vulnerabilities) return;
4908
+ const overrides = await collectOverrides(rootDir, vulnerabilities);
4909
+ if (Object.keys(overrides).length === 0) return;
4910
+ const pkgPath = path.join(rootDir, "package.json");
4911
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
4912
+ pkg.overrides = {
4913
+ ...pkg.overrides || {},
4914
+ ...overrides
4915
+ };
4916
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
4917
+ await runSubprocess("npm", ["install"], {
4918
+ cwd: rootDir,
4919
+ timeout: 18e4
4920
+ });
4921
+ } catch {}
4560
4922
  };
4561
4923
  const fixExpoDependencies = async (context) => {
4924
+ await removeDisallowedExpoPackages(context.rootDirectory);
4562
4925
  if ((await runSubprocess("npx", [
4563
4926
  "--yes",
4564
4927
  "expo",
@@ -4579,6 +4942,56 @@ const fixExpoDependencies = async (context) => {
4579
4942
  });
4580
4943
  if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
4581
4944
  };
4945
+ /**
4946
+ * Run expo-doctor to detect packages that should not be installed directly,
4947
+ * then uninstall them. No hardcoded list — expo-doctor is the source of truth.
4948
+ */
4949
+ const removeDisallowedExpoPackages = async (rootDir) => {
4950
+ try {
4951
+ const result = await runSubprocess("npx", [
4952
+ "--yes",
4953
+ "expo-doctor",
4954
+ rootDir
4955
+ ], {
4956
+ cwd: rootDir,
4957
+ timeout: 12e4
4958
+ });
4959
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
4960
+ const packagePattern = /The package "([^"]+)" should not be installed directly/g;
4961
+ const toRemove = [];
4962
+ let match;
4963
+ while ((match = packagePattern.exec(output)) !== null) toRemove.push(match[1]);
4964
+ if (toRemove.length === 0) return;
4965
+ await runSubprocess("npm", ["uninstall", ...toRemove], {
4966
+ cwd: rootDir,
4967
+ timeout: 6e4
4968
+ });
4969
+ } catch {}
4970
+ };
4971
+
4972
+ //#endregion
4973
+ //#region src/commands/fix-plan.ts
4974
+ const hasJsTs = (projectInfo) => projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript");
4975
+ const buildFixStepNames = (projectInfo, config, options) => {
4976
+ const stepNames = [];
4977
+ if (config.engines["ai-slop"]) stepNames.push("Unused imports", "Dead code & comments");
4978
+ if (config.engines.lint) {
4979
+ if (hasJsTs(projectInfo)) stepNames.push("JS/TS lint fixes");
4980
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python lint fixes");
4981
+ }
4982
+ if (config.engines["code-quality"] && hasJsTs(projectInfo)) stepNames.push("Unused dependencies");
4983
+ if (config.engines.format) {
4984
+ if (hasJsTs(projectInfo)) stepNames.push("JS/TS formatting");
4985
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python formatting");
4986
+ if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) stepNames.push("Go formatting");
4987
+ }
4988
+ if (options.force) {
4989
+ if (config.engines["code-quality"] && hasJsTs(projectInfo)) stepNames.push("Remove unused files");
4990
+ if (config.engines.security) stepNames.push("Dependency audit fixes");
4991
+ if (projectInfo.frameworks.includes("expo")) stepNames.push("Expo dependency alignment");
4992
+ }
4993
+ return stepNames;
4994
+ };
4582
4995
 
4583
4996
  //#endregion
4584
4997
  //#region src/commands/fix-step.ts
@@ -4610,12 +5023,12 @@ const runFixStep = async (name, detect, applyFix, options, progress) => {
4610
5023
  const stepStart = performance.now();
4611
5024
  const before = await detect();
4612
5025
  let applyError = null;
4613
- try {
5026
+ if (before.length > 0) try {
4614
5027
  await applyFix();
4615
5028
  } catch (error) {
4616
5029
  applyError = error;
4617
5030
  }
4618
- const after = await detect();
5031
+ const after = before.length > 0 ? await detect() : before;
4619
5032
  const elapsedMs = performance.now() - stepStart;
4620
5033
  const result = {
4621
5034
  name,
@@ -4658,7 +5071,7 @@ const summarizeFixRun = (steps) => {
4658
5071
  logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s).`);
4659
5072
  logger.warn(` ${totals.failedSteps} step(s) reported tool errors; unresolved issue count is unknown for failed steps.`);
4660
5073
  } else logger.log(` Fix summary: checked ${steps.length} step(s), resolved ${totals.resolvedIssues} issue(s), remaining ${totals.afterIssues}.`);
4661
- 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.");
5074
+ if (totals.failedSteps === 0 && totals.beforeIssues > 0 && totals.resolvedIssues === 0) logger.dim(" Remaining issues require manual fixes or agent assistance. Run `scan` for details.");
4662
5075
  };
4663
5076
 
4664
5077
  //#endregion
@@ -4678,34 +5091,18 @@ const fixCommand = async (directory, config, options = {
4678
5091
  showHeader: true
4679
5092
  }) => {
4680
5093
  const resolvedDir = path.resolve(directory);
5094
+ if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
5095
+ const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
5096
+ logger.error(` ${msg}`);
5097
+ return;
5098
+ }
4681
5099
  if (options.showHeader !== false) printCommandHeader("Fix");
4682
5100
  const projectInfo = await discoverProject(resolvedDir);
4683
5101
  logger.success(` ✓ ${formatProjectSummary(projectInfo)}`);
4684
5102
  printProjectMetadata(projectInfo);
4685
5103
  const context = createEngineContext(resolvedDir, projectInfo, config);
4686
5104
  const steps = [];
4687
- const stepNames = [];
4688
- if (config.engines["ai-slop"]) {
4689
- stepNames.push("Unused imports");
4690
- stepNames.push("Dead code & comments");
4691
- }
4692
- if (config.engines.lint) {
4693
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS lint fixes");
4694
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python lint fixes");
4695
- }
4696
- if (config.engines["code-quality"]) {
4697
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("Unused dependencies");
4698
- }
4699
- if (config.engines.format) {
4700
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) stepNames.push("JS/TS formatting");
4701
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) stepNames.push("Python formatting");
4702
- if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) stepNames.push("Go formatting");
4703
- }
4704
- if (options.force) {
4705
- if (config.engines.security) stepNames.push("Dependency audit fixes");
4706
- if (projectInfo.frameworks.includes("expo")) stepNames.push("Expo dependency alignment");
4707
- }
4708
- const progress = new FixProgressRenderer(stepNames);
5105
+ const progress = new FixProgressRenderer(buildFixStepNames(projectInfo, config, options));
4709
5106
  progress.start();
4710
5107
  if (config.engines["ai-slop"]) {
4711
5108
  steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options, progress));
@@ -4731,6 +5128,7 @@ const fixCommand = async (directory, config, options = {
4731
5128
  else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4732
5129
  }
4733
5130
  if (options.force) {
5131
+ if (config.engines["code-quality"] && (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript"))) steps.push(await runFixStep("Remove unused files", () => runKnipUnusedFiles(resolvedDir), () => fixUnusedFiles(resolvedDir), options, progress));
4734
5132
  if (config.engines.security) steps.push(await runFixStep("Dependency audit fixes", () => runDependencyAudit(context), () => fixDependencyAudit(context), options, progress));
4735
5133
  if (projectInfo.frameworks.includes("expo")) steps.push(await runFixStep("Expo dependency alignment", () => runExpoDoctor(context), () => fixExpoDependencies(context), options, progress));
4736
5134
  }
@@ -4748,6 +5146,7 @@ const fixCommand = async (directory, config, options = {
4748
5146
  fixResolved: totalResolved
4749
5147
  });
4750
5148
  logger.break();
5149
+ const verifySpinner = spinner(" Verifying results...").start();
4751
5150
  const configDir = findConfigDir(resolvedDir);
4752
5151
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
4753
5152
  const engineConfig = {
@@ -4755,13 +5154,15 @@ const fixCommand = async (directory, config, options = {
4755
5154
  security: config.security,
4756
5155
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
4757
5156
  };
4758
- const allDiagnostics = (await runEngines({
5157
+ const scanResults = await runEngines({
4759
5158
  rootDirectory: resolvedDir,
4760
5159
  languages: projectInfo.languages,
4761
5160
  frameworks: projectInfo.frameworks,
4762
5161
  installedTools: projectInfo.installedTools,
4763
5162
  config: engineConfig
4764
- }, config.engines, () => {}, () => {})).flatMap((r) => r.diagnostics);
5163
+ }, config.engines, () => {}, () => {});
5164
+ verifySpinner.stop();
5165
+ const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
4765
5166
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
4766
5167
  const errors = allDiagnostics.filter((d) => d.severity === "error").length;
4767
5168
  const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
@@ -4776,6 +5177,26 @@ const fixCommand = async (directory, config, options = {
4776
5177
  if (fixable > 0) logger.log(` Auto-fixable: ${highlighter.info(String(fixable))}`);
4777
5178
  if (manual > 0) logger.log(` Manual effort: ${highlighter.dim(String(manual))}`);
4778
5179
  logger.log(highlighter.dim("------------------------------------------------------------"));
5180
+ if (options.agent) {
5181
+ launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
5182
+ return;
5183
+ }
5184
+ if (options.prompt) {
5185
+ printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
5186
+ return;
5187
+ }
5188
+ const nextSteps = [];
5189
+ if (!options.force && errors + warnings > 0) nextSteps.push(`Run ${highlighter.info("fix -f")} to try aggressive fixes (dependency audit, unused files, unsafe lint)`);
5190
+ if (errors + warnings > 0 && !options.agent && !options.prompt) {
5191
+ nextSteps.push(`Run ${highlighter.info("fix --<agent>")} to hand off to a coding agent, or ${highlighter.info("fix --prompt")} to copy the prompt`);
5192
+ nextSteps.push(highlighter.dim("Agents: --claude, --codex, --gemini, --opencode, --cursor, --amp, --aider, --goose and more"));
5193
+ }
5194
+ if (errors + warnings > 0) nextSteps.push(`Run ${highlighter.info("scan")} to see remaining issues with full details`);
5195
+ if (nextSteps.length > 0) {
5196
+ logger.break();
5197
+ logger.log(highlighter.bold("Next steps"));
5198
+ for (let i = 0; i < nextSteps.length; i++) logger.log(` ${i + 1}. ${nextSteps[i]}`);
5199
+ }
4779
5200
  logger.break();
4780
5201
  };
4781
5202
 
@@ -5149,7 +5570,10 @@ ${highlighter.dim("Examples:")}
5149
5570
  aislop scan --changes Scan only changed files
5150
5571
  aislop scan --staged Scan only staged files (for hooks)
5151
5572
  aislop fix Auto-fix ai slop in codebase
5152
- aislop fix --force Run aggressive fixes (includes audit and dependency alignment)
5573
+ aislop fix -f Run aggressive fixes (includes audit and dependency alignment)
5574
+ aislop fix --claude Open Claude Code to fix remaining issues
5575
+ aislop fix --cursor Open Cursor + copy prompt to clipboard
5576
+ aislop fix -p Print a prompt to paste into any coding agent
5153
5577
  aislop ci JSON output for CI pipelines
5154
5578
  `);
5155
5579
  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) => {
@@ -5165,11 +5589,33 @@ program.command("scan [directory]").description("Run full code quality scan").op
5165
5589
  process.exit(exitCode);
5166
5590
  }
5167
5591
  });
5168
- 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) => {
5592
+ 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)").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues").option("--claude", "open Claude Code to fix remaining issues").option("--codex", "open Codex to fix remaining issues").option("--cursor", "open Cursor and copy prompt to clipboard").option("--windsurf", "open Windsurf and copy prompt to clipboard").option("--vscode", "open VS Code and copy prompt to clipboard").option("--amp", "open Amp to fix remaining issues").option("--antigravity", "open Antigravity to fix remaining issues").option("--deep-agents", "open Deep Agents to fix remaining issues").option("--gemini", "open Gemini CLI to fix remaining issues").option("--kimi", "open Kimi Code CLI to fix remaining issues").option("--opencode", "open OpenCode to fix remaining issues").option("--warp", "open Warp to fix remaining issues").option("--aider", "open Aider to fix remaining issues").option("--goose", "open Goose to fix remaining issues").action(async (directory = ".", _flags, command) => {
5169
5593
  const flags = command.optsWithGlobals();
5170
- await fixCommand(directory, loadConfig(directory), {
5594
+ const config = loadConfig(directory);
5595
+ const agentNames = [
5596
+ "claude",
5597
+ "codex",
5598
+ "cursor",
5599
+ "windsurf",
5600
+ "vscode",
5601
+ "amp",
5602
+ "antigravity",
5603
+ "deepAgents",
5604
+ "gemini",
5605
+ "kimi",
5606
+ "opencode",
5607
+ "warp",
5608
+ "aider",
5609
+ "goose"
5610
+ ];
5611
+ const flagToAgent = { deepAgents: "deep-agents" };
5612
+ const matched = agentNames.find((name) => flags[name]);
5613
+ const agent = matched ? flagToAgent[matched] ?? matched : void 0;
5614
+ await fixCommand(directory, config, {
5171
5615
  verbose: Boolean(flags.verbose),
5172
- force: Boolean(flags.force)
5616
+ force: Boolean(flags.force),
5617
+ prompt: Boolean(flags.prompt),
5618
+ agent
5173
5619
  });
5174
5620
  });
5175
5621
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {