aislop 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +744 -127
- package/dist/index.js +396 -128
- package/dist/{json-BScQXSOX.js → json-D_i2_5_-.js} +1 -1
- package/dist/{version-CxBRws3M.js → version-CIlgPf8Q.js} +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import ts from "typescript";
|
|
14
14
|
import wcwidth from "wcwidth";
|
|
15
|
+
import crypto from "node:crypto";
|
|
15
16
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
16
17
|
|
|
17
18
|
//#region \0rolldown/runtime.js
|
|
@@ -982,7 +983,7 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
982
983
|
};
|
|
983
984
|
|
|
984
985
|
//#endregion
|
|
985
|
-
//#region src/engines/ai-slop/narrative-comments.ts
|
|
986
|
+
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
986
987
|
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
987
988
|
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
988
989
|
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
@@ -1041,6 +1042,19 @@ const SUPPORTED_EXTS = new Set([
|
|
|
1041
1042
|
".java",
|
|
1042
1043
|
".php"
|
|
1043
1044
|
]);
|
|
1045
|
+
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1046
|
+
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1047
|
+
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1048
|
+
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1049
|
+
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
1050
|
+
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1051
|
+
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1052
|
+
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1053
|
+
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1054
|
+
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1055
|
+
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1044
1058
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1045
1059
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1046
1060
|
const getCommentSyntax = (ext) => {
|
|
@@ -1132,16 +1146,6 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1132
1146
|
}
|
|
1133
1147
|
return blocks;
|
|
1134
1148
|
};
|
|
1135
|
-
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1136
|
-
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1137
|
-
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1138
|
-
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1139
|
-
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
1140
|
-
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1141
|
-
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1142
|
-
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1143
|
-
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1144
|
-
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1145
1149
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
1146
1150
|
if (nextLine === null) return false;
|
|
1147
1151
|
if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
|
|
@@ -1682,72 +1686,12 @@ const architectureEngine = {
|
|
|
1682
1686
|
};
|
|
1683
1687
|
|
|
1684
1688
|
//#endregion
|
|
1685
|
-
//#region src/engines/code-quality/
|
|
1686
|
-
const FUNCTION_PATTERNS = [
|
|
1687
|
-
{
|
|
1688
|
-
regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
1689
|
-
langFilter: [
|
|
1690
|
-
".js",
|
|
1691
|
-
".ts",
|
|
1692
|
-
".jsx",
|
|
1693
|
-
".tsx",
|
|
1694
|
-
".mjs",
|
|
1695
|
-
".cjs"
|
|
1696
|
-
]
|
|
1697
|
-
},
|
|
1698
|
-
{
|
|
1699
|
-
regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
|
|
1700
|
-
langFilter: [
|
|
1701
|
-
".js",
|
|
1702
|
-
".ts",
|
|
1703
|
-
".jsx",
|
|
1704
|
-
".tsx",
|
|
1705
|
-
".mjs",
|
|
1706
|
-
".cjs"
|
|
1707
|
-
]
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
|
|
1711
|
-
langFilter: [".py"]
|
|
1712
|
-
},
|
|
1713
|
-
{
|
|
1714
|
-
regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
|
|
1715
|
-
langFilter: [".go"]
|
|
1716
|
-
},
|
|
1717
|
-
{
|
|
1718
|
-
regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
|
|
1719
|
-
langFilter: [".rs"]
|
|
1720
|
-
},
|
|
1721
|
-
{
|
|
1722
|
-
regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
|
|
1723
|
-
langFilter: [
|
|
1724
|
-
".java",
|
|
1725
|
-
".cs",
|
|
1726
|
-
".cpp",
|
|
1727
|
-
".c",
|
|
1728
|
-
".php"
|
|
1729
|
-
]
|
|
1730
|
-
}
|
|
1731
|
-
];
|
|
1732
|
-
const countParams = (p) => p.trim() ? p.split(",").length : 0;
|
|
1733
|
-
const matchFunctionOnLine = (line, ext) => {
|
|
1734
|
-
for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
|
|
1735
|
-
const pattern = FUNCTION_PATTERNS[i];
|
|
1736
|
-
if (!pattern.langFilter.includes(ext)) continue;
|
|
1737
|
-
const match = line.match(pattern.regex);
|
|
1738
|
-
if (match) return {
|
|
1739
|
-
name: match[1],
|
|
1740
|
-
params: match[2] ?? "",
|
|
1741
|
-
patternIndex: i
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
return null;
|
|
1745
|
-
};
|
|
1689
|
+
//#region src/engines/code-quality/function-boundaries.ts
|
|
1746
1690
|
const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1691
|
+
const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
|
|
1692
|
+
const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
|
|
1693
|
+
const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
|
|
1694
|
+
const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
|
|
1751
1695
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
1752
1696
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
1753
1697
|
if (before.endsWith(")")) return true;
|
|
@@ -1827,16 +1771,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
1827
1771
|
maxNesting
|
|
1828
1772
|
};
|
|
1829
1773
|
};
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1834
|
-
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1774
|
+
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
1775
|
+
if (isPython) return findPythonFunctionEnd(lines, startIndex);
|
|
1776
|
+
return findBraceFunctionEnd(lines, startIndex);
|
|
1835
1777
|
};
|
|
1836
|
-
const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
|
|
1837
|
-
const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
|
|
1838
|
-
const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
|
|
1839
|
-
const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
|
|
1840
1778
|
const isBlockArrow = (lines, startIndex) => {
|
|
1841
1779
|
if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
|
|
1842
1780
|
if (ARROW_END_RE.test(lines[startIndex])) {
|
|
@@ -1872,6 +1810,75 @@ const countTemplateLines = (bodyLines) => {
|
|
|
1872
1810
|
}
|
|
1873
1811
|
return templateLineCount;
|
|
1874
1812
|
};
|
|
1813
|
+
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region src/engines/code-quality/complexity.ts
|
|
1816
|
+
const FUNCTION_PATTERNS = [
|
|
1817
|
+
{
|
|
1818
|
+
regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
1819
|
+
langFilter: [
|
|
1820
|
+
".js",
|
|
1821
|
+
".ts",
|
|
1822
|
+
".jsx",
|
|
1823
|
+
".tsx",
|
|
1824
|
+
".mjs",
|
|
1825
|
+
".cjs"
|
|
1826
|
+
]
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
|
|
1830
|
+
langFilter: [
|
|
1831
|
+
".js",
|
|
1832
|
+
".ts",
|
|
1833
|
+
".jsx",
|
|
1834
|
+
".tsx",
|
|
1835
|
+
".mjs",
|
|
1836
|
+
".cjs"
|
|
1837
|
+
]
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
|
|
1841
|
+
langFilter: [".py"]
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
|
|
1845
|
+
langFilter: [".go"]
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
|
|
1849
|
+
langFilter: [".rs"]
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
|
|
1853
|
+
langFilter: [
|
|
1854
|
+
".java",
|
|
1855
|
+
".cs",
|
|
1856
|
+
".cpp",
|
|
1857
|
+
".c",
|
|
1858
|
+
".php"
|
|
1859
|
+
]
|
|
1860
|
+
}
|
|
1861
|
+
];
|
|
1862
|
+
const countParams = (p) => p.trim() ? p.split(",").length : 0;
|
|
1863
|
+
const matchFunctionOnLine = (line, ext) => {
|
|
1864
|
+
for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
|
|
1865
|
+
const pattern = FUNCTION_PATTERNS[i];
|
|
1866
|
+
if (!pattern.langFilter.includes(ext)) continue;
|
|
1867
|
+
const match = line.match(pattern.regex);
|
|
1868
|
+
if (match) return {
|
|
1869
|
+
name: match[1],
|
|
1870
|
+
params: match[2] ?? "",
|
|
1871
|
+
patternIndex: i
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
return null;
|
|
1875
|
+
};
|
|
1876
|
+
const isDataFile = (content) => {
|
|
1877
|
+
const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
|
|
1878
|
+
if (nonEmpty.length === 0) return false;
|
|
1879
|
+
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1880
|
+
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1881
|
+
};
|
|
1875
1882
|
const analyzeFunctions = (content, ext) => {
|
|
1876
1883
|
const lines = content.split("\n");
|
|
1877
1884
|
const functions = [];
|
|
@@ -1894,13 +1901,14 @@ const analyzeFunctions = (content, ext) => {
|
|
|
1894
1901
|
}
|
|
1895
1902
|
return functions;
|
|
1896
1903
|
};
|
|
1904
|
+
const JSX_FILE_LOC_MULTIPLIER = 1.5;
|
|
1897
1905
|
const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
1898
1906
|
const results = [];
|
|
1899
1907
|
const lineCount = content.split("\n").length;
|
|
1900
1908
|
const ext = path.extname(relativePath).toLowerCase();
|
|
1901
1909
|
if (isDataFile(content)) return results;
|
|
1902
|
-
const effectiveMax = ext === ".jsx" || ext === ".tsx" ? limits.maxFileLoc *
|
|
1903
|
-
if (lineCount >
|
|
1910
|
+
const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
|
|
1911
|
+
if (lineCount > effectiveMax) results.push({
|
|
1904
1912
|
filePath: relativePath,
|
|
1905
1913
|
engine: "code-quality",
|
|
1906
1914
|
rule: "complexity/file-too-large",
|
|
@@ -3581,6 +3589,216 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
3581
3589
|
}
|
|
3582
3590
|
};
|
|
3583
3591
|
|
|
3592
|
+
//#endregion
|
|
3593
|
+
//#region src/utils/source-masker.ts
|
|
3594
|
+
const JS_EXTS = new Set([
|
|
3595
|
+
".ts",
|
|
3596
|
+
".tsx",
|
|
3597
|
+
".js",
|
|
3598
|
+
".jsx",
|
|
3599
|
+
".mjs",
|
|
3600
|
+
".cjs"
|
|
3601
|
+
]);
|
|
3602
|
+
const PY_EXTS = new Set([".py"]);
|
|
3603
|
+
const RB_EXTS = new Set([".rb"]);
|
|
3604
|
+
const PHP_EXTS = new Set([".php"]);
|
|
3605
|
+
const familyForExt = (ext) => {
|
|
3606
|
+
if (JS_EXTS.has(ext)) return "js";
|
|
3607
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
3608
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
3609
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
3610
|
+
return "none";
|
|
3611
|
+
};
|
|
3612
|
+
const maskStringsAndComments = (content, ext) => {
|
|
3613
|
+
const family = familyForExt(ext);
|
|
3614
|
+
if (family === "none") return content;
|
|
3615
|
+
if (family === "js") return maskJs(content);
|
|
3616
|
+
return maskSimple(content, family);
|
|
3617
|
+
};
|
|
3618
|
+
const maskJs = (content) => {
|
|
3619
|
+
const out = content.split("");
|
|
3620
|
+
const len = content.length;
|
|
3621
|
+
const tplStack = [];
|
|
3622
|
+
let i = 0;
|
|
3623
|
+
const mask = (start, end) => {
|
|
3624
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
3625
|
+
};
|
|
3626
|
+
while (i < len) {
|
|
3627
|
+
const c = content[i];
|
|
3628
|
+
const next = content[i + 1];
|
|
3629
|
+
if (tplStack.length > 0) {
|
|
3630
|
+
if (c === "{") {
|
|
3631
|
+
tplStack[tplStack.length - 1]++;
|
|
3632
|
+
i++;
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
if (c === "}") {
|
|
3636
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
3637
|
+
tplStack.pop();
|
|
3638
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3639
|
+
mask(i + 1, scan.maskEnd);
|
|
3640
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3641
|
+
i = scan.resumeAt;
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
tplStack[tplStack.length - 1]--;
|
|
3645
|
+
i++;
|
|
3646
|
+
continue;
|
|
3647
|
+
}
|
|
3648
|
+
if (c === "\"" || c === "'") {
|
|
3649
|
+
const strStart = i;
|
|
3650
|
+
i = consumeQuotedString(content, i, c);
|
|
3651
|
+
mask(strStart + 1, i - 1);
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
if (c === "`") {
|
|
3655
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3656
|
+
mask(i + 1, scan.maskEnd);
|
|
3657
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3658
|
+
i = scan.resumeAt;
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
if (c === "/" && next === "/") {
|
|
3662
|
+
const strStart = i;
|
|
3663
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3664
|
+
mask(strStart, i);
|
|
3665
|
+
continue;
|
|
3666
|
+
}
|
|
3667
|
+
if (c === "/" && next === "*") {
|
|
3668
|
+
const strStart = i;
|
|
3669
|
+
i += 2;
|
|
3670
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3671
|
+
if (i < len - 1) i += 2;
|
|
3672
|
+
mask(strStart, i);
|
|
3673
|
+
continue;
|
|
3674
|
+
}
|
|
3675
|
+
i++;
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3678
|
+
if (c === "\"" || c === "'") {
|
|
3679
|
+
const strStart = i;
|
|
3680
|
+
i = consumeQuotedString(content, i, c);
|
|
3681
|
+
mask(strStart + 1, i - 1);
|
|
3682
|
+
continue;
|
|
3683
|
+
}
|
|
3684
|
+
if (c === "`") {
|
|
3685
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3686
|
+
mask(i + 1, scan.maskEnd);
|
|
3687
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3688
|
+
i = scan.resumeAt;
|
|
3689
|
+
continue;
|
|
3690
|
+
}
|
|
3691
|
+
if (c === "/" && next === "/") {
|
|
3692
|
+
const strStart = i;
|
|
3693
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3694
|
+
mask(strStart, i);
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
if (c === "/" && next === "*") {
|
|
3698
|
+
const strStart = i;
|
|
3699
|
+
i += 2;
|
|
3700
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3701
|
+
if (i < len - 1) i += 2;
|
|
3702
|
+
mask(strStart, i);
|
|
3703
|
+
continue;
|
|
3704
|
+
}
|
|
3705
|
+
i++;
|
|
3706
|
+
}
|
|
3707
|
+
return out.join("");
|
|
3708
|
+
};
|
|
3709
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
3710
|
+
const len = content.length;
|
|
3711
|
+
let i = start + 1;
|
|
3712
|
+
while (i < len) {
|
|
3713
|
+
const c = content[i];
|
|
3714
|
+
if (c === "\\" && i + 1 < len) {
|
|
3715
|
+
i += 2;
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
if (c === quote) return i + 1;
|
|
3719
|
+
if (c === "\n") return i;
|
|
3720
|
+
i++;
|
|
3721
|
+
}
|
|
3722
|
+
return i;
|
|
3723
|
+
};
|
|
3724
|
+
const consumeTemplateString = (content, start) => {
|
|
3725
|
+
const len = content.length;
|
|
3726
|
+
let i = start;
|
|
3727
|
+
while (i < len) {
|
|
3728
|
+
const c = content[i];
|
|
3729
|
+
if (c === "\\" && i + 1 < len) {
|
|
3730
|
+
i += 2;
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (c === "`") return {
|
|
3734
|
+
maskEnd: i,
|
|
3735
|
+
resumeAt: i + 1,
|
|
3736
|
+
openedInterp: false
|
|
3737
|
+
};
|
|
3738
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
3739
|
+
maskEnd: i,
|
|
3740
|
+
resumeAt: i + 2,
|
|
3741
|
+
openedInterp: true
|
|
3742
|
+
};
|
|
3743
|
+
i++;
|
|
3744
|
+
}
|
|
3745
|
+
return {
|
|
3746
|
+
maskEnd: i,
|
|
3747
|
+
resumeAt: i,
|
|
3748
|
+
openedInterp: false
|
|
3749
|
+
};
|
|
3750
|
+
};
|
|
3751
|
+
const maskSimple = (content, family) => {
|
|
3752
|
+
const out = content.split("");
|
|
3753
|
+
const len = content.length;
|
|
3754
|
+
let i = 0;
|
|
3755
|
+
const mask = (start, end) => {
|
|
3756
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
3757
|
+
};
|
|
3758
|
+
while (i < len) {
|
|
3759
|
+
const c = content[i];
|
|
3760
|
+
const next = content[i + 1];
|
|
3761
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
3762
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
3763
|
+
const triple = c + c + c;
|
|
3764
|
+
const end = content.indexOf(triple, i + 3);
|
|
3765
|
+
const stop = end === -1 ? len : end + 3;
|
|
3766
|
+
mask(i + 3, stop - 3);
|
|
3767
|
+
i = stop;
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
if (c === "\"" || c === "'") {
|
|
3772
|
+
const strStart = i;
|
|
3773
|
+
i = consumeQuotedString(content, i, c);
|
|
3774
|
+
mask(strStart + 1, i - 1);
|
|
3775
|
+
continue;
|
|
3776
|
+
}
|
|
3777
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
3778
|
+
const strStart = i;
|
|
3779
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3780
|
+
mask(strStart, i);
|
|
3781
|
+
continue;
|
|
3782
|
+
}
|
|
3783
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
3784
|
+
const strStart = i;
|
|
3785
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3786
|
+
mask(strStart, i);
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
3790
|
+
const strStart = i;
|
|
3791
|
+
i += 2;
|
|
3792
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3793
|
+
if (i < len - 1) i += 2;
|
|
3794
|
+
mask(strStart, i);
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
i++;
|
|
3798
|
+
}
|
|
3799
|
+
return out.join("");
|
|
3800
|
+
};
|
|
3801
|
+
|
|
3584
3802
|
//#endregion
|
|
3585
3803
|
//#region src/engines/security/risky.ts
|
|
3586
3804
|
const ev = "eval";
|
|
@@ -3710,12 +3928,13 @@ const detectRiskyConstructs = async (context) => {
|
|
|
3710
3928
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
3711
3929
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
3712
3930
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
3931
|
+
const masked = maskStringsAndComments(content, ext);
|
|
3713
3932
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
3714
3933
|
if (!extensions.includes(ext)) continue;
|
|
3715
3934
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
3716
3935
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3717
3936
|
let match;
|
|
3718
|
-
while ((match = regex.exec(
|
|
3937
|
+
while ((match = regex.exec(masked)) !== null) {
|
|
3719
3938
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3720
3939
|
if (name === "innerhtml") {
|
|
3721
3940
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
@@ -4472,7 +4691,7 @@ const getStagedFiles = (cwd) => {
|
|
|
4472
4691
|
* Application version — injected at build time by tsdown from package.json.
|
|
4473
4692
|
* The fallback should always match the "version" field in package.json.
|
|
4474
4693
|
*/
|
|
4475
|
-
const APP_VERSION = "0.5.
|
|
4694
|
+
const APP_VERSION = "0.5.1";
|
|
4476
4695
|
|
|
4477
4696
|
//#endregion
|
|
4478
4697
|
//#region src/utils/telemetry.ts
|
|
@@ -6071,42 +6290,40 @@ const runExpoDoctor = async (context) => {
|
|
|
6071
6290
|
|
|
6072
6291
|
//#endregion
|
|
6073
6292
|
//#region src/commands/fix-force.ts
|
|
6074
|
-
const
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
|
|
6080
|
-
command: "npm",
|
|
6081
|
-
args: ["audit", "fix"]
|
|
6082
|
-
};
|
|
6293
|
+
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
6294
|
+
const AUDIT_TIMEOUT = 60 * 1e3;
|
|
6295
|
+
const detectPackageManager = (rootDirectory) => {
|
|
6296
|
+
if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
|
|
6297
|
+
if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
|
|
6083
6298
|
return null;
|
|
6084
6299
|
};
|
|
6085
|
-
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
6086
6300
|
const fixDependencyAudit = async (context, onProgress) => {
|
|
6087
|
-
const
|
|
6088
|
-
if (!
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6301
|
+
const pm = detectPackageManager(context.rootDirectory);
|
|
6302
|
+
if (!pm) return;
|
|
6303
|
+
if (pm === "npm") {
|
|
6304
|
+
await runNpmAuditFix(context.rootDirectory, onProgress);
|
|
6305
|
+
await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6306
|
+
return;
|
|
6307
|
+
}
|
|
6308
|
+
await tryPnpmOverrides(context.rootDirectory, onProgress);
|
|
6309
|
+
};
|
|
6310
|
+
const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
6311
|
+
onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
|
|
6312
|
+
const result = await runSubprocess("npm", ["audit", "fix"], {
|
|
6313
|
+
cwd: rootDir,
|
|
6092
6314
|
timeout: INSTALL_TIMEOUT
|
|
6093
6315
|
});
|
|
6094
|
-
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error(
|
|
6095
|
-
onProgress?.(
|
|
6096
|
-
const installResult = await runSubprocess(
|
|
6097
|
-
cwd:
|
|
6316
|
+
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
|
|
6317
|
+
onProgress?.("Dependency audit fixes · running npm install");
|
|
6318
|
+
const installResult = await runSubprocess("npm", ["install"], {
|
|
6319
|
+
cwd: rootDir,
|
|
6098
6320
|
timeout: INSTALL_TIMEOUT
|
|
6099
6321
|
});
|
|
6100
|
-
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout ||
|
|
6101
|
-
if (auditFix.command === "npm") await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6322
|
+
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
|
|
6102
6323
|
};
|
|
6103
|
-
|
|
6104
|
-
* For unresolvable transitive vulnerabilities, attempt to add npm overrides
|
|
6105
|
-
* in package.json. This forces a newer version of the vulnerable transitive dep.
|
|
6106
|
-
*/
|
|
6107
|
-
const fetchLatestVersion = async (rootDir, pkgName) => {
|
|
6324
|
+
const fetchLatestVersion = async (rootDir, pkgName, pm) => {
|
|
6108
6325
|
try {
|
|
6109
|
-
const result = await runSubprocess(
|
|
6326
|
+
const result = await runSubprocess(pm, [
|
|
6110
6327
|
"view",
|
|
6111
6328
|
pkgName,
|
|
6112
6329
|
"version",
|
|
@@ -6120,11 +6337,11 @@ const fetchLatestVersion = async (rootDir, pkgName) => {
|
|
|
6120
6337
|
return null;
|
|
6121
6338
|
}
|
|
6122
6339
|
};
|
|
6123
|
-
const
|
|
6340
|
+
const collectNpmOverrides = async (rootDir, vulnerabilities) => {
|
|
6124
6341
|
const overrides = {};
|
|
6125
6342
|
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
6126
6343
|
if (vuln.fixAvailable !== false || !vuln.range) continue;
|
|
6127
|
-
const latest = await fetchLatestVersion(rootDir, pkgName);
|
|
6344
|
+
const latest = await fetchLatestVersion(rootDir, pkgName, "npm");
|
|
6128
6345
|
if (latest) overrides[pkgName] = latest;
|
|
6129
6346
|
}
|
|
6130
6347
|
return overrides;
|
|
@@ -6133,12 +6350,12 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6133
6350
|
try {
|
|
6134
6351
|
const auditResult = await runSubprocess("npm", ["audit", "--json"], {
|
|
6135
6352
|
cwd: rootDir,
|
|
6136
|
-
timeout:
|
|
6353
|
+
timeout: AUDIT_TIMEOUT
|
|
6137
6354
|
});
|
|
6138
6355
|
if (!auditResult.stdout) return;
|
|
6139
6356
|
const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
|
|
6140
6357
|
if (!vulnerabilities) return;
|
|
6141
|
-
const overrides = await
|
|
6358
|
+
const overrides = await collectNpmOverrides(rootDir, vulnerabilities);
|
|
6142
6359
|
if (Object.keys(overrides).length === 0) return;
|
|
6143
6360
|
const pkgPath = path.join(rootDir, "package.json");
|
|
6144
6361
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -6146,7 +6363,7 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6146
6363
|
...pkg.overrides || {},
|
|
6147
6364
|
...overrides
|
|
6148
6365
|
};
|
|
6149
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)
|
|
6366
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
6150
6367
|
onProgress?.("Dependency audit fixes · applying npm overrides (npm install)");
|
|
6151
6368
|
await runSubprocess("npm", ["install"], {
|
|
6152
6369
|
cwd: rootDir,
|
|
@@ -6154,6 +6371,61 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6154
6371
|
});
|
|
6155
6372
|
} catch {}
|
|
6156
6373
|
};
|
|
6374
|
+
const patchedRangeToVersion = (patched) => {
|
|
6375
|
+
const match = patched.match(/^\s*>=?\s*([0-9]+\.[0-9]+\.[0-9]+[^\s]*)/);
|
|
6376
|
+
return match ? `^${match[1]}` : null;
|
|
6377
|
+
};
|
|
6378
|
+
const overrideKey = (name, vulnerable, patched) => {
|
|
6379
|
+
if (vulnerable && vulnerable.trim().length > 0 && !/^\*$/.test(vulnerable.trim())) return `${name}@${vulnerable.trim()}`;
|
|
6380
|
+
const first = patched.match(/([0-9]+\.[0-9]+\.[0-9]+)/)?.[1];
|
|
6381
|
+
return first ? `${name}@<${first}` : name;
|
|
6382
|
+
};
|
|
6383
|
+
const collectPnpmOverrides = (advisories) => {
|
|
6384
|
+
const overrides = {};
|
|
6385
|
+
for (const adv of Object.values(advisories)) {
|
|
6386
|
+
if (!adv.module_name || !adv.patched_versions) continue;
|
|
6387
|
+
const target = patchedRangeToVersion(adv.patched_versions);
|
|
6388
|
+
if (!target) continue;
|
|
6389
|
+
const key = overrideKey(adv.module_name, adv.vulnerable_versions, adv.patched_versions);
|
|
6390
|
+
overrides[key] = target;
|
|
6391
|
+
}
|
|
6392
|
+
return overrides;
|
|
6393
|
+
};
|
|
6394
|
+
const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
6395
|
+
onProgress?.("Dependency audit fixes · running pnpm audit");
|
|
6396
|
+
const auditResult = await runSubprocess("pnpm", ["audit", "--json"], {
|
|
6397
|
+
cwd: rootDir,
|
|
6398
|
+
timeout: AUDIT_TIMEOUT
|
|
6399
|
+
});
|
|
6400
|
+
if (!auditResult.stdout) return;
|
|
6401
|
+
let parsed;
|
|
6402
|
+
try {
|
|
6403
|
+
parsed = JSON.parse(auditResult.stdout);
|
|
6404
|
+
} catch {
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6407
|
+
const advisories = parsed.advisories;
|
|
6408
|
+
if (!advisories || Object.keys(advisories).length === 0) return;
|
|
6409
|
+
const overrides = collectPnpmOverrides(advisories);
|
|
6410
|
+
if (Object.keys(overrides).length === 0) return;
|
|
6411
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
6412
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
6413
|
+
const pnpmBlock = pkg.pnpm ?? {};
|
|
6414
|
+
const existing = pnpmBlock.overrides ?? {};
|
|
6415
|
+
pkg.pnpm = {
|
|
6416
|
+
...pnpmBlock,
|
|
6417
|
+
overrides: {
|
|
6418
|
+
...existing,
|
|
6419
|
+
...overrides
|
|
6420
|
+
}
|
|
6421
|
+
};
|
|
6422
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
6423
|
+
onProgress?.("Dependency audit fixes · applying pnpm overrides (pnpm install)");
|
|
6424
|
+
await runSubprocess("pnpm", ["install"], {
|
|
6425
|
+
cwd: rootDir,
|
|
6426
|
+
timeout: INSTALL_TIMEOUT
|
|
6427
|
+
});
|
|
6428
|
+
};
|
|
6157
6429
|
const fixExpoDependencies = async (context, onProgress) => {
|
|
6158
6430
|
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
6159
6431
|
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
@@ -6178,10 +6450,6 @@ const fixExpoDependencies = async (context, onProgress) => {
|
|
|
6178
6450
|
});
|
|
6179
6451
|
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
6180
6452
|
};
|
|
6181
|
-
/**
|
|
6182
|
-
* Run expo-doctor to detect packages that should not be installed directly,
|
|
6183
|
-
* then uninstall them. No hardcoded list — expo-doctor is the source of truth.
|
|
6184
|
-
*/
|
|
6185
6453
|
const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
6186
6454
|
try {
|
|
6187
6455
|
onProgress?.("Expo dependency alignment · running expo-doctor");
|
|
@@ -6268,8 +6536,9 @@ const runFormattingStep = async (deps) => {
|
|
|
6268
6536
|
const runForceSteps = async (deps) => {
|
|
6269
6537
|
if (!deps.force) return;
|
|
6270
6538
|
if (deps.config.engines["code-quality"] && hasJsOrTs(deps.projectInfo)) await deps.runStep("Remove unused files", () => runKnipUnusedFiles(deps.resolvedDir), () => fixUnusedFiles(deps.resolvedDir));
|
|
6271
|
-
|
|
6272
|
-
if (deps.
|
|
6539
|
+
const railUpdate = (label) => deps.rail.setActiveLabel(label);
|
|
6540
|
+
if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
|
|
6541
|
+
if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
|
|
6273
6542
|
};
|
|
6274
6543
|
|
|
6275
6544
|
//#endregion
|
|
@@ -6433,6 +6702,344 @@ const fixCommand = async (directory, config, options = {
|
|
|
6433
6702
|
}
|
|
6434
6703
|
};
|
|
6435
6704
|
|
|
6705
|
+
//#endregion
|
|
6706
|
+
//#region src/hooks/feedback.ts
|
|
6707
|
+
const MAX_FINDINGS = 20;
|
|
6708
|
+
const toFinding = (d, rootDirectory) => {
|
|
6709
|
+
if (d.severity !== "error" && d.severity !== "warning") return null;
|
|
6710
|
+
const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
|
|
6711
|
+
return {
|
|
6712
|
+
ruleId: d.rule,
|
|
6713
|
+
severity: d.severity,
|
|
6714
|
+
category: d.category,
|
|
6715
|
+
file,
|
|
6716
|
+
line: d.line,
|
|
6717
|
+
col: d.column || void 0,
|
|
6718
|
+
message: d.message
|
|
6719
|
+
};
|
|
6720
|
+
};
|
|
6721
|
+
const buildNextSteps = (findings) => {
|
|
6722
|
+
const steps = [];
|
|
6723
|
+
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
6724
|
+
if (errorCount > 0) steps.push(`Fix ${errorCount} error${errorCount === 1 ? "" : "s"} before the next turn.`);
|
|
6725
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6726
|
+
for (const f of findings) {
|
|
6727
|
+
const list = byFile.get(f.file) ?? [];
|
|
6728
|
+
list.push(f);
|
|
6729
|
+
byFile.set(f.file, list);
|
|
6730
|
+
}
|
|
6731
|
+
for (const [file, list] of Array.from(byFile.entries()).slice(0, 3)) {
|
|
6732
|
+
const lines = list.map((f) => f.line).slice(0, 3).join(", ");
|
|
6733
|
+
steps.push(`Address ${list.length} finding${list.length === 1 ? "" : "s"} in ${file} (line${list.length === 1 ? "" : "s"} ${lines}).`);
|
|
6734
|
+
}
|
|
6735
|
+
return steps;
|
|
6736
|
+
};
|
|
6737
|
+
const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
6738
|
+
const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
|
|
6739
|
+
const capped = all.slice(0, MAX_FINDINGS);
|
|
6740
|
+
const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
|
|
6741
|
+
const counts = {
|
|
6742
|
+
error: diagnostics.filter((d) => d.severity === "error").length,
|
|
6743
|
+
warning: diagnostics.filter((d) => d.severity === "warning").length,
|
|
6744
|
+
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
6745
|
+
total: all.length
|
|
6746
|
+
};
|
|
6747
|
+
return {
|
|
6748
|
+
schema: "aislop.hook.v1",
|
|
6749
|
+
score,
|
|
6750
|
+
baseline,
|
|
6751
|
+
regressed: typeof baseline === "number" ? score < baseline : false,
|
|
6752
|
+
counts,
|
|
6753
|
+
findings: capped,
|
|
6754
|
+
elided,
|
|
6755
|
+
nextSteps: buildNextSteps(capped)
|
|
6756
|
+
};
|
|
6757
|
+
};
|
|
6758
|
+
|
|
6759
|
+
//#endregion
|
|
6760
|
+
//#region src/hooks/adapters/claude.ts
|
|
6761
|
+
const extractFiles = (stdin) => {
|
|
6762
|
+
const files = /* @__PURE__ */ new Set();
|
|
6763
|
+
const input = stdin.tool_input ?? {};
|
|
6764
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
6765
|
+
if (Array.isArray(input.edits)) {
|
|
6766
|
+
for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
6767
|
+
}
|
|
6768
|
+
return Array.from(files);
|
|
6769
|
+
};
|
|
6770
|
+
const parseClaudeStdin = (raw) => {
|
|
6771
|
+
if (!raw.trim()) return {};
|
|
6772
|
+
try {
|
|
6773
|
+
return JSON.parse(raw);
|
|
6774
|
+
} catch {
|
|
6775
|
+
return {};
|
|
6776
|
+
}
|
|
6777
|
+
};
|
|
6778
|
+
const readStdin = async () => {
|
|
6779
|
+
if (process.stdin.isTTY) return "";
|
|
6780
|
+
const chunks = [];
|
|
6781
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
6782
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
6783
|
+
};
|
|
6784
|
+
const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f)).filter((p) => {
|
|
6785
|
+
try {
|
|
6786
|
+
return fs.statSync(p).isFile();
|
|
6787
|
+
} catch {
|
|
6788
|
+
return false;
|
|
6789
|
+
}
|
|
6790
|
+
});
|
|
6791
|
+
const runScopedScan = async (cwd, filePaths) => {
|
|
6792
|
+
const project = await discoverProject(cwd);
|
|
6793
|
+
const config = loadConfig(cwd);
|
|
6794
|
+
const configDir = findConfigDir(project.rootDirectory);
|
|
6795
|
+
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
6796
|
+
const diagnostics = (await runEngines({
|
|
6797
|
+
rootDirectory: project.rootDirectory,
|
|
6798
|
+
languages: project.languages,
|
|
6799
|
+
frameworks: project.frameworks,
|
|
6800
|
+
files: filterProjectFiles(project.rootDirectory, filePaths),
|
|
6801
|
+
installedTools: project.installedTools,
|
|
6802
|
+
config: {
|
|
6803
|
+
quality: config.quality,
|
|
6804
|
+
security: {
|
|
6805
|
+
audit: false,
|
|
6806
|
+
auditTimeout: 0
|
|
6807
|
+
},
|
|
6808
|
+
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6809
|
+
}
|
|
6810
|
+
}, {
|
|
6811
|
+
format: config.engines.format,
|
|
6812
|
+
lint: config.engines.lint,
|
|
6813
|
+
"code-quality": config.engines["code-quality"],
|
|
6814
|
+
"ai-slop": config.engines["ai-slop"],
|
|
6815
|
+
architecture: config.engines.architecture,
|
|
6816
|
+
security: false
|
|
6817
|
+
})).flatMap((r) => r.diagnostics);
|
|
6818
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6819
|
+
return {
|
|
6820
|
+
diagnostics,
|
|
6821
|
+
score,
|
|
6822
|
+
rootDirectory: project.rootDirectory
|
|
6823
|
+
};
|
|
6824
|
+
};
|
|
6825
|
+
const renderClaudeOutput = (additional, block) => {
|
|
6826
|
+
const out = { hookSpecificOutput: {
|
|
6827
|
+
hookEventName: "PostToolUse",
|
|
6828
|
+
additionalContext: additional
|
|
6829
|
+
} };
|
|
6830
|
+
if (block) {
|
|
6831
|
+
out.decision = "block";
|
|
6832
|
+
out.reason = block.reason;
|
|
6833
|
+
}
|
|
6834
|
+
return out;
|
|
6835
|
+
};
|
|
6836
|
+
const runClaudeHook = async (deps = {}) => {
|
|
6837
|
+
const getStdin = deps.stdin ?? readStdin;
|
|
6838
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6839
|
+
const input = parseClaudeStdin(await getStdin());
|
|
6840
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
6841
|
+
const absFiles = existingAbsolutePaths(cwd, extractFiles(input));
|
|
6842
|
+
if (absFiles.length === 0) return 0;
|
|
6843
|
+
try {
|
|
6844
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, absFiles);
|
|
6845
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory);
|
|
6846
|
+
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
6847
|
+
write(JSON.stringify(envelope));
|
|
6848
|
+
return 0;
|
|
6849
|
+
} catch {
|
|
6850
|
+
return 0;
|
|
6851
|
+
}
|
|
6852
|
+
};
|
|
6853
|
+
|
|
6854
|
+
//#endregion
|
|
6855
|
+
//#region src/hooks/assets.ts
|
|
6856
|
+
const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
6857
|
+
|
|
6858
|
+
[aislop](https://github.com/heavykenny/aislop) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, oversized functions, risky security constructs — and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
|
|
6859
|
+
|
|
6860
|
+
## On every edit
|
|
6861
|
+
|
|
6862
|
+
A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v1\`). Act on them the same turn.
|
|
6863
|
+
|
|
6864
|
+
## Severity ladder
|
|
6865
|
+
|
|
6866
|
+
- \`error\` — MUST fix this turn.
|
|
6867
|
+
- \`warning\` + \`fixable: true\` — MUST fix this turn.
|
|
6868
|
+
- \`warning\` + \`fixable: false\` — fix if trivially mechanical, otherwise surface in your reply.
|
|
6869
|
+
|
|
6870
|
+
## Rules
|
|
6871
|
+
|
|
6872
|
+
- \`.aislop/config.yaml\` — thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
|
|
6873
|
+
- \`.aislop/rules.yaml\` — project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
|
|
6874
|
+
- Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
|
|
6875
|
+
|
|
6876
|
+
## Principles
|
|
6877
|
+
|
|
6878
|
+
- Do not disable rules to pass the scan. Fix the underlying issue.
|
|
6879
|
+
- If a finding is a false positive, leave it and explain in your reply — do not delete the rule config.
|
|
6880
|
+
- The findings payload includes \`nextSteps[]\` — treat those as your plan for the turn.
|
|
6881
|
+
`;
|
|
6882
|
+
|
|
6883
|
+
//#endregion
|
|
6884
|
+
//#region src/hooks/io/atomic-write.ts
|
|
6885
|
+
const atomicWrite = (targetPath, content) => {
|
|
6886
|
+
const dir = path.dirname(targetPath);
|
|
6887
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
6888
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
6889
|
+
const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
|
|
6890
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
6891
|
+
fs.renameSync(tmp, targetPath);
|
|
6892
|
+
};
|
|
6893
|
+
const readIfExists = (targetPath) => {
|
|
6894
|
+
try {
|
|
6895
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
6896
|
+
} catch {
|
|
6897
|
+
return null;
|
|
6898
|
+
}
|
|
6899
|
+
};
|
|
6900
|
+
|
|
6901
|
+
//#endregion
|
|
6902
|
+
//#region src/hooks/io/json-patch.ts
|
|
6903
|
+
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
6904
|
+
const isAislopManaged = (x) => typeof x === "object" && x !== null && AISLOP_SENTINEL_KEY in x && x[AISLOP_SENTINEL_KEY] != null;
|
|
6905
|
+
const groupIsAislop = (group) => {
|
|
6906
|
+
if (typeof group !== "object" || group === null) return false;
|
|
6907
|
+
const hooks = group.hooks;
|
|
6908
|
+
if (!Array.isArray(hooks)) return false;
|
|
6909
|
+
return hooks.some((h) => isAislopManaged(h));
|
|
6910
|
+
};
|
|
6911
|
+
const upsertHookGroup = (config, event, group) => {
|
|
6912
|
+
const next = { ...config };
|
|
6913
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
6914
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
|
|
6915
|
+
next.hooks = {
|
|
6916
|
+
...hooks,
|
|
6917
|
+
[event]: [...cleaned, group]
|
|
6918
|
+
};
|
|
6919
|
+
return next;
|
|
6920
|
+
};
|
|
6921
|
+
|
|
6922
|
+
//#endregion
|
|
6923
|
+
//#region src/hooks/io/sentinel.ts
|
|
6924
|
+
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
6925
|
+
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
6926
|
+
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
6927
|
+
const renderFence = (body, hash) => [
|
|
6928
|
+
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
6929
|
+
body.trimEnd(),
|
|
6930
|
+
"<!-- aislop:end v1 -->"
|
|
6931
|
+
].join("\n");
|
|
6932
|
+
const upsertMarkdownFence = (existing, body, hash) => {
|
|
6933
|
+
const fenced = renderFence(body, hash);
|
|
6934
|
+
if (existing == null || existing.length === 0) return {
|
|
6935
|
+
nextContent: `${fenced}\n`,
|
|
6936
|
+
replaced: false
|
|
6937
|
+
};
|
|
6938
|
+
const begin = existing.match(BEGIN_RE);
|
|
6939
|
+
const end = existing.match(END_RE);
|
|
6940
|
+
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
6941
|
+
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
6942
|
+
replaced: true
|
|
6943
|
+
};
|
|
6944
|
+
return {
|
|
6945
|
+
nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
|
|
6946
|
+
replaced: false
|
|
6947
|
+
};
|
|
6948
|
+
};
|
|
6949
|
+
|
|
6950
|
+
//#endregion
|
|
6951
|
+
//#region src/hooks/install/claude.ts
|
|
6952
|
+
const resolveClaudePaths = (home) => ({
|
|
6953
|
+
settings: path.join(home, ".claude", "settings.json"),
|
|
6954
|
+
aislopMd: path.join(home, ".claude", "AISLOP.md"),
|
|
6955
|
+
claudeMd: path.join(home, ".claude", "CLAUDE.md")
|
|
6956
|
+
});
|
|
6957
|
+
const buildHookGroup = () => {
|
|
6958
|
+
const hashBody = JSON.stringify({
|
|
6959
|
+
command: "aislop hook claude",
|
|
6960
|
+
matcher: "Edit|Write|MultiEdit"
|
|
6961
|
+
});
|
|
6962
|
+
return {
|
|
6963
|
+
matcher: "Edit|Write|MultiEdit",
|
|
6964
|
+
hooks: [{
|
|
6965
|
+
type: "command",
|
|
6966
|
+
command: "aislop hook claude",
|
|
6967
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
6968
|
+
v: 1,
|
|
6969
|
+
managed: true,
|
|
6970
|
+
hash: sentinelHash(hashBody)
|
|
6971
|
+
}
|
|
6972
|
+
}]
|
|
6973
|
+
};
|
|
6974
|
+
};
|
|
6975
|
+
const installClaude = (opts = { home: os.homedir() }) => {
|
|
6976
|
+
const paths = resolveClaudePaths(opts.home);
|
|
6977
|
+
const wrote = [];
|
|
6978
|
+
const skipped = [];
|
|
6979
|
+
const existingSettingsRaw = readIfExists(paths.settings);
|
|
6980
|
+
let settingsObj = {};
|
|
6981
|
+
if (existingSettingsRaw) try {
|
|
6982
|
+
settingsObj = JSON.parse(existingSettingsRaw);
|
|
6983
|
+
} catch {
|
|
6984
|
+
atomicWrite(`${paths.settings}.aislop-bak`, existingSettingsRaw);
|
|
6985
|
+
}
|
|
6986
|
+
const nextSettings = upsertHookGroup(settingsObj, "PostToolUse", buildHookGroup());
|
|
6987
|
+
const nextSettingsStr = `${JSON.stringify(nextSettings, null, 2)}\n`;
|
|
6988
|
+
if (nextSettingsStr !== existingSettingsRaw) {
|
|
6989
|
+
atomicWrite(paths.settings, nextSettingsStr);
|
|
6990
|
+
wrote.push(paths.settings);
|
|
6991
|
+
} else skipped.push(paths.settings);
|
|
6992
|
+
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
6993
|
+
const existingMd = readIfExists(paths.aislopMd);
|
|
6994
|
+
const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, mdHash);
|
|
6995
|
+
if (fenced.nextContent !== existingMd) {
|
|
6996
|
+
atomicWrite(paths.aislopMd, fenced.nextContent);
|
|
6997
|
+
wrote.push(paths.aislopMd);
|
|
6998
|
+
} else skipped.push(paths.aislopMd);
|
|
6999
|
+
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
7000
|
+
const marker = "@AISLOP.md";
|
|
7001
|
+
if (!existingClaudeMd.includes(marker)) {
|
|
7002
|
+
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
7003
|
+
const nextClaudeMd = `${existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`}${marker}\n`;
|
|
7004
|
+
atomicWrite(paths.claudeMd, nextClaudeMd);
|
|
7005
|
+
wrote.push(paths.claudeMd);
|
|
7006
|
+
} else skipped.push(paths.claudeMd);
|
|
7007
|
+
return {
|
|
7008
|
+
wrote,
|
|
7009
|
+
skipped
|
|
7010
|
+
};
|
|
7011
|
+
};
|
|
7012
|
+
|
|
7013
|
+
//#endregion
|
|
7014
|
+
//#region src/commands/hook.ts
|
|
7015
|
+
const hookInstall = async (opts) => {
|
|
7016
|
+
if (opts.agent !== "claude") {
|
|
7017
|
+
process.stderr.write(`hook install: agent "${opts.agent}" not implemented yet\n`);
|
|
7018
|
+
process.exitCode = 1;
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
if (!opts.global) {
|
|
7022
|
+
process.stderr.write("hook install: only --global supported in this release\n");
|
|
7023
|
+
process.exitCode = 1;
|
|
7024
|
+
return;
|
|
7025
|
+
}
|
|
7026
|
+
const result = installClaude({ home: os.homedir() });
|
|
7027
|
+
if (result.wrote.length === 0) {
|
|
7028
|
+
process.stdout.write("hook install: nothing to do (already up to date)\n");
|
|
7029
|
+
return;
|
|
7030
|
+
}
|
|
7031
|
+
for (const f of result.wrote) process.stdout.write(`wrote ${f}\n`);
|
|
7032
|
+
for (const f of result.skipped) process.stdout.write(`skip ${f}\n`);
|
|
7033
|
+
};
|
|
7034
|
+
const hookRun = async (agent) => {
|
|
7035
|
+
if (agent !== "claude") {
|
|
7036
|
+
process.stderr.write(`hook: agent "${agent}" not implemented yet\n`);
|
|
7037
|
+
process.exit(0);
|
|
7038
|
+
}
|
|
7039
|
+
const exitCode = await runClaudeHook();
|
|
7040
|
+
process.exit(exitCode);
|
|
7041
|
+
};
|
|
7042
|
+
|
|
6436
7043
|
//#endregion
|
|
6437
7044
|
//#region src/commands/init.ts
|
|
6438
7045
|
const buildInitSuccessRender = (input) => {
|
|
@@ -7004,6 +7611,16 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
|
|
|
7004
7611
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
7005
7612
|
await rulesCommand(directory);
|
|
7006
7613
|
});
|
|
7614
|
+
const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
|
|
7615
|
+
hook.command("install").description("Install an agent-integration hook (Claude Code supported)").option("--agent <name>", "target agent (claude)", "claude").option("-g, --global", "install to the user-scope config", true).action(async (opts) => {
|
|
7616
|
+
await hookInstall({
|
|
7617
|
+
agent: opts.agent,
|
|
7618
|
+
global: Boolean(opts.global)
|
|
7619
|
+
});
|
|
7620
|
+
});
|
|
7621
|
+
hook.command("claude").description("Internal: Claude Code PostToolUse callback (reads stdin)").action(async () => {
|
|
7622
|
+
await hookRun("claude");
|
|
7623
|
+
});
|
|
7007
7624
|
const main = async () => {
|
|
7008
7625
|
await program.parseAsync();
|
|
7009
7626
|
await flushTelemetry();
|