aislop 0.5.0 → 0.6.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 +4 -4
- package/dist/cli.js +1748 -127
- package/dist/index.js +471 -128
- package/dist/{json-BScQXSOX.js → json-DcE9soYJ.js} +1 -1
- package/dist/{version-CxBRws3M.js → version-C2lM_2fE.js} +1 -1
- package/package.json +4 -4
- package/scripts/postinstall-tools.mjs +2 -2
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;
|
|
@@ -1025,7 +1026,49 @@ const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
|
1025
1026
|
"todo",
|
|
1026
1027
|
"link",
|
|
1027
1028
|
"license",
|
|
1028
|
-
"preserve"
|
|
1029
|
+
"preserve",
|
|
1030
|
+
"swagger",
|
|
1031
|
+
"openapi",
|
|
1032
|
+
"route",
|
|
1033
|
+
"group",
|
|
1034
|
+
"summary",
|
|
1035
|
+
"description",
|
|
1036
|
+
"operationid",
|
|
1037
|
+
"response",
|
|
1038
|
+
"responses",
|
|
1039
|
+
"request",
|
|
1040
|
+
"requestbody",
|
|
1041
|
+
"security",
|
|
1042
|
+
"tag",
|
|
1043
|
+
"tags",
|
|
1044
|
+
"path",
|
|
1045
|
+
"body",
|
|
1046
|
+
"query",
|
|
1047
|
+
"queryparam",
|
|
1048
|
+
"header",
|
|
1049
|
+
"headers",
|
|
1050
|
+
"produces",
|
|
1051
|
+
"accept",
|
|
1052
|
+
"middleware",
|
|
1053
|
+
"api",
|
|
1054
|
+
"apiname",
|
|
1055
|
+
"apidefine",
|
|
1056
|
+
"apigroup",
|
|
1057
|
+
"apiparam",
|
|
1058
|
+
"apiquery",
|
|
1059
|
+
"apibody",
|
|
1060
|
+
"apiheader",
|
|
1061
|
+
"apisuccess",
|
|
1062
|
+
"apierror",
|
|
1063
|
+
"apiexample",
|
|
1064
|
+
"apiversion",
|
|
1065
|
+
"apidescription",
|
|
1066
|
+
"apipermission",
|
|
1067
|
+
"apiuse",
|
|
1068
|
+
"apiignore",
|
|
1069
|
+
"apiprivate",
|
|
1070
|
+
"namespace",
|
|
1071
|
+
"category"
|
|
1029
1072
|
]);
|
|
1030
1073
|
const SUPPORTED_EXTS = new Set([
|
|
1031
1074
|
".ts",
|
|
@@ -1041,6 +1084,19 @@ const SUPPORTED_EXTS = new Set([
|
|
|
1041
1084
|
".java",
|
|
1042
1085
|
".php"
|
|
1043
1086
|
]);
|
|
1087
|
+
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1088
|
+
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1089
|
+
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1090
|
+
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1091
|
+
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
1092
|
+
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1093
|
+
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1094
|
+
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1095
|
+
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1096
|
+
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1097
|
+
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1044
1100
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1045
1101
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1046
1102
|
const getCommentSyntax = (ext) => {
|
|
@@ -1132,16 +1188,6 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1132
1188
|
}
|
|
1133
1189
|
return blocks;
|
|
1134
1190
|
};
|
|
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
1191
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
1146
1192
|
if (nextLine === null) return false;
|
|
1147
1193
|
if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
|
|
@@ -1682,72 +1728,12 @@ const architectureEngine = {
|
|
|
1682
1728
|
};
|
|
1683
1729
|
|
|
1684
1730
|
//#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
|
-
};
|
|
1731
|
+
//#region src/engines/code-quality/function-boundaries.ts
|
|
1746
1732
|
const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1733
|
+
const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
|
|
1734
|
+
const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
|
|
1735
|
+
const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
|
|
1736
|
+
const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
|
|
1751
1737
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
1752
1738
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
1753
1739
|
if (before.endsWith(")")) return true;
|
|
@@ -1827,16 +1813,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
1827
1813
|
maxNesting
|
|
1828
1814
|
};
|
|
1829
1815
|
};
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1834
|
-
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1816
|
+
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
1817
|
+
if (isPython) return findPythonFunctionEnd(lines, startIndex);
|
|
1818
|
+
return findBraceFunctionEnd(lines, startIndex);
|
|
1835
1819
|
};
|
|
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
1820
|
const isBlockArrow = (lines, startIndex) => {
|
|
1841
1821
|
if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
|
|
1842
1822
|
if (ARROW_END_RE.test(lines[startIndex])) {
|
|
@@ -1872,6 +1852,75 @@ const countTemplateLines = (bodyLines) => {
|
|
|
1872
1852
|
}
|
|
1873
1853
|
return templateLineCount;
|
|
1874
1854
|
};
|
|
1855
|
+
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/engines/code-quality/complexity.ts
|
|
1858
|
+
const FUNCTION_PATTERNS = [
|
|
1859
|
+
{
|
|
1860
|
+
regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
1861
|
+
langFilter: [
|
|
1862
|
+
".js",
|
|
1863
|
+
".ts",
|
|
1864
|
+
".jsx",
|
|
1865
|
+
".tsx",
|
|
1866
|
+
".mjs",
|
|
1867
|
+
".cjs"
|
|
1868
|
+
]
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
|
|
1872
|
+
langFilter: [
|
|
1873
|
+
".js",
|
|
1874
|
+
".ts",
|
|
1875
|
+
".jsx",
|
|
1876
|
+
".tsx",
|
|
1877
|
+
".mjs",
|
|
1878
|
+
".cjs"
|
|
1879
|
+
]
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
|
|
1883
|
+
langFilter: [".py"]
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
|
|
1887
|
+
langFilter: [".go"]
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
|
|
1891
|
+
langFilter: [".rs"]
|
|
1892
|
+
},
|
|
1893
|
+
{
|
|
1894
|
+
regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
|
|
1895
|
+
langFilter: [
|
|
1896
|
+
".java",
|
|
1897
|
+
".cs",
|
|
1898
|
+
".cpp",
|
|
1899
|
+
".c",
|
|
1900
|
+
".php"
|
|
1901
|
+
]
|
|
1902
|
+
}
|
|
1903
|
+
];
|
|
1904
|
+
const countParams = (p) => p.trim() ? p.split(",").length : 0;
|
|
1905
|
+
const matchFunctionOnLine = (line, ext) => {
|
|
1906
|
+
for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
|
|
1907
|
+
const pattern = FUNCTION_PATTERNS[i];
|
|
1908
|
+
if (!pattern.langFilter.includes(ext)) continue;
|
|
1909
|
+
const match = line.match(pattern.regex);
|
|
1910
|
+
if (match) return {
|
|
1911
|
+
name: match[1],
|
|
1912
|
+
params: match[2] ?? "",
|
|
1913
|
+
patternIndex: i
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
return null;
|
|
1917
|
+
};
|
|
1918
|
+
const isDataFile = (content) => {
|
|
1919
|
+
const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
|
|
1920
|
+
if (nonEmpty.length === 0) return false;
|
|
1921
|
+
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1922
|
+
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1923
|
+
};
|
|
1875
1924
|
const analyzeFunctions = (content, ext) => {
|
|
1876
1925
|
const lines = content.split("\n");
|
|
1877
1926
|
const functions = [];
|
|
@@ -1894,13 +1943,14 @@ const analyzeFunctions = (content, ext) => {
|
|
|
1894
1943
|
}
|
|
1895
1944
|
return functions;
|
|
1896
1945
|
};
|
|
1946
|
+
const JSX_FILE_LOC_MULTIPLIER = 1.5;
|
|
1897
1947
|
const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
1898
1948
|
const results = [];
|
|
1899
1949
|
const lineCount = content.split("\n").length;
|
|
1900
1950
|
const ext = path.extname(relativePath).toLowerCase();
|
|
1901
1951
|
if (isDataFile(content)) return results;
|
|
1902
|
-
const effectiveMax = ext === ".jsx" || ext === ".tsx" ? limits.maxFileLoc *
|
|
1903
|
-
if (lineCount >
|
|
1952
|
+
const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
|
|
1953
|
+
if (lineCount > effectiveMax) results.push({
|
|
1904
1954
|
filePath: relativePath,
|
|
1905
1955
|
engine: "code-quality",
|
|
1906
1956
|
rule: "complexity/file-too-large",
|
|
@@ -3581,6 +3631,216 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
3581
3631
|
}
|
|
3582
3632
|
};
|
|
3583
3633
|
|
|
3634
|
+
//#endregion
|
|
3635
|
+
//#region src/utils/source-masker.ts
|
|
3636
|
+
const JS_EXTS = new Set([
|
|
3637
|
+
".ts",
|
|
3638
|
+
".tsx",
|
|
3639
|
+
".js",
|
|
3640
|
+
".jsx",
|
|
3641
|
+
".mjs",
|
|
3642
|
+
".cjs"
|
|
3643
|
+
]);
|
|
3644
|
+
const PY_EXTS = new Set([".py"]);
|
|
3645
|
+
const RB_EXTS = new Set([".rb"]);
|
|
3646
|
+
const PHP_EXTS = new Set([".php"]);
|
|
3647
|
+
const familyForExt = (ext) => {
|
|
3648
|
+
if (JS_EXTS.has(ext)) return "js";
|
|
3649
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
3650
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
3651
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
3652
|
+
return "none";
|
|
3653
|
+
};
|
|
3654
|
+
const maskStringsAndComments = (content, ext) => {
|
|
3655
|
+
const family = familyForExt(ext);
|
|
3656
|
+
if (family === "none") return content;
|
|
3657
|
+
if (family === "js") return maskJs(content);
|
|
3658
|
+
return maskSimple(content, family);
|
|
3659
|
+
};
|
|
3660
|
+
const maskJs = (content) => {
|
|
3661
|
+
const out = content.split("");
|
|
3662
|
+
const len = content.length;
|
|
3663
|
+
const tplStack = [];
|
|
3664
|
+
let i = 0;
|
|
3665
|
+
const mask = (start, end) => {
|
|
3666
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
3667
|
+
};
|
|
3668
|
+
while (i < len) {
|
|
3669
|
+
const c = content[i];
|
|
3670
|
+
const next = content[i + 1];
|
|
3671
|
+
if (tplStack.length > 0) {
|
|
3672
|
+
if (c === "{") {
|
|
3673
|
+
tplStack[tplStack.length - 1]++;
|
|
3674
|
+
i++;
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
if (c === "}") {
|
|
3678
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
3679
|
+
tplStack.pop();
|
|
3680
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3681
|
+
mask(i + 1, scan.maskEnd);
|
|
3682
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3683
|
+
i = scan.resumeAt;
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
tplStack[tplStack.length - 1]--;
|
|
3687
|
+
i++;
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
if (c === "\"" || c === "'") {
|
|
3691
|
+
const strStart = i;
|
|
3692
|
+
i = consumeQuotedString(content, i, c);
|
|
3693
|
+
mask(strStart + 1, i - 1);
|
|
3694
|
+
continue;
|
|
3695
|
+
}
|
|
3696
|
+
if (c === "`") {
|
|
3697
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3698
|
+
mask(i + 1, scan.maskEnd);
|
|
3699
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3700
|
+
i = scan.resumeAt;
|
|
3701
|
+
continue;
|
|
3702
|
+
}
|
|
3703
|
+
if (c === "/" && next === "/") {
|
|
3704
|
+
const strStart = i;
|
|
3705
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3706
|
+
mask(strStart, i);
|
|
3707
|
+
continue;
|
|
3708
|
+
}
|
|
3709
|
+
if (c === "/" && next === "*") {
|
|
3710
|
+
const strStart = i;
|
|
3711
|
+
i += 2;
|
|
3712
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3713
|
+
if (i < len - 1) i += 2;
|
|
3714
|
+
mask(strStart, i);
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
i++;
|
|
3718
|
+
continue;
|
|
3719
|
+
}
|
|
3720
|
+
if (c === "\"" || c === "'") {
|
|
3721
|
+
const strStart = i;
|
|
3722
|
+
i = consumeQuotedString(content, i, c);
|
|
3723
|
+
mask(strStart + 1, i - 1);
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3726
|
+
if (c === "`") {
|
|
3727
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
3728
|
+
mask(i + 1, scan.maskEnd);
|
|
3729
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
3730
|
+
i = scan.resumeAt;
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (c === "/" && next === "/") {
|
|
3734
|
+
const strStart = i;
|
|
3735
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3736
|
+
mask(strStart, i);
|
|
3737
|
+
continue;
|
|
3738
|
+
}
|
|
3739
|
+
if (c === "/" && next === "*") {
|
|
3740
|
+
const strStart = i;
|
|
3741
|
+
i += 2;
|
|
3742
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3743
|
+
if (i < len - 1) i += 2;
|
|
3744
|
+
mask(strStart, i);
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
i++;
|
|
3748
|
+
}
|
|
3749
|
+
return out.join("");
|
|
3750
|
+
};
|
|
3751
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
3752
|
+
const len = content.length;
|
|
3753
|
+
let i = start + 1;
|
|
3754
|
+
while (i < len) {
|
|
3755
|
+
const c = content[i];
|
|
3756
|
+
if (c === "\\" && i + 1 < len) {
|
|
3757
|
+
i += 2;
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
if (c === quote) return i + 1;
|
|
3761
|
+
if (c === "\n") return i;
|
|
3762
|
+
i++;
|
|
3763
|
+
}
|
|
3764
|
+
return i;
|
|
3765
|
+
};
|
|
3766
|
+
const consumeTemplateString = (content, start) => {
|
|
3767
|
+
const len = content.length;
|
|
3768
|
+
let i = start;
|
|
3769
|
+
while (i < len) {
|
|
3770
|
+
const c = content[i];
|
|
3771
|
+
if (c === "\\" && i + 1 < len) {
|
|
3772
|
+
i += 2;
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
if (c === "`") return {
|
|
3776
|
+
maskEnd: i,
|
|
3777
|
+
resumeAt: i + 1,
|
|
3778
|
+
openedInterp: false
|
|
3779
|
+
};
|
|
3780
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
3781
|
+
maskEnd: i,
|
|
3782
|
+
resumeAt: i + 2,
|
|
3783
|
+
openedInterp: true
|
|
3784
|
+
};
|
|
3785
|
+
i++;
|
|
3786
|
+
}
|
|
3787
|
+
return {
|
|
3788
|
+
maskEnd: i,
|
|
3789
|
+
resumeAt: i,
|
|
3790
|
+
openedInterp: false
|
|
3791
|
+
};
|
|
3792
|
+
};
|
|
3793
|
+
const maskSimple = (content, family) => {
|
|
3794
|
+
const out = content.split("");
|
|
3795
|
+
const len = content.length;
|
|
3796
|
+
let i = 0;
|
|
3797
|
+
const mask = (start, end) => {
|
|
3798
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
3799
|
+
};
|
|
3800
|
+
while (i < len) {
|
|
3801
|
+
const c = content[i];
|
|
3802
|
+
const next = content[i + 1];
|
|
3803
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
3804
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
3805
|
+
const triple = c + c + c;
|
|
3806
|
+
const end = content.indexOf(triple, i + 3);
|
|
3807
|
+
const stop = end === -1 ? len : end + 3;
|
|
3808
|
+
mask(i + 3, stop - 3);
|
|
3809
|
+
i = stop;
|
|
3810
|
+
continue;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
if (c === "\"" || c === "'") {
|
|
3814
|
+
const strStart = i;
|
|
3815
|
+
i = consumeQuotedString(content, i, c);
|
|
3816
|
+
mask(strStart + 1, i - 1);
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
3820
|
+
const strStart = i;
|
|
3821
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3822
|
+
mask(strStart, i);
|
|
3823
|
+
continue;
|
|
3824
|
+
}
|
|
3825
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
3826
|
+
const strStart = i;
|
|
3827
|
+
while (i < len && content[i] !== "\n") i++;
|
|
3828
|
+
mask(strStart, i);
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
3832
|
+
const strStart = i;
|
|
3833
|
+
i += 2;
|
|
3834
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
3835
|
+
if (i < len - 1) i += 2;
|
|
3836
|
+
mask(strStart, i);
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
i++;
|
|
3840
|
+
}
|
|
3841
|
+
return out.join("");
|
|
3842
|
+
};
|
|
3843
|
+
|
|
3584
3844
|
//#endregion
|
|
3585
3845
|
//#region src/engines/security/risky.ts
|
|
3586
3846
|
const ev = "eval";
|
|
@@ -3710,12 +3970,13 @@ const detectRiskyConstructs = async (context) => {
|
|
|
3710
3970
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
3711
3971
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
3712
3972
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
3973
|
+
const masked = maskStringsAndComments(content, ext);
|
|
3713
3974
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
3714
3975
|
if (!extensions.includes(ext)) continue;
|
|
3715
3976
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
3716
3977
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3717
3978
|
let match;
|
|
3718
|
-
while ((match = regex.exec(
|
|
3979
|
+
while ((match = regex.exec(masked)) !== null) {
|
|
3719
3980
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3720
3981
|
if (name === "innerhtml") {
|
|
3721
3982
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
@@ -4438,7 +4699,7 @@ const discoverProject = async (directory) => {
|
|
|
4438
4699
|
//#region src/utils/git.ts
|
|
4439
4700
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
4440
4701
|
const getChangedFiles = (cwd, base) => {
|
|
4441
|
-
const
|
|
4702
|
+
const diff = spawnSync("git", [
|
|
4442
4703
|
"diff",
|
|
4443
4704
|
"--name-only",
|
|
4444
4705
|
"--diff-filter=ACMR",
|
|
@@ -4448,8 +4709,22 @@ const getChangedFiles = (cwd, base) => {
|
|
|
4448
4709
|
encoding: "utf-8",
|
|
4449
4710
|
maxBuffer: MAX_BUFFER
|
|
4450
4711
|
});
|
|
4451
|
-
if (
|
|
4452
|
-
|
|
4712
|
+
if (diff.error || diff.status !== 0) return [];
|
|
4713
|
+
const untracked = spawnSync("git", [
|
|
4714
|
+
"ls-files",
|
|
4715
|
+
"--others",
|
|
4716
|
+
"--exclude-standard"
|
|
4717
|
+
], {
|
|
4718
|
+
cwd,
|
|
4719
|
+
encoding: "utf-8",
|
|
4720
|
+
maxBuffer: MAX_BUFFER
|
|
4721
|
+
});
|
|
4722
|
+
const names = /* @__PURE__ */ new Set();
|
|
4723
|
+
for (const line of diff.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
4724
|
+
if (!untracked.error && untracked.status === 0) {
|
|
4725
|
+
for (const line of untracked.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
4726
|
+
}
|
|
4727
|
+
return Array.from(names).map((f) => path.resolve(cwd, f));
|
|
4453
4728
|
};
|
|
4454
4729
|
const getStagedFiles = (cwd) => {
|
|
4455
4730
|
const result = spawnSync("git", [
|
|
@@ -4472,7 +4747,7 @@ const getStagedFiles = (cwd) => {
|
|
|
4472
4747
|
* Application version — injected at build time by tsdown from package.json.
|
|
4473
4748
|
* The fallback should always match the "version" field in package.json.
|
|
4474
4749
|
*/
|
|
4475
|
-
const APP_VERSION = "0.
|
|
4750
|
+
const APP_VERSION = "0.6.0";
|
|
4476
4751
|
|
|
4477
4752
|
//#endregion
|
|
4478
4753
|
//#region src/utils/telemetry.ts
|
|
@@ -6071,42 +6346,46 @@ const runExpoDoctor = async (context) => {
|
|
|
6071
6346
|
|
|
6072
6347
|
//#endregion
|
|
6073
6348
|
//#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
|
-
};
|
|
6349
|
+
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
6350
|
+
const AUDIT_TIMEOUT = 60 * 1e3;
|
|
6351
|
+
const detectPackageManager = (rootDirectory) => {
|
|
6352
|
+
if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
|
|
6353
|
+
if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
|
|
6083
6354
|
return null;
|
|
6084
6355
|
};
|
|
6085
|
-
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
6086
6356
|
const fixDependencyAudit = async (context, onProgress) => {
|
|
6087
|
-
const
|
|
6088
|
-
if (!
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6357
|
+
const pm = detectPackageManager(context.rootDirectory);
|
|
6358
|
+
if (!pm) return;
|
|
6359
|
+
if (pm === "npm") {
|
|
6360
|
+
await runNpmAuditFix(context.rootDirectory, onProgress);
|
|
6361
|
+
await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6362
|
+
return;
|
|
6363
|
+
}
|
|
6364
|
+
if (await tryPnpmOverrides(context.rootDirectory, onProgress)) return;
|
|
6365
|
+
if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json"))) {
|
|
6366
|
+
await runNpmAuditFix(context.rootDirectory, onProgress);
|
|
6367
|
+
await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
onProgress?.("Dependency audit fixes · skipping (pnpm audit unavailable and no package-lock.json for npm fallback)");
|
|
6371
|
+
};
|
|
6372
|
+
const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
6373
|
+
onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
|
|
6374
|
+
const result = await runSubprocess("npm", ["audit", "fix"], {
|
|
6375
|
+
cwd: rootDir,
|
|
6092
6376
|
timeout: INSTALL_TIMEOUT
|
|
6093
6377
|
});
|
|
6094
|
-
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error(
|
|
6095
|
-
onProgress?.(
|
|
6096
|
-
const installResult = await runSubprocess(
|
|
6097
|
-
cwd:
|
|
6378
|
+
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
|
|
6379
|
+
onProgress?.("Dependency audit fixes · running npm install");
|
|
6380
|
+
const installResult = await runSubprocess("npm", ["install"], {
|
|
6381
|
+
cwd: rootDir,
|
|
6098
6382
|
timeout: INSTALL_TIMEOUT
|
|
6099
6383
|
});
|
|
6100
|
-
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout ||
|
|
6101
|
-
if (auditFix.command === "npm") await tryNpmOverrides(context.rootDirectory, onProgress);
|
|
6384
|
+
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
|
|
6102
6385
|
};
|
|
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) => {
|
|
6386
|
+
const fetchLatestVersion = async (rootDir, pkgName, pm) => {
|
|
6108
6387
|
try {
|
|
6109
|
-
const result = await runSubprocess(
|
|
6388
|
+
const result = await runSubprocess(pm, [
|
|
6110
6389
|
"view",
|
|
6111
6390
|
pkgName,
|
|
6112
6391
|
"version",
|
|
@@ -6120,11 +6399,11 @@ const fetchLatestVersion = async (rootDir, pkgName) => {
|
|
|
6120
6399
|
return null;
|
|
6121
6400
|
}
|
|
6122
6401
|
};
|
|
6123
|
-
const collectOverrides = async (rootDir, vulnerabilities) => {
|
|
6402
|
+
const collectOverrides = async (rootDir, vulnerabilities, pm) => {
|
|
6124
6403
|
const overrides = {};
|
|
6125
6404
|
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
6126
6405
|
if (vuln.fixAvailable !== false || !vuln.range) continue;
|
|
6127
|
-
const latest = await fetchLatestVersion(rootDir, pkgName);
|
|
6406
|
+
const latest = await fetchLatestVersion(rootDir, pkgName, pm);
|
|
6128
6407
|
if (latest) overrides[pkgName] = latest;
|
|
6129
6408
|
}
|
|
6130
6409
|
return overrides;
|
|
@@ -6133,12 +6412,12 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6133
6412
|
try {
|
|
6134
6413
|
const auditResult = await runSubprocess("npm", ["audit", "--json"], {
|
|
6135
6414
|
cwd: rootDir,
|
|
6136
|
-
timeout:
|
|
6415
|
+
timeout: AUDIT_TIMEOUT
|
|
6137
6416
|
});
|
|
6138
6417
|
if (!auditResult.stdout) return;
|
|
6139
6418
|
const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
|
|
6140
6419
|
if (!vulnerabilities) return;
|
|
6141
|
-
const overrides = await collectOverrides(rootDir, vulnerabilities);
|
|
6420
|
+
const overrides = await collectOverrides(rootDir, vulnerabilities, "npm");
|
|
6142
6421
|
if (Object.keys(overrides).length === 0) return;
|
|
6143
6422
|
const pkgPath = path.join(rootDir, "package.json");
|
|
6144
6423
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -6146,7 +6425,7 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6146
6425
|
...pkg.overrides || {},
|
|
6147
6426
|
...overrides
|
|
6148
6427
|
};
|
|
6149
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)
|
|
6428
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
6150
6429
|
onProgress?.("Dependency audit fixes · applying npm overrides (npm install)");
|
|
6151
6430
|
await runSubprocess("npm", ["install"], {
|
|
6152
6431
|
cwd: rootDir,
|
|
@@ -6154,6 +6433,70 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
6154
6433
|
});
|
|
6155
6434
|
} catch {}
|
|
6156
6435
|
};
|
|
6436
|
+
const patchedRangeToVersion = (patched) => {
|
|
6437
|
+
const match = patched.match(/^\s*>=?\s*([0-9]+\.[0-9]+\.[0-9]+[^\s]*)/);
|
|
6438
|
+
return match ? `^${match[1]}` : null;
|
|
6439
|
+
};
|
|
6440
|
+
const overrideKey = (name, vulnerable, patched) => {
|
|
6441
|
+
if (vulnerable && vulnerable.trim().length > 0 && !/^\*$/.test(vulnerable.trim())) return `${name}@${vulnerable.trim()}`;
|
|
6442
|
+
const first = patched.match(/([0-9]+\.[0-9]+\.[0-9]+)/)?.[1];
|
|
6443
|
+
return first ? `${name}@<${first}` : name;
|
|
6444
|
+
};
|
|
6445
|
+
const collectPnpmOverrides = (advisories) => {
|
|
6446
|
+
const overrides = {};
|
|
6447
|
+
for (const adv of Object.values(advisories)) {
|
|
6448
|
+
if (!adv.module_name || !adv.patched_versions) continue;
|
|
6449
|
+
const target = patchedRangeToVersion(adv.patched_versions);
|
|
6450
|
+
if (!target) continue;
|
|
6451
|
+
const key = overrideKey(adv.module_name, adv.vulnerable_versions, adv.patched_versions);
|
|
6452
|
+
overrides[key] = target;
|
|
6453
|
+
}
|
|
6454
|
+
return overrides;
|
|
6455
|
+
};
|
|
6456
|
+
const isPnpmAuditRetired = (stdout, stderr) => {
|
|
6457
|
+
const haystack = `${stdout}\n${stderr}`.toLowerCase();
|
|
6458
|
+
return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
|
|
6459
|
+
};
|
|
6460
|
+
const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
6461
|
+
onProgress?.("Dependency audit fixes · running pnpm audit");
|
|
6462
|
+
const auditResult = await runSubprocess("pnpm", ["audit", "--json"], {
|
|
6463
|
+
cwd: rootDir,
|
|
6464
|
+
timeout: AUDIT_TIMEOUT
|
|
6465
|
+
});
|
|
6466
|
+
if (!auditResult.stdout) {
|
|
6467
|
+
if (isPnpmAuditRetired(auditResult.stdout ?? "", auditResult.stderr ?? "")) return false;
|
|
6468
|
+
return auditResult.exitCode === 0;
|
|
6469
|
+
}
|
|
6470
|
+
let parsed;
|
|
6471
|
+
try {
|
|
6472
|
+
parsed = JSON.parse(auditResult.stdout);
|
|
6473
|
+
} catch {
|
|
6474
|
+
if (auditResult.exitCode !== 0 || isPnpmAuditRetired(auditResult.stdout, auditResult.stderr ?? "")) return false;
|
|
6475
|
+
return true;
|
|
6476
|
+
}
|
|
6477
|
+
const advisories = parsed.advisories;
|
|
6478
|
+
if (!advisories || Object.keys(advisories).length === 0) return true;
|
|
6479
|
+
const overrides = collectPnpmOverrides(advisories);
|
|
6480
|
+
if (Object.keys(overrides).length === 0) return true;
|
|
6481
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
6482
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
6483
|
+
const pnpmBlock = pkg.pnpm ?? {};
|
|
6484
|
+
const existing = pnpmBlock.overrides ?? {};
|
|
6485
|
+
pkg.pnpm = {
|
|
6486
|
+
...pnpmBlock,
|
|
6487
|
+
overrides: {
|
|
6488
|
+
...existing,
|
|
6489
|
+
...overrides
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
6493
|
+
onProgress?.("Dependency audit fixes · applying pnpm overrides (pnpm install)");
|
|
6494
|
+
await runSubprocess("pnpm", ["install"], {
|
|
6495
|
+
cwd: rootDir,
|
|
6496
|
+
timeout: INSTALL_TIMEOUT
|
|
6497
|
+
});
|
|
6498
|
+
return true;
|
|
6499
|
+
};
|
|
6157
6500
|
const fixExpoDependencies = async (context, onProgress) => {
|
|
6158
6501
|
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
6159
6502
|
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
@@ -6268,8 +6611,9 @@ const runFormattingStep = async (deps) => {
|
|
|
6268
6611
|
const runForceSteps = async (deps) => {
|
|
6269
6612
|
if (!deps.force) return;
|
|
6270
6613
|
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.
|
|
6614
|
+
const railUpdate = (label) => deps.rail.setActiveLabel(label);
|
|
6615
|
+
if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
|
|
6616
|
+
if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
|
|
6273
6617
|
};
|
|
6274
6618
|
|
|
6275
6619
|
//#endregion
|
|
@@ -6433,6 +6777,1244 @@ const fixCommand = async (directory, config, options = {
|
|
|
6433
6777
|
}
|
|
6434
6778
|
};
|
|
6435
6779
|
|
|
6780
|
+
//#endregion
|
|
6781
|
+
//#region src/hooks/feedback.ts
|
|
6782
|
+
const MAX_FINDINGS = 20;
|
|
6783
|
+
const toFinding = (d, rootDirectory) => {
|
|
6784
|
+
if (d.severity !== "error" && d.severity !== "warning") return null;
|
|
6785
|
+
const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
|
|
6786
|
+
return {
|
|
6787
|
+
ruleId: d.rule,
|
|
6788
|
+
severity: d.severity,
|
|
6789
|
+
category: d.category,
|
|
6790
|
+
file,
|
|
6791
|
+
line: d.line,
|
|
6792
|
+
col: d.column || void 0,
|
|
6793
|
+
message: d.message
|
|
6794
|
+
};
|
|
6795
|
+
};
|
|
6796
|
+
const buildNextSteps = (findings) => {
|
|
6797
|
+
const steps = [];
|
|
6798
|
+
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
6799
|
+
if (errorCount > 0) steps.push(`Fix ${errorCount} error${errorCount === 1 ? "" : "s"} before the next turn.`);
|
|
6800
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6801
|
+
for (const f of findings) {
|
|
6802
|
+
const list = byFile.get(f.file) ?? [];
|
|
6803
|
+
list.push(f);
|
|
6804
|
+
byFile.set(f.file, list);
|
|
6805
|
+
}
|
|
6806
|
+
for (const [file, list] of Array.from(byFile.entries()).slice(0, 3)) {
|
|
6807
|
+
const lines = list.map((f) => f.line).slice(0, 3).join(", ");
|
|
6808
|
+
steps.push(`Address ${list.length} finding${list.length === 1 ? "" : "s"} in ${file} (line${list.length === 1 ? "" : "s"} ${lines}).`);
|
|
6809
|
+
}
|
|
6810
|
+
return steps;
|
|
6811
|
+
};
|
|
6812
|
+
const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
6813
|
+
const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
|
|
6814
|
+
const capped = all.slice(0, MAX_FINDINGS);
|
|
6815
|
+
const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
|
|
6816
|
+
const counts = {
|
|
6817
|
+
error: diagnostics.filter((d) => d.severity === "error").length,
|
|
6818
|
+
warning: diagnostics.filter((d) => d.severity === "warning").length,
|
|
6819
|
+
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
6820
|
+
total: all.length
|
|
6821
|
+
};
|
|
6822
|
+
return {
|
|
6823
|
+
schema: "aislop.hook.v1",
|
|
6824
|
+
score,
|
|
6825
|
+
baseline,
|
|
6826
|
+
regressed: typeof baseline === "number" ? score < baseline : false,
|
|
6827
|
+
counts,
|
|
6828
|
+
findings: capped,
|
|
6829
|
+
elided,
|
|
6830
|
+
nextSteps: buildNextSteps(capped)
|
|
6831
|
+
};
|
|
6832
|
+
};
|
|
6833
|
+
|
|
6834
|
+
//#endregion
|
|
6835
|
+
//#region src/hooks/io/scan-lock.ts
|
|
6836
|
+
const LOCK_DIR = ".aislop";
|
|
6837
|
+
const LOCK_FILE = "hook.lock";
|
|
6838
|
+
const STALE_MS = 3e4;
|
|
6839
|
+
const lockPath = (cwd) => path.join(cwd, LOCK_DIR, LOCK_FILE);
|
|
6840
|
+
const readLock = (target) => {
|
|
6841
|
+
try {
|
|
6842
|
+
const raw = fs.readFileSync(target, "utf-8");
|
|
6843
|
+
const parsed = JSON.parse(raw);
|
|
6844
|
+
if (typeof parsed.pid !== "number" || typeof parsed.ts !== "number") return null;
|
|
6845
|
+
return parsed;
|
|
6846
|
+
} catch {
|
|
6847
|
+
return null;
|
|
6848
|
+
}
|
|
6849
|
+
};
|
|
6850
|
+
const acquireHookLock = (cwd) => {
|
|
6851
|
+
const target = lockPath(cwd);
|
|
6852
|
+
const existing = readLock(target);
|
|
6853
|
+
if (existing && Date.now() - existing.ts < STALE_MS) return null;
|
|
6854
|
+
try {
|
|
6855
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
6856
|
+
fs.writeFileSync(target, JSON.stringify({
|
|
6857
|
+
pid: process.pid,
|
|
6858
|
+
ts: Date.now()
|
|
6859
|
+
}));
|
|
6860
|
+
} catch {
|
|
6861
|
+
return null;
|
|
6862
|
+
}
|
|
6863
|
+
return () => {
|
|
6864
|
+
try {
|
|
6865
|
+
if (readLock(target)?.pid === process.pid) fs.unlinkSync(target);
|
|
6866
|
+
} catch {}
|
|
6867
|
+
};
|
|
6868
|
+
};
|
|
6869
|
+
|
|
6870
|
+
//#endregion
|
|
6871
|
+
//#region src/hooks/io/scoped-scan.ts
|
|
6872
|
+
const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f)).filter((p) => {
|
|
6873
|
+
try {
|
|
6874
|
+
return fs.statSync(p).isFile();
|
|
6875
|
+
} catch {
|
|
6876
|
+
return false;
|
|
6877
|
+
}
|
|
6878
|
+
});
|
|
6879
|
+
const resolveHookFiles = (cwd, files) => {
|
|
6880
|
+
const direct = existingAbsolutePaths(cwd, files);
|
|
6881
|
+
if (direct.length > 0) return direct;
|
|
6882
|
+
return existingAbsolutePaths(cwd, getChangedFiles(cwd));
|
|
6883
|
+
};
|
|
6884
|
+
const runScopedScan = async (cwd, filePaths) => {
|
|
6885
|
+
const project = await discoverProject(cwd);
|
|
6886
|
+
const config = loadConfig(cwd);
|
|
6887
|
+
const configDir = findConfigDir(project.rootDirectory);
|
|
6888
|
+
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
6889
|
+
const diagnostics = (await runEngines({
|
|
6890
|
+
rootDirectory: project.rootDirectory,
|
|
6891
|
+
languages: project.languages,
|
|
6892
|
+
frameworks: project.frameworks,
|
|
6893
|
+
files: filterProjectFiles(project.rootDirectory, filePaths),
|
|
6894
|
+
installedTools: project.installedTools,
|
|
6895
|
+
config: {
|
|
6896
|
+
quality: config.quality,
|
|
6897
|
+
security: {
|
|
6898
|
+
audit: false,
|
|
6899
|
+
auditTimeout: 0
|
|
6900
|
+
},
|
|
6901
|
+
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6902
|
+
}
|
|
6903
|
+
}, {
|
|
6904
|
+
format: config.engines.format,
|
|
6905
|
+
lint: config.engines.lint,
|
|
6906
|
+
"code-quality": config.engines["code-quality"],
|
|
6907
|
+
"ai-slop": config.engines["ai-slop"],
|
|
6908
|
+
architecture: config.engines.architecture,
|
|
6909
|
+
security: false
|
|
6910
|
+
})).flatMap((r) => r.diagnostics);
|
|
6911
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6912
|
+
return {
|
|
6913
|
+
diagnostics,
|
|
6914
|
+
score,
|
|
6915
|
+
rootDirectory: project.rootDirectory
|
|
6916
|
+
};
|
|
6917
|
+
};
|
|
6918
|
+
|
|
6919
|
+
//#endregion
|
|
6920
|
+
//#region src/hooks/io/atomic-write.ts
|
|
6921
|
+
const atomicWrite = (targetPath, content) => {
|
|
6922
|
+
const dir = path.dirname(targetPath);
|
|
6923
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
6924
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
6925
|
+
const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
|
|
6926
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
6927
|
+
fs.renameSync(tmp, targetPath);
|
|
6928
|
+
};
|
|
6929
|
+
const readIfExists = (targetPath) => {
|
|
6930
|
+
try {
|
|
6931
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
6932
|
+
} catch {
|
|
6933
|
+
return null;
|
|
6934
|
+
}
|
|
6935
|
+
};
|
|
6936
|
+
|
|
6937
|
+
//#endregion
|
|
6938
|
+
//#region src/hooks/quality-gate/baseline.ts
|
|
6939
|
+
const BASELINE_REL = path.join(".aislop", "baseline.json");
|
|
6940
|
+
const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
|
|
6941
|
+
const readBaseline = (cwd) => {
|
|
6942
|
+
const raw = readIfExists(baselinePath(cwd));
|
|
6943
|
+
if (!raw) return null;
|
|
6944
|
+
try {
|
|
6945
|
+
const parsed = JSON.parse(raw);
|
|
6946
|
+
if (parsed.schema !== "aislop.baseline.v1") return null;
|
|
6947
|
+
return parsed;
|
|
6948
|
+
} catch {
|
|
6949
|
+
return null;
|
|
6950
|
+
}
|
|
6951
|
+
};
|
|
6952
|
+
const writeBaseline = (cwd, baseline) => {
|
|
6953
|
+
const target = baselinePath(cwd);
|
|
6954
|
+
atomicWrite(target, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
6955
|
+
return target;
|
|
6956
|
+
};
|
|
6957
|
+
const captureBaseline = async (cwd) => {
|
|
6958
|
+
const project = await discoverProject(cwd);
|
|
6959
|
+
const config = loadConfig(cwd);
|
|
6960
|
+
const results = await runEngines({
|
|
6961
|
+
rootDirectory: project.rootDirectory,
|
|
6962
|
+
languages: project.languages,
|
|
6963
|
+
frameworks: project.frameworks,
|
|
6964
|
+
files: [],
|
|
6965
|
+
installedTools: project.installedTools,
|
|
6966
|
+
config: {
|
|
6967
|
+
quality: config.quality,
|
|
6968
|
+
security: {
|
|
6969
|
+
audit: false,
|
|
6970
|
+
auditTimeout: 0
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
}, {
|
|
6974
|
+
format: config.engines.format,
|
|
6975
|
+
lint: config.engines.lint,
|
|
6976
|
+
"code-quality": config.engines["code-quality"],
|
|
6977
|
+
"ai-slop": config.engines["ai-slop"],
|
|
6978
|
+
architecture: config.engines.architecture,
|
|
6979
|
+
security: false
|
|
6980
|
+
});
|
|
6981
|
+
const diagnostics = results.flatMap((r) => r.diagnostics);
|
|
6982
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6983
|
+
const byEngine = {};
|
|
6984
|
+
for (const r of results) {
|
|
6985
|
+
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
6986
|
+
byEngine[r.engine] = engineScore;
|
|
6987
|
+
}
|
|
6988
|
+
const target = writeBaseline(cwd, {
|
|
6989
|
+
schema: "aislop.baseline.v1",
|
|
6990
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6991
|
+
score,
|
|
6992
|
+
byEngine,
|
|
6993
|
+
fileCount: project.sourceFileCount
|
|
6994
|
+
});
|
|
6995
|
+
return {
|
|
6996
|
+
score,
|
|
6997
|
+
fileCount: project.sourceFileCount,
|
|
6998
|
+
path: target
|
|
6999
|
+
};
|
|
7000
|
+
};
|
|
7001
|
+
const appendSessionFiles = (cwd, files) => {
|
|
7002
|
+
if (files.length === 0) return;
|
|
7003
|
+
const target = path.join(cwd, ".aislop", "session.jsonl");
|
|
7004
|
+
try {
|
|
7005
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
7006
|
+
const line = `${JSON.stringify({
|
|
7007
|
+
ts: Date.now(),
|
|
7008
|
+
files
|
|
7009
|
+
})}\n`;
|
|
7010
|
+
fs.appendFileSync(target, line);
|
|
7011
|
+
} catch {}
|
|
7012
|
+
};
|
|
7013
|
+
const readSessionFiles = (cwd) => {
|
|
7014
|
+
const raw = readIfExists(path.join(cwd, ".aislop", "session.jsonl"));
|
|
7015
|
+
if (!raw) return [];
|
|
7016
|
+
const files = /* @__PURE__ */ new Set();
|
|
7017
|
+
for (const line of raw.split("\n")) {
|
|
7018
|
+
if (!line.trim()) continue;
|
|
7019
|
+
try {
|
|
7020
|
+
const entry = JSON.parse(line);
|
|
7021
|
+
for (const f of entry.files ?? []) files.add(f);
|
|
7022
|
+
} catch {}
|
|
7023
|
+
}
|
|
7024
|
+
return Array.from(files);
|
|
7025
|
+
};
|
|
7026
|
+
const clearSessionFiles = (cwd) => {
|
|
7027
|
+
const target = path.join(cwd, ".aislop", "session.jsonl");
|
|
7028
|
+
try {
|
|
7029
|
+
fs.unlinkSync(target);
|
|
7030
|
+
} catch {}
|
|
7031
|
+
};
|
|
7032
|
+
|
|
7033
|
+
//#endregion
|
|
7034
|
+
//#region src/hooks/adapters/claude.ts
|
|
7035
|
+
const extractFiles$2 = (stdin) => {
|
|
7036
|
+
const files = /* @__PURE__ */ new Set();
|
|
7037
|
+
const input = stdin.tool_input ?? {};
|
|
7038
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7039
|
+
if (Array.isArray(input.edits)) {
|
|
7040
|
+
for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7041
|
+
}
|
|
7042
|
+
return Array.from(files);
|
|
7043
|
+
};
|
|
7044
|
+
const parseClaudeStdin = (raw) => {
|
|
7045
|
+
if (!raw.trim()) return {};
|
|
7046
|
+
try {
|
|
7047
|
+
return JSON.parse(raw);
|
|
7048
|
+
} catch {
|
|
7049
|
+
return {};
|
|
7050
|
+
}
|
|
7051
|
+
};
|
|
7052
|
+
const readStdin$2 = async () => {
|
|
7053
|
+
if (process.stdin.isTTY) return "";
|
|
7054
|
+
const chunks = [];
|
|
7055
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7056
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7057
|
+
};
|
|
7058
|
+
const renderClaudeOutput = (additional, block) => {
|
|
7059
|
+
const out = { hookSpecificOutput: {
|
|
7060
|
+
hookEventName: "PostToolUse",
|
|
7061
|
+
additionalContext: additional
|
|
7062
|
+
} };
|
|
7063
|
+
if (block) {
|
|
7064
|
+
out.decision = "block";
|
|
7065
|
+
out.reason = block.reason;
|
|
7066
|
+
}
|
|
7067
|
+
return out;
|
|
7068
|
+
};
|
|
7069
|
+
const runClaudeHook = async (deps = {}) => {
|
|
7070
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
7071
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7072
|
+
const input = parseClaudeStdin(await getStdin());
|
|
7073
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7074
|
+
const files = resolveHookFiles(cwd, extractFiles$2(input));
|
|
7075
|
+
if (files.length === 0) return 0;
|
|
7076
|
+
const release = acquireHookLock(cwd);
|
|
7077
|
+
if (!release) return 0;
|
|
7078
|
+
try {
|
|
7079
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7080
|
+
const baseline = readBaseline(cwd);
|
|
7081
|
+
appendSessionFiles(cwd, files);
|
|
7082
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline?.score);
|
|
7083
|
+
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
7084
|
+
write(JSON.stringify(envelope));
|
|
7085
|
+
return 0;
|
|
7086
|
+
} catch {
|
|
7087
|
+
return 0;
|
|
7088
|
+
} finally {
|
|
7089
|
+
release();
|
|
7090
|
+
}
|
|
7091
|
+
};
|
|
7092
|
+
const parseClaudeStopStdin = (raw) => {
|
|
7093
|
+
if (!raw.trim()) return {};
|
|
7094
|
+
try {
|
|
7095
|
+
return JSON.parse(raw);
|
|
7096
|
+
} catch {
|
|
7097
|
+
return {};
|
|
7098
|
+
}
|
|
7099
|
+
};
|
|
7100
|
+
const runClaudeStopHook = async (deps = {}) => {
|
|
7101
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
7102
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7103
|
+
const input = parseClaudeStopStdin(await getStdin());
|
|
7104
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7105
|
+
if (input.stop_hook_active) return 0;
|
|
7106
|
+
const baseline = readBaseline(cwd);
|
|
7107
|
+
if (!baseline) return 0;
|
|
7108
|
+
const sessionFiles = readSessionFiles(cwd);
|
|
7109
|
+
if (sessionFiles.length === 0) return 0;
|
|
7110
|
+
const release = acquireHookLock(cwd);
|
|
7111
|
+
if (!release) return 0;
|
|
7112
|
+
try {
|
|
7113
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
|
|
7114
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline.score);
|
|
7115
|
+
if (!feedback.regressed) {
|
|
7116
|
+
clearSessionFiles(cwd);
|
|
7117
|
+
return 0;
|
|
7118
|
+
}
|
|
7119
|
+
const envelope = renderClaudeOutput(JSON.stringify(feedback), { reason: `aislop: score dropped from ${baseline.score} to ${score}. Fix the ${feedback.counts.total} finding${feedback.counts.total === 1 ? "" : "s"} before finishing.` });
|
|
7120
|
+
write(JSON.stringify(envelope));
|
|
7121
|
+
return 0;
|
|
7122
|
+
} catch {
|
|
7123
|
+
return 0;
|
|
7124
|
+
} finally {
|
|
7125
|
+
release();
|
|
7126
|
+
}
|
|
7127
|
+
};
|
|
7128
|
+
|
|
7129
|
+
//#endregion
|
|
7130
|
+
//#region src/hooks/adapters/cursor.ts
|
|
7131
|
+
const extractFiles$1 = (stdin) => {
|
|
7132
|
+
const files = /* @__PURE__ */ new Set();
|
|
7133
|
+
if (typeof stdin.file_path === "string" && stdin.file_path.length > 0) files.add(stdin.file_path);
|
|
7134
|
+
if (Array.isArray(stdin.edits)) {
|
|
7135
|
+
for (const e of stdin.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7136
|
+
}
|
|
7137
|
+
const input = stdin.tool_input ?? {};
|
|
7138
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7139
|
+
if (Array.isArray(input.edits)) {
|
|
7140
|
+
for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
|
|
7141
|
+
}
|
|
7142
|
+
return Array.from(files);
|
|
7143
|
+
};
|
|
7144
|
+
const parseCursorStdin = (raw) => {
|
|
7145
|
+
if (!raw.trim()) return {};
|
|
7146
|
+
try {
|
|
7147
|
+
return JSON.parse(raw);
|
|
7148
|
+
} catch {
|
|
7149
|
+
return {};
|
|
7150
|
+
}
|
|
7151
|
+
};
|
|
7152
|
+
const renderCursorOutput = (additional, event = "afterFileEdit") => ({ hookSpecificOutput: {
|
|
7153
|
+
hookEventName: event,
|
|
7154
|
+
additionalContext: additional
|
|
7155
|
+
} });
|
|
7156
|
+
const readStdin$1 = async () => {
|
|
7157
|
+
if (process.stdin.isTTY) return "";
|
|
7158
|
+
const chunks = [];
|
|
7159
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7160
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7161
|
+
};
|
|
7162
|
+
const runCursorHook = async (deps = {}) => {
|
|
7163
|
+
const getStdin = deps.stdin ?? readStdin$1;
|
|
7164
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7165
|
+
const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
|
|
7166
|
+
const input = parseCursorStdin(await getStdin());
|
|
7167
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7168
|
+
const files = resolveHookFiles(cwd, extractFiles$1(input));
|
|
7169
|
+
if (files.length === 0) return 0;
|
|
7170
|
+
const release = acquireHookLock(cwd);
|
|
7171
|
+
if (!release) return 0;
|
|
7172
|
+
try {
|
|
7173
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7174
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory);
|
|
7175
|
+
const serialized = JSON.stringify(feedback);
|
|
7176
|
+
write(JSON.stringify(renderCursorOutput(serialized)));
|
|
7177
|
+
writeErr(`${serialized}\n`);
|
|
7178
|
+
return 0;
|
|
7179
|
+
} catch {
|
|
7180
|
+
return 0;
|
|
7181
|
+
} finally {
|
|
7182
|
+
release();
|
|
7183
|
+
}
|
|
7184
|
+
};
|
|
7185
|
+
|
|
7186
|
+
//#endregion
|
|
7187
|
+
//#region src/hooks/adapters/gemini.ts
|
|
7188
|
+
const extractFiles = (stdin) => {
|
|
7189
|
+
const files = /* @__PURE__ */ new Set();
|
|
7190
|
+
const input = stdin.tool_input ?? {};
|
|
7191
|
+
if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
|
|
7192
|
+
if (typeof input.path === "string" && input.path.length > 0) files.add(input.path);
|
|
7193
|
+
return Array.from(files);
|
|
7194
|
+
};
|
|
7195
|
+
const parseGeminiStdin = (raw) => {
|
|
7196
|
+
if (!raw.trim()) return {};
|
|
7197
|
+
try {
|
|
7198
|
+
return JSON.parse(raw);
|
|
7199
|
+
} catch {
|
|
7200
|
+
return {};
|
|
7201
|
+
}
|
|
7202
|
+
};
|
|
7203
|
+
const renderGeminiOutput = (additional) => ({ hookSpecificOutput: {
|
|
7204
|
+
hookEventName: "AfterTool",
|
|
7205
|
+
additionalContext: additional
|
|
7206
|
+
} });
|
|
7207
|
+
const readStdin = async () => {
|
|
7208
|
+
if (process.stdin.isTTY) return "";
|
|
7209
|
+
const chunks = [];
|
|
7210
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7211
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7212
|
+
};
|
|
7213
|
+
const runGeminiHook = async (deps = {}) => {
|
|
7214
|
+
const getStdin = deps.stdin ?? readStdin;
|
|
7215
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7216
|
+
const input = parseGeminiStdin(await getStdin());
|
|
7217
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7218
|
+
const files = resolveHookFiles(cwd, extractFiles(input));
|
|
7219
|
+
if (files.length === 0) return 0;
|
|
7220
|
+
const release = acquireHookLock(cwd);
|
|
7221
|
+
if (!release) return 0;
|
|
7222
|
+
try {
|
|
7223
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7224
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory);
|
|
7225
|
+
write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
|
|
7226
|
+
return 0;
|
|
7227
|
+
} catch {
|
|
7228
|
+
return 0;
|
|
7229
|
+
} finally {
|
|
7230
|
+
release();
|
|
7231
|
+
}
|
|
7232
|
+
};
|
|
7233
|
+
|
|
7234
|
+
//#endregion
|
|
7235
|
+
//#region src/hooks/assets.ts
|
|
7236
|
+
const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
7237
|
+
|
|
7238
|
+
[aislop](https://github.com/scanaislop/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.
|
|
7239
|
+
|
|
7240
|
+
## On every edit
|
|
7241
|
+
|
|
7242
|
+
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.
|
|
7243
|
+
|
|
7244
|
+
## Severity ladder
|
|
7245
|
+
|
|
7246
|
+
- \`error\` — MUST fix this turn.
|
|
7247
|
+
- \`warning\` + \`fixable: true\` — MUST fix this turn.
|
|
7248
|
+
- \`warning\` + \`fixable: false\` — fix if trivially mechanical, otherwise surface in your reply.
|
|
7249
|
+
|
|
7250
|
+
## Rules
|
|
7251
|
+
|
|
7252
|
+
- \`.aislop/config.yaml\` — thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
|
|
7253
|
+
- \`.aislop/rules.yaml\` — project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
|
|
7254
|
+
- Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
|
|
7255
|
+
|
|
7256
|
+
## Principles
|
|
7257
|
+
|
|
7258
|
+
- Do not disable rules to pass the scan. Fix the underlying issue.
|
|
7259
|
+
- If a finding is a false positive, leave it and explain in your reply — do not delete the rule config.
|
|
7260
|
+
- The findings payload includes \`nextSteps[]\` — treat those as your plan for the turn.
|
|
7261
|
+
`;
|
|
7262
|
+
|
|
7263
|
+
//#endregion
|
|
7264
|
+
//#region src/hooks/io/sentinel.ts
|
|
7265
|
+
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
7266
|
+
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
7267
|
+
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
7268
|
+
const renderFence = (body, hash) => [
|
|
7269
|
+
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
7270
|
+
body.trimEnd(),
|
|
7271
|
+
"<!-- aislop:end v1 -->"
|
|
7272
|
+
].join("\n");
|
|
7273
|
+
const upsertMarkdownFence = (existing, body, hash) => {
|
|
7274
|
+
const fenced = renderFence(body, hash);
|
|
7275
|
+
if (existing == null || existing.length === 0) return {
|
|
7276
|
+
nextContent: `${fenced}\n`,
|
|
7277
|
+
replaced: false
|
|
7278
|
+
};
|
|
7279
|
+
const begin = existing.match(BEGIN_RE);
|
|
7280
|
+
const end = existing.match(END_RE);
|
|
7281
|
+
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
7282
|
+
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
7283
|
+
replaced: true
|
|
7284
|
+
};
|
|
7285
|
+
return {
|
|
7286
|
+
nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
|
|
7287
|
+
replaced: false
|
|
7288
|
+
};
|
|
7289
|
+
};
|
|
7290
|
+
|
|
7291
|
+
//#endregion
|
|
7292
|
+
//#region src/hooks/install/types.ts
|
|
7293
|
+
const emptyResult = () => ({
|
|
7294
|
+
wrote: [],
|
|
7295
|
+
skipped: [],
|
|
7296
|
+
planned: []
|
|
7297
|
+
});
|
|
7298
|
+
const applyContent = (result, opts, target, nextContent, summary) => {
|
|
7299
|
+
if (readIfExists(target) === nextContent) {
|
|
7300
|
+
result.skipped.push(target);
|
|
7301
|
+
return;
|
|
7302
|
+
}
|
|
7303
|
+
if (opts.dryRun) {
|
|
7304
|
+
result.planned.push({
|
|
7305
|
+
path: target,
|
|
7306
|
+
summary
|
|
7307
|
+
});
|
|
7308
|
+
return;
|
|
7309
|
+
}
|
|
7310
|
+
atomicWrite(target, nextContent);
|
|
7311
|
+
result.wrote.push(target);
|
|
7312
|
+
};
|
|
7313
|
+
const applyRemoval = (result, opts, target, nextContent) => {
|
|
7314
|
+
const existing = readIfExists(target);
|
|
7315
|
+
if (existing == null) {
|
|
7316
|
+
result.skipped.push(target);
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
7319
|
+
if (existing === (nextContent ?? "")) {
|
|
7320
|
+
result.skipped.push(target);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
if (opts.dryRun) {
|
|
7324
|
+
result.removed.push(target);
|
|
7325
|
+
return;
|
|
7326
|
+
}
|
|
7327
|
+
if (nextContent == null) try {
|
|
7328
|
+
fs.unlinkSync(target);
|
|
7329
|
+
} catch {}
|
|
7330
|
+
else atomicWrite(target, nextContent);
|
|
7331
|
+
result.removed.push(target);
|
|
7332
|
+
};
|
|
7333
|
+
|
|
7334
|
+
//#endregion
|
|
7335
|
+
//#region src/hooks/install/rules-only.ts
|
|
7336
|
+
const installRulesOnly = (opts, paths, summary) => {
|
|
7337
|
+
const result = emptyResult();
|
|
7338
|
+
const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7339
|
+
applyContent(result, opts, paths.rules, next, summary);
|
|
7340
|
+
if (paths.host && paths.marker) {
|
|
7341
|
+
const host = readIfExists(paths.host) ?? "";
|
|
7342
|
+
if (!host.includes(paths.marker)) {
|
|
7343
|
+
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
7344
|
+
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
7345
|
+
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
7346
|
+
} else result.skipped.push(paths.host);
|
|
7347
|
+
}
|
|
7348
|
+
return result;
|
|
7349
|
+
};
|
|
7350
|
+
const uninstallRulesOnly = (opts, paths) => {
|
|
7351
|
+
const result = {
|
|
7352
|
+
removed: [],
|
|
7353
|
+
skipped: []
|
|
7354
|
+
};
|
|
7355
|
+
if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
|
|
7356
|
+
else result.skipped.push(paths.rules);
|
|
7357
|
+
if (paths.host && paths.marker) {
|
|
7358
|
+
const host = readIfExists(paths.host);
|
|
7359
|
+
if (host != null && host.includes(paths.marker)) {
|
|
7360
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7361
|
+
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7362
|
+
} else result.skipped.push(paths.host);
|
|
7363
|
+
}
|
|
7364
|
+
return result;
|
|
7365
|
+
};
|
|
7366
|
+
|
|
7367
|
+
//#endregion
|
|
7368
|
+
//#region src/hooks/install/antigravity.ts
|
|
7369
|
+
const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
|
|
7370
|
+
const installAntigravity = (opts) => {
|
|
7371
|
+
if (opts.scope !== "project") return {
|
|
7372
|
+
wrote: [],
|
|
7373
|
+
skipped: [],
|
|
7374
|
+
planned: [{
|
|
7375
|
+
path: ".agents/rules/antigravity-aislop-rules.md",
|
|
7376
|
+
summary: "Antigravity is project-scope only; pass --project"
|
|
7377
|
+
}]
|
|
7378
|
+
};
|
|
7379
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
|
|
7380
|
+
};
|
|
7381
|
+
const uninstallAntigravity = (opts) => {
|
|
7382
|
+
if (opts.scope !== "project") return {
|
|
7383
|
+
removed: [],
|
|
7384
|
+
skipped: []
|
|
7385
|
+
};
|
|
7386
|
+
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
7387
|
+
};
|
|
7388
|
+
|
|
7389
|
+
//#endregion
|
|
7390
|
+
//#region src/hooks/io/json-patch.ts
|
|
7391
|
+
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
7392
|
+
const isAislopManaged = (x) => typeof x === "object" && x !== null && AISLOP_SENTINEL_KEY in x && x[AISLOP_SENTINEL_KEY] != null;
|
|
7393
|
+
const groupIsAislop = (group) => {
|
|
7394
|
+
if (typeof group !== "object" || group === null) return false;
|
|
7395
|
+
const hooks = group.hooks;
|
|
7396
|
+
if (!Array.isArray(hooks)) return false;
|
|
7397
|
+
return hooks.some((h) => isAislopManaged(h));
|
|
7398
|
+
};
|
|
7399
|
+
const upsertHookGroup = (config, event, group) => {
|
|
7400
|
+
const next = { ...config };
|
|
7401
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
7402
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
|
|
7403
|
+
next.hooks = {
|
|
7404
|
+
...hooks,
|
|
7405
|
+
[event]: [...cleaned, group]
|
|
7406
|
+
};
|
|
7407
|
+
return next;
|
|
7408
|
+
};
|
|
7409
|
+
const upsertFlatHook = (config, event, entry) => {
|
|
7410
|
+
const next = { ...config };
|
|
7411
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
7412
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
|
|
7413
|
+
next.hooks = {
|
|
7414
|
+
...hooks,
|
|
7415
|
+
[event]: [...cleaned, entry]
|
|
7416
|
+
};
|
|
7417
|
+
return next;
|
|
7418
|
+
};
|
|
7419
|
+
const removeAislopEntries = (config, event) => {
|
|
7420
|
+
const next = { ...config };
|
|
7421
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
7422
|
+
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
7423
|
+
const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
|
|
7424
|
+
const removed = existing.length - cleaned.length;
|
|
7425
|
+
const nextHooks = { ...hooks };
|
|
7426
|
+
if (cleaned.length === 0) delete nextHooks[event];
|
|
7427
|
+
else nextHooks[event] = cleaned;
|
|
7428
|
+
if (Object.keys(nextHooks).length === 0) delete next.hooks;
|
|
7429
|
+
else next.hooks = nextHooks;
|
|
7430
|
+
return {
|
|
7431
|
+
next,
|
|
7432
|
+
removed
|
|
7433
|
+
};
|
|
7434
|
+
};
|
|
7435
|
+
|
|
7436
|
+
//#endregion
|
|
7437
|
+
//#region src/hooks/install/claude.ts
|
|
7438
|
+
const resolveClaudePaths = (opts) => {
|
|
7439
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
7440
|
+
return {
|
|
7441
|
+
settings: path.join(root, "settings.json"),
|
|
7442
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
7443
|
+
claudeMd: path.join(root, "CLAUDE.md")
|
|
7444
|
+
};
|
|
7445
|
+
};
|
|
7446
|
+
const buildHookGroup$1 = () => {
|
|
7447
|
+
const hashBody = JSON.stringify({
|
|
7448
|
+
command: "aislop hook claude",
|
|
7449
|
+
matcher: "Edit|Write|MultiEdit"
|
|
7450
|
+
});
|
|
7451
|
+
return {
|
|
7452
|
+
matcher: "Edit|Write|MultiEdit",
|
|
7453
|
+
hooks: [{
|
|
7454
|
+
type: "command",
|
|
7455
|
+
command: "aislop hook claude",
|
|
7456
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7457
|
+
v: 1,
|
|
7458
|
+
managed: true,
|
|
7459
|
+
hash: sentinelHash(hashBody)
|
|
7460
|
+
}
|
|
7461
|
+
}]
|
|
7462
|
+
};
|
|
7463
|
+
};
|
|
7464
|
+
const buildStopHookGroup = () => {
|
|
7465
|
+
const hashBody = JSON.stringify({ command: "aislop hook claude --stop" });
|
|
7466
|
+
return {
|
|
7467
|
+
matcher: "",
|
|
7468
|
+
hooks: [{
|
|
7469
|
+
type: "command",
|
|
7470
|
+
command: "aislop hook claude --stop",
|
|
7471
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7472
|
+
v: 1,
|
|
7473
|
+
managed: true,
|
|
7474
|
+
hash: sentinelHash(hashBody)
|
|
7475
|
+
}
|
|
7476
|
+
}]
|
|
7477
|
+
};
|
|
7478
|
+
};
|
|
7479
|
+
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
7480
|
+
let obj = {};
|
|
7481
|
+
if (existingRaw) try {
|
|
7482
|
+
obj = JSON.parse(existingRaw);
|
|
7483
|
+
} catch {
|
|
7484
|
+
obj = {};
|
|
7485
|
+
}
|
|
7486
|
+
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
7487
|
+
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
7488
|
+
else next = removeAislopEntries(next, "Stop").next;
|
|
7489
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7490
|
+
};
|
|
7491
|
+
const installClaude = (opts) => {
|
|
7492
|
+
const paths = resolveClaudePaths(opts);
|
|
7493
|
+
const result = emptyResult();
|
|
7494
|
+
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
7495
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse hook");
|
|
7496
|
+
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
7497
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
|
|
7498
|
+
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
7499
|
+
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
7500
|
+
const marker = "@AISLOP.md";
|
|
7501
|
+
if (!existingClaudeMd.includes(marker)) {
|
|
7502
|
+
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
7503
|
+
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
7504
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
7505
|
+
} else result.skipped.push(paths.claudeMd);
|
|
7506
|
+
return result;
|
|
7507
|
+
};
|
|
7508
|
+
const uninstallClaude = (opts) => {
|
|
7509
|
+
const paths = resolveClaudePaths({
|
|
7510
|
+
...opts,
|
|
7511
|
+
qualityGate: false
|
|
7512
|
+
});
|
|
7513
|
+
const result = {
|
|
7514
|
+
removed: [],
|
|
7515
|
+
skipped: []
|
|
7516
|
+
};
|
|
7517
|
+
const settingsRaw = readIfExists(paths.settings);
|
|
7518
|
+
if (settingsRaw) {
|
|
7519
|
+
let obj = {};
|
|
7520
|
+
try {
|
|
7521
|
+
obj = JSON.parse(settingsRaw);
|
|
7522
|
+
} catch {
|
|
7523
|
+
obj = {};
|
|
7524
|
+
}
|
|
7525
|
+
const stripped = removeAislopEntries(removeAislopEntries(obj, "PostToolUse").next, "Stop").next;
|
|
7526
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7527
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
7528
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
7529
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7530
|
+
} else result.skipped.push(paths.settings);
|
|
7531
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
7532
|
+
else result.skipped.push(paths.aislopMd);
|
|
7533
|
+
const claudeMd = readIfExists(paths.claudeMd);
|
|
7534
|
+
if (claudeMd != null && claudeMd.includes("@AISLOP.md")) {
|
|
7535
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7536
|
+
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7537
|
+
} else result.skipped.push(paths.claudeMd);
|
|
7538
|
+
return result;
|
|
7539
|
+
};
|
|
7540
|
+
|
|
7541
|
+
//#endregion
|
|
7542
|
+
//#region src/hooks/install/cline.ts
|
|
7543
|
+
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
7544
|
+
const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
|
|
7545
|
+
const installCline = (opts) => {
|
|
7546
|
+
if (opts.scope !== "project") return {
|
|
7547
|
+
wrote: [],
|
|
7548
|
+
skipped: [],
|
|
7549
|
+
planned: [{
|
|
7550
|
+
path: ".clinerules",
|
|
7551
|
+
summary: "Cline is project-scope only; pass --project"
|
|
7552
|
+
}]
|
|
7553
|
+
};
|
|
7554
|
+
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
7555
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
|
|
7556
|
+
return {
|
|
7557
|
+
wrote: [...cline.wrote, ...roo.wrote],
|
|
7558
|
+
skipped: [...cline.skipped, ...roo.skipped],
|
|
7559
|
+
planned: [...cline.planned, ...roo.planned]
|
|
7560
|
+
};
|
|
7561
|
+
};
|
|
7562
|
+
const uninstallCline = (opts) => {
|
|
7563
|
+
if (opts.scope !== "project") return {
|
|
7564
|
+
removed: [],
|
|
7565
|
+
skipped: []
|
|
7566
|
+
};
|
|
7567
|
+
const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
|
|
7568
|
+
const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
|
|
7569
|
+
return {
|
|
7570
|
+
removed: [...a.removed, ...b.removed],
|
|
7571
|
+
skipped: [...a.skipped, ...b.skipped]
|
|
7572
|
+
};
|
|
7573
|
+
};
|
|
7574
|
+
|
|
7575
|
+
//#endregion
|
|
7576
|
+
//#region src/hooks/install/codex.ts
|
|
7577
|
+
const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
|
|
7578
|
+
const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
|
|
7579
|
+
const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
|
|
7580
|
+
|
|
7581
|
+
//#endregion
|
|
7582
|
+
//#region src/hooks/install/copilot.ts
|
|
7583
|
+
const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
|
|
7584
|
+
const installCopilot = (opts) => {
|
|
7585
|
+
if (opts.scope !== "project") return {
|
|
7586
|
+
wrote: [],
|
|
7587
|
+
skipped: [],
|
|
7588
|
+
planned: [{
|
|
7589
|
+
path: ".github/copilot-instructions.md",
|
|
7590
|
+
summary: "Copilot is project-scope only; pass --project"
|
|
7591
|
+
}]
|
|
7592
|
+
};
|
|
7593
|
+
return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
|
|
7594
|
+
};
|
|
7595
|
+
const uninstallCopilot = (opts) => {
|
|
7596
|
+
if (opts.scope !== "project") return {
|
|
7597
|
+
removed: [],
|
|
7598
|
+
skipped: []
|
|
7599
|
+
};
|
|
7600
|
+
return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
|
|
7601
|
+
};
|
|
7602
|
+
|
|
7603
|
+
//#endregion
|
|
7604
|
+
//#region src/hooks/install/cursor.ts
|
|
7605
|
+
const resolveCursorPaths = (opts) => {
|
|
7606
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
7607
|
+
return {
|
|
7608
|
+
hooks: path.join(root, "hooks.json"),
|
|
7609
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
7610
|
+
};
|
|
7611
|
+
};
|
|
7612
|
+
const buildHookEntry = () => {
|
|
7613
|
+
const hashBody = JSON.stringify({
|
|
7614
|
+
command: "aislop hook cursor",
|
|
7615
|
+
timeout: 5e3
|
|
7616
|
+
});
|
|
7617
|
+
return {
|
|
7618
|
+
command: "aislop hook cursor",
|
|
7619
|
+
type: "command",
|
|
7620
|
+
timeout: 5e3,
|
|
7621
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7622
|
+
v: 1,
|
|
7623
|
+
managed: true,
|
|
7624
|
+
hash: sentinelHash(hashBody)
|
|
7625
|
+
}
|
|
7626
|
+
};
|
|
7627
|
+
};
|
|
7628
|
+
const renderHooksJson = (existingRaw) => {
|
|
7629
|
+
let obj = { version: 1 };
|
|
7630
|
+
if (existingRaw) try {
|
|
7631
|
+
obj = JSON.parse(existingRaw);
|
|
7632
|
+
} catch {
|
|
7633
|
+
obj = { version: 1 };
|
|
7634
|
+
}
|
|
7635
|
+
if (typeof obj.version !== "number") obj.version = 1;
|
|
7636
|
+
const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
|
|
7637
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7638
|
+
};
|
|
7639
|
+
const installCursor = (opts) => {
|
|
7640
|
+
const paths = resolveCursorPaths(opts);
|
|
7641
|
+
const result = emptyResult();
|
|
7642
|
+
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
7643
|
+
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
7644
|
+
if (opts.scope === "project") {
|
|
7645
|
+
const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7646
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
|
|
7647
|
+
}
|
|
7648
|
+
return result;
|
|
7649
|
+
};
|
|
7650
|
+
const uninstallCursor = (opts) => {
|
|
7651
|
+
const paths = resolveCursorPaths(opts);
|
|
7652
|
+
const result = {
|
|
7653
|
+
removed: [],
|
|
7654
|
+
skipped: []
|
|
7655
|
+
};
|
|
7656
|
+
const raw = readIfExists(paths.hooks);
|
|
7657
|
+
if (raw) {
|
|
7658
|
+
let obj = {};
|
|
7659
|
+
try {
|
|
7660
|
+
obj = JSON.parse(raw);
|
|
7661
|
+
} catch {
|
|
7662
|
+
obj = {};
|
|
7663
|
+
}
|
|
7664
|
+
const stripped = removeAislopEntries(obj, "afterFileEdit").next;
|
|
7665
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7666
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
|
|
7667
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
7668
|
+
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7669
|
+
} else result.skipped.push(paths.hooks);
|
|
7670
|
+
if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
|
|
7671
|
+
return result;
|
|
7672
|
+
};
|
|
7673
|
+
|
|
7674
|
+
//#endregion
|
|
7675
|
+
//#region src/hooks/install/gemini.ts
|
|
7676
|
+
const resolveGeminiPaths = (opts) => {
|
|
7677
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
7678
|
+
return {
|
|
7679
|
+
settings: path.join(root, "settings.json"),
|
|
7680
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
7681
|
+
geminiMd: path.join(root, "GEMINI.md")
|
|
7682
|
+
};
|
|
7683
|
+
};
|
|
7684
|
+
const buildHookGroup = () => {
|
|
7685
|
+
const hashBody = JSON.stringify({
|
|
7686
|
+
command: "aislop hook gemini",
|
|
7687
|
+
matcher: "write_file|replace"
|
|
7688
|
+
});
|
|
7689
|
+
return {
|
|
7690
|
+
matcher: "write_file|replace",
|
|
7691
|
+
hooks: [{
|
|
7692
|
+
name: "aislop",
|
|
7693
|
+
type: "command",
|
|
7694
|
+
command: "aislop hook gemini",
|
|
7695
|
+
timeout: 5e3,
|
|
7696
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
7697
|
+
v: 1,
|
|
7698
|
+
managed: true,
|
|
7699
|
+
hash: sentinelHash(hashBody)
|
|
7700
|
+
}
|
|
7701
|
+
}]
|
|
7702
|
+
};
|
|
7703
|
+
};
|
|
7704
|
+
const renderSettings = (existingRaw) => {
|
|
7705
|
+
let obj = {};
|
|
7706
|
+
if (existingRaw) try {
|
|
7707
|
+
obj = JSON.parse(existingRaw);
|
|
7708
|
+
} catch {
|
|
7709
|
+
obj = {};
|
|
7710
|
+
}
|
|
7711
|
+
const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
|
|
7712
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
7713
|
+
};
|
|
7714
|
+
const installGemini = (opts) => {
|
|
7715
|
+
const paths = resolveGeminiPaths(opts);
|
|
7716
|
+
const result = emptyResult();
|
|
7717
|
+
const next = renderSettings(readIfExists(paths.settings));
|
|
7718
|
+
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
7719
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
7720
|
+
applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
|
|
7721
|
+
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
7722
|
+
const marker = "@AISLOP.md";
|
|
7723
|
+
if (!existingGeminiMd.includes(marker)) {
|
|
7724
|
+
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
7725
|
+
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
7726
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
7727
|
+
} else result.skipped.push(paths.geminiMd);
|
|
7728
|
+
return result;
|
|
7729
|
+
};
|
|
7730
|
+
const uninstallGemini = (opts) => {
|
|
7731
|
+
const paths = resolveGeminiPaths(opts);
|
|
7732
|
+
const result = {
|
|
7733
|
+
removed: [],
|
|
7734
|
+
skipped: []
|
|
7735
|
+
};
|
|
7736
|
+
const raw = readIfExists(paths.settings);
|
|
7737
|
+
if (raw) {
|
|
7738
|
+
let obj = {};
|
|
7739
|
+
try {
|
|
7740
|
+
obj = JSON.parse(raw);
|
|
7741
|
+
} catch {
|
|
7742
|
+
obj = {};
|
|
7743
|
+
}
|
|
7744
|
+
const stripped = removeAislopEntries(obj, "AfterTool").next;
|
|
7745
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
7746
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
7747
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
7748
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
7749
|
+
} else result.skipped.push(paths.settings);
|
|
7750
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
7751
|
+
else result.skipped.push(paths.aislopMd);
|
|
7752
|
+
const geminiMd = readIfExists(paths.geminiMd);
|
|
7753
|
+
if (geminiMd != null && geminiMd.includes("@AISLOP.md")) {
|
|
7754
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
7755
|
+
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
7756
|
+
} else result.skipped.push(paths.geminiMd);
|
|
7757
|
+
return result;
|
|
7758
|
+
};
|
|
7759
|
+
|
|
7760
|
+
//#endregion
|
|
7761
|
+
//#region src/hooks/install/kilocode.ts
|
|
7762
|
+
const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
|
|
7763
|
+
const installKilocode = (opts) => {
|
|
7764
|
+
if (opts.scope !== "project") return {
|
|
7765
|
+
wrote: [],
|
|
7766
|
+
skipped: [],
|
|
7767
|
+
planned: [{
|
|
7768
|
+
path: ".kilocode/rules/aislop-rules.md",
|
|
7769
|
+
summary: "Kilo Code is project-scope only; pass --project"
|
|
7770
|
+
}]
|
|
7771
|
+
};
|
|
7772
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
|
|
7773
|
+
};
|
|
7774
|
+
const uninstallKilocode = (opts) => {
|
|
7775
|
+
if (opts.scope !== "project") return {
|
|
7776
|
+
removed: [],
|
|
7777
|
+
skipped: []
|
|
7778
|
+
};
|
|
7779
|
+
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
7780
|
+
};
|
|
7781
|
+
|
|
7782
|
+
//#endregion
|
|
7783
|
+
//#region src/hooks/install/windsurf.ts
|
|
7784
|
+
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
7785
|
+
const installWindsurf = (opts) => {
|
|
7786
|
+
if (opts.scope !== "project") return {
|
|
7787
|
+
wrote: [],
|
|
7788
|
+
skipped: [],
|
|
7789
|
+
planned: [{
|
|
7790
|
+
path: ".windsurfrules",
|
|
7791
|
+
summary: "Windsurf is project-scope only; pass --project"
|
|
7792
|
+
}]
|
|
7793
|
+
};
|
|
7794
|
+
return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
|
|
7795
|
+
};
|
|
7796
|
+
const uninstallWindsurf = (opts) => {
|
|
7797
|
+
if (opts.scope !== "project") return {
|
|
7798
|
+
removed: [],
|
|
7799
|
+
skipped: []
|
|
7800
|
+
};
|
|
7801
|
+
return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
|
|
7802
|
+
};
|
|
7803
|
+
|
|
7804
|
+
//#endregion
|
|
7805
|
+
//#region src/hooks/install/registry.ts
|
|
7806
|
+
const ALL_AGENTS = [
|
|
7807
|
+
"claude",
|
|
7808
|
+
"cursor",
|
|
7809
|
+
"gemini",
|
|
7810
|
+
"codex",
|
|
7811
|
+
"windsurf",
|
|
7812
|
+
"cline",
|
|
7813
|
+
"kilocode",
|
|
7814
|
+
"antigravity",
|
|
7815
|
+
"copilot"
|
|
7816
|
+
];
|
|
7817
|
+
const AGENTS_PROJECT_ONLY = [
|
|
7818
|
+
"windsurf",
|
|
7819
|
+
"cline",
|
|
7820
|
+
"kilocode",
|
|
7821
|
+
"antigravity",
|
|
7822
|
+
"copilot"
|
|
7823
|
+
];
|
|
7824
|
+
const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
7825
|
+
"claude",
|
|
7826
|
+
"cursor",
|
|
7827
|
+
"gemini",
|
|
7828
|
+
"codex"
|
|
7829
|
+
];
|
|
7830
|
+
const paths = {
|
|
7831
|
+
claude: (opts) => {
|
|
7832
|
+
const p = resolveClaudePaths(opts);
|
|
7833
|
+
return [
|
|
7834
|
+
p.settings,
|
|
7835
|
+
p.aislopMd,
|
|
7836
|
+
p.claudeMd
|
|
7837
|
+
];
|
|
7838
|
+
},
|
|
7839
|
+
cursor: (opts) => {
|
|
7840
|
+
const p = resolveCursorPaths(opts);
|
|
7841
|
+
return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
|
|
7842
|
+
},
|
|
7843
|
+
gemini: (opts) => {
|
|
7844
|
+
const p = resolveGeminiPaths(opts);
|
|
7845
|
+
return [
|
|
7846
|
+
p.settings,
|
|
7847
|
+
p.aislopMd,
|
|
7848
|
+
p.geminiMd
|
|
7849
|
+
];
|
|
7850
|
+
},
|
|
7851
|
+
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
7852
|
+
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
7853
|
+
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
7854
|
+
kilocode: (opts) => [resolveKilocodePaths(opts).rules],
|
|
7855
|
+
antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
|
|
7856
|
+
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
7857
|
+
};
|
|
7858
|
+
const REGISTRY = {
|
|
7859
|
+
claude: {
|
|
7860
|
+
install: installClaude,
|
|
7861
|
+
uninstall: uninstallClaude,
|
|
7862
|
+
paths: paths.claude
|
|
7863
|
+
},
|
|
7864
|
+
cursor: {
|
|
7865
|
+
install: installCursor,
|
|
7866
|
+
uninstall: uninstallCursor,
|
|
7867
|
+
paths: paths.cursor
|
|
7868
|
+
},
|
|
7869
|
+
gemini: {
|
|
7870
|
+
install: installGemini,
|
|
7871
|
+
uninstall: uninstallGemini,
|
|
7872
|
+
paths: paths.gemini
|
|
7873
|
+
},
|
|
7874
|
+
codex: {
|
|
7875
|
+
install: installCodex,
|
|
7876
|
+
uninstall: uninstallCodex,
|
|
7877
|
+
paths: paths.codex
|
|
7878
|
+
},
|
|
7879
|
+
windsurf: {
|
|
7880
|
+
install: installWindsurf,
|
|
7881
|
+
uninstall: uninstallWindsurf,
|
|
7882
|
+
paths: paths.windsurf
|
|
7883
|
+
},
|
|
7884
|
+
cline: {
|
|
7885
|
+
install: installCline,
|
|
7886
|
+
uninstall: uninstallCline,
|
|
7887
|
+
paths: paths.cline
|
|
7888
|
+
},
|
|
7889
|
+
kilocode: {
|
|
7890
|
+
install: installKilocode,
|
|
7891
|
+
uninstall: uninstallKilocode,
|
|
7892
|
+
paths: paths.kilocode
|
|
7893
|
+
},
|
|
7894
|
+
antigravity: {
|
|
7895
|
+
install: installAntigravity,
|
|
7896
|
+
uninstall: uninstallAntigravity,
|
|
7897
|
+
paths: paths.antigravity
|
|
7898
|
+
},
|
|
7899
|
+
copilot: {
|
|
7900
|
+
install: installCopilot,
|
|
7901
|
+
uninstall: uninstallCopilot,
|
|
7902
|
+
paths: paths.copilot
|
|
7903
|
+
}
|
|
7904
|
+
};
|
|
7905
|
+
const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
|
|
7906
|
+
const detectInstalledAgents = (opts) => {
|
|
7907
|
+
const hits = [];
|
|
7908
|
+
for (const agent of ALL_AGENTS) {
|
|
7909
|
+
const scope = defaultScopeFor(agent);
|
|
7910
|
+
if (REGISTRY[agent].paths({
|
|
7911
|
+
home: opts.home,
|
|
7912
|
+
cwd: opts.cwd,
|
|
7913
|
+
scope
|
|
7914
|
+
}).some((p) => fs.existsSync(p))) hits.push(agent);
|
|
7915
|
+
}
|
|
7916
|
+
return hits;
|
|
7917
|
+
};
|
|
7918
|
+
|
|
7919
|
+
//#endregion
|
|
7920
|
+
//#region src/commands/hook.ts
|
|
7921
|
+
const resolveOpts = (agent, flags) => {
|
|
7922
|
+
const scope = AGENTS_PROJECT_ONLY.includes(agent) ? "project" : flags.scope;
|
|
7923
|
+
return {
|
|
7924
|
+
home: os.homedir(),
|
|
7925
|
+
cwd: process.cwd(),
|
|
7926
|
+
scope,
|
|
7927
|
+
dryRun: flags.dryRun,
|
|
7928
|
+
qualityGate: flags.qualityGate
|
|
7929
|
+
};
|
|
7930
|
+
};
|
|
7931
|
+
const printPlan = (agent, result) => {
|
|
7932
|
+
if (result.planned.length === 0) {
|
|
7933
|
+
process.stdout.write(` ${agent}: already up to date\n`);
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7936
|
+
process.stdout.write(` ${agent}:\n`);
|
|
7937
|
+
for (const op of result.planned) process.stdout.write(` ${style(theme, "dim", "+")} ${op.path} — ${op.summary}\n`);
|
|
7938
|
+
};
|
|
7939
|
+
const hookInstall = async (flags) => {
|
|
7940
|
+
if (flags.dryRun) process.stdout.write("aislop hook install (dry-run)\n\n");
|
|
7941
|
+
for (const agent of flags.agents) {
|
|
7942
|
+
const opts = resolveOpts(agent, flags);
|
|
7943
|
+
const result = REGISTRY[agent].install(opts);
|
|
7944
|
+
if (flags.dryRun) {
|
|
7945
|
+
printPlan(agent, result);
|
|
7946
|
+
continue;
|
|
7947
|
+
}
|
|
7948
|
+
if (result.wrote.length === 0) {
|
|
7949
|
+
process.stdout.write(`${agent}: nothing to do (already up to date)\n`);
|
|
7950
|
+
continue;
|
|
7951
|
+
}
|
|
7952
|
+
for (const f of result.wrote) process.stdout.write(` wrote ${f}\n`);
|
|
7953
|
+
for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
|
|
7954
|
+
}
|
|
7955
|
+
if (flags.dryRun) process.stdout.write("\nNo files touched. Re-run without --dry-run to apply.\n");
|
|
7956
|
+
};
|
|
7957
|
+
const hookUninstall = async (flags) => {
|
|
7958
|
+
if (flags.dryRun) process.stdout.write("aislop hook uninstall (dry-run)\n\n");
|
|
7959
|
+
for (const agent of flags.agents) {
|
|
7960
|
+
const opts = resolveOpts(agent, flags);
|
|
7961
|
+
const result = REGISTRY[agent].uninstall(opts);
|
|
7962
|
+
if (result.removed.length === 0) {
|
|
7963
|
+
process.stdout.write(`${agent}: nothing installed\n`);
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
7966
|
+
for (const f of result.removed) process.stdout.write(` remove ${f}\n`);
|
|
7967
|
+
for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
|
|
7968
|
+
}
|
|
7969
|
+
};
|
|
7970
|
+
const hookStatus = async () => {
|
|
7971
|
+
const home = os.homedir();
|
|
7972
|
+
const cwd = process.cwd();
|
|
7973
|
+
process.stdout.write("aislop hook status\n\n");
|
|
7974
|
+
const installed = new Set(detectInstalledAgents({
|
|
7975
|
+
home,
|
|
7976
|
+
cwd
|
|
7977
|
+
}));
|
|
7978
|
+
for (const agent of ALL_AGENTS) {
|
|
7979
|
+
const scope = defaultScopeFor(agent);
|
|
7980
|
+
const hits = REGISTRY[agent].paths({
|
|
7981
|
+
home,
|
|
7982
|
+
cwd,
|
|
7983
|
+
scope
|
|
7984
|
+
}).filter((p) => fs.existsSync(p));
|
|
7985
|
+
const status = installed.has(agent) ? "installed" : "not installed";
|
|
7986
|
+
const marker = installed.has(agent) ? "✓" : "·";
|
|
7987
|
+
process.stdout.write(` ${marker} ${agent.padEnd(12)} ${scope.padEnd(8)} ${status}\n`);
|
|
7988
|
+
for (const p of hits) process.stdout.write(` ${p}\n`);
|
|
7989
|
+
}
|
|
7990
|
+
};
|
|
7991
|
+
const hookRun = async (agent, flags) => {
|
|
7992
|
+
let exitCode = 0;
|
|
7993
|
+
if (agent === "claude") exitCode = flags?.stop ? await runClaudeStopHook() : await runClaudeHook();
|
|
7994
|
+
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
7995
|
+
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
7996
|
+
else {
|
|
7997
|
+
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
7998
|
+
process.exit(0);
|
|
7999
|
+
}
|
|
8000
|
+
process.exit(exitCode);
|
|
8001
|
+
};
|
|
8002
|
+
const hookBaseline = async () => {
|
|
8003
|
+
const result = await captureBaseline(process.cwd());
|
|
8004
|
+
process.stdout.write(`baseline captured: score=${result.score} files=${result.fileCount}\n`);
|
|
8005
|
+
process.stdout.write(` -> ${result.path}\n`);
|
|
8006
|
+
};
|
|
8007
|
+
const parseAgentFlag = (raw, fallback) => {
|
|
8008
|
+
if (!raw) return fallback;
|
|
8009
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8010
|
+
const unknown = parts.filter((p) => !ALL_AGENTS.includes(p));
|
|
8011
|
+
if (unknown.length > 0) throw new Error(`Unknown agent(s): ${unknown.join(", ")}. Valid: ${ALL_AGENTS.join(", ")}`);
|
|
8012
|
+
return parts;
|
|
8013
|
+
};
|
|
8014
|
+
const defaultInstallTargets = () => {
|
|
8015
|
+
return AGENTS_SUPPORTING_BOTH_SCOPES;
|
|
8016
|
+
};
|
|
8017
|
+
|
|
6436
8018
|
//#endregion
|
|
6437
8019
|
//#region src/commands/init.ts
|
|
6438
8020
|
const buildInitSuccessRender = (input) => {
|
|
@@ -7004,6 +8586,45 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
|
|
|
7004
8586
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
7005
8587
|
await rulesCommand(directory);
|
|
7006
8588
|
});
|
|
8589
|
+
const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
|
|
8590
|
+
const resolveScope = (flags) => {
|
|
8591
|
+
if (flags.project) return "project";
|
|
8592
|
+
if (flags.global) return "global";
|
|
8593
|
+
return "global";
|
|
8594
|
+
};
|
|
8595
|
+
hook.command("install").description("Install aislop hooks for one or more coding agents").option("--agent <names>", "comma-separated agent list (claude,cursor,gemini,codex,windsurf,cline,kilocode,antigravity,copilot). default: all non-project-only agents").option("-g, --global", "install to the user-scope config (default for agents that support it)").option("--project", "install to the project-scope config").option("--dry-run", "print the planned diff without writing").option("--yes", "skip the confirmation prompt (reserved)").option("--quality-gate", "add a Stop hook that blocks when score regresses below baseline (Claude only)").action(async (opts) => {
|
|
8596
|
+
await hookInstall({
|
|
8597
|
+
agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
|
|
8598
|
+
scope: resolveScope(opts),
|
|
8599
|
+
dryRun: Boolean(opts.dryRun),
|
|
8600
|
+
yes: Boolean(opts.yes),
|
|
8601
|
+
qualityGate: Boolean(opts.qualityGate)
|
|
8602
|
+
});
|
|
8603
|
+
});
|
|
8604
|
+
hook.command("uninstall").description("Uninstall aislop hooks for one or more agents").option("--agent <names>", "comma-separated agent list. default: all agents with installed hooks").option("-g, --global", "uninstall from user-scope config").option("--project", "uninstall from project-scope config").option("--dry-run", "print the planned removal without writing").action(async (opts) => {
|
|
8605
|
+
await hookUninstall({
|
|
8606
|
+
agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
|
|
8607
|
+
scope: resolveScope(opts),
|
|
8608
|
+
dryRun: Boolean(opts.dryRun),
|
|
8609
|
+
yes: true,
|
|
8610
|
+
qualityGate: false
|
|
8611
|
+
});
|
|
8612
|
+
});
|
|
8613
|
+
hook.command("status").description("Show which agent hooks are installed").action(async () => {
|
|
8614
|
+
await hookStatus();
|
|
8615
|
+
});
|
|
8616
|
+
hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
|
|
8617
|
+
await hookBaseline();
|
|
8618
|
+
});
|
|
8619
|
+
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
|
|
8620
|
+
await hookRun("claude", { stop: Boolean(opts.stop) });
|
|
8621
|
+
});
|
|
8622
|
+
hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
8623
|
+
await hookRun("cursor");
|
|
8624
|
+
});
|
|
8625
|
+
hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
8626
|
+
await hookRun("gemini");
|
|
8627
|
+
});
|
|
7007
8628
|
const main = async () => {
|
|
7008
8629
|
await program.parseAsync();
|
|
7009
8630
|
await flushTelemetry();
|