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/README.md +48 -19
- package/dist/cli.js +607 -161
- package/dist/{engine-info-N9t3U_CZ.js → engine-info-s7Gy2StX.js} +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +596 -175
- package/dist/{json-gLNX92Bu.js → json-CdzBXUbz.js} +1 -1
- package/package.json +2 -2
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
|
-
|
|
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("|")}
|
|
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))
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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 =
|
|
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:
|
|
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 =
|
|
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: "
|
|
2442
|
-
rule: "
|
|
2407
|
+
plugin: "eslint",
|
|
2408
|
+
rule: "syntax-error"
|
|
2443
2409
|
};
|
|
2444
2410
|
const match = code.match(/^(.+)\((.+)\)$/);
|
|
2445
2411
|
if (!match) return {
|
|
2446
|
-
plugin: "
|
|
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(
|
|
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
|
|
2769
|
+
const runPnpmAuditWithFallback = async (rootDir, timeout) => {
|
|
2770
|
+
const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
|
|
2798
2771
|
try {
|
|
2799
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
]
|
|
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
|
-
*
|
|
4166
|
-
* - ai-slop/
|
|
4167
|
-
*
|
|
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
|
|
4176
|
-
|
|
4177
|
-
|
|
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,
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
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
|
|
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("
|
|
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
|
|
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
|
|
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, () => {}, () => {})
|
|
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
|
|
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
|
-
|
|
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 = ".") => {
|