aislop 0.9.3 → 0.9.5
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 +58 -5
- package/dist/cli.js +1133 -64
- package/dist/{version-BNO_Lw7E.js → engine-info-DCvIfZ0f.js} +1 -5
- package/dist/index.d.ts +6 -0
- package/dist/index.js +857 -53
- package/dist/{json-BhO1Ufj3.js → json-CZU3lEfE.js} +2 -1
- package/dist/mcp.js +738 -39
- package/dist/sarif-CZVuavf_.js +61 -0
- package/dist/sarif-Cneulb6L.js +60 -0
- package/dist/version-ls3wZmOU.js +5 -0
- package/package.json +93 -91
- package/scripts/gen-config-schema.mjs +35 -0
- /package/dist/{expo-doctor-Bz0LZhQ6.js → expo-doctor-BcIkOte5.js} +0 -0
- /package/dist/{generic-BrcWMW7E.js → generic-D_T4cUaC.js} +0 -0
- /package/dist/{typecheck-XJMuCczG.js → typecheck-DQSzG8fX.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,8 @@ import YAML from "yaml";
|
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
11
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import micromatch from "micromatch";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
13
|
import ts from "typescript";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
16
16
|
import pc from "picocolors";
|
|
17
17
|
import wcwidth from "wcwidth";
|
|
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.9.
|
|
37
|
+
const APP_VERSION = "0.9.5";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -180,8 +180,8 @@ const redactProperties = (props) => {
|
|
|
180
180
|
|
|
181
181
|
//#endregion
|
|
182
182
|
//#region src/telemetry/client.ts
|
|
183
|
-
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
184
|
-
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
183
|
+
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
184
|
+
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
185
185
|
const SCHEMA_VERSION = "v2";
|
|
186
186
|
const REQUEST_TIMEOUT_MS = 3e3;
|
|
187
187
|
const isTelemetryDisabled = (config) => {
|
|
@@ -602,7 +602,8 @@ const DEFAULT_CONFIG = {
|
|
|
602
602
|
failBelow: 70,
|
|
603
603
|
format: "json"
|
|
604
604
|
},
|
|
605
|
-
telemetry: { enabled: true }
|
|
605
|
+
telemetry: { enabled: true },
|
|
606
|
+
rules: {}
|
|
606
607
|
};
|
|
607
608
|
const GITHUB_WORKFLOW_DIR = ".github/workflows";
|
|
608
609
|
const GITHUB_WORKFLOW_FILE = "aislop.yml";
|
|
@@ -729,6 +730,12 @@ const CiSchema = z.object({
|
|
|
729
730
|
format: z.enum(["json"]).default("json")
|
|
730
731
|
});
|
|
731
732
|
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) });
|
|
733
|
+
const RuleSeverityOverride = z.enum([
|
|
734
|
+
"error",
|
|
735
|
+
"warning",
|
|
736
|
+
"off"
|
|
737
|
+
]);
|
|
738
|
+
const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
|
|
732
739
|
const AislopConfigSchema = z.object({
|
|
733
740
|
version: z.number().default(1),
|
|
734
741
|
engines: EnginesSchema.default(() => ({
|
|
@@ -763,6 +770,7 @@ const AislopConfigSchema = z.object({
|
|
|
763
770
|
format: "json"
|
|
764
771
|
})),
|
|
765
772
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
773
|
+
rules: RulesSchema,
|
|
766
774
|
exclude: z.array(z.string()).default(() => [
|
|
767
775
|
"node_modules",
|
|
768
776
|
".git",
|
|
@@ -831,7 +839,7 @@ const loadConfig = (directory) => {
|
|
|
831
839
|
//#endregion
|
|
832
840
|
//#region src/utils/source-files.ts
|
|
833
841
|
const MAX_BUFFER$1 = 50 * 1024 * 1024;
|
|
834
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
842
|
+
const SOURCE_EXTENSIONS$1 = new Set([
|
|
835
843
|
".ts",
|
|
836
844
|
".tsx",
|
|
837
845
|
".js",
|
|
@@ -939,7 +947,7 @@ const toProjectPath = (rootDirectory, filePath) => {
|
|
|
939
947
|
const isWithinProject = (relativePath) => relativePath.length > 0 && !relativePath.startsWith("..");
|
|
940
948
|
const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
941
949
|
const extension = path.extname(filePath);
|
|
942
|
-
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
950
|
+
return SOURCE_EXTENSIONS$1.has(extension) || extraExtensions.has(extension);
|
|
943
951
|
};
|
|
944
952
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
945
953
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
@@ -1078,7 +1086,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1078
1086
|
|
|
1079
1087
|
//#endregion
|
|
1080
1088
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1081
|
-
const JS_EXTS$
|
|
1089
|
+
const JS_EXTS$2 = new Set([
|
|
1082
1090
|
".ts",
|
|
1083
1091
|
".tsx",
|
|
1084
1092
|
".js",
|
|
@@ -1089,11 +1097,11 @@ const JS_EXTS$1 = new Set([
|
|
|
1089
1097
|
const THIN_WRAPPER_PATTERNS = [
|
|
1090
1098
|
{
|
|
1091
1099
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1092
|
-
extensions: JS_EXTS$
|
|
1100
|
+
extensions: JS_EXTS$2
|
|
1093
1101
|
},
|
|
1094
1102
|
{
|
|
1095
1103
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1096
|
-
extensions: JS_EXTS$
|
|
1104
|
+
extensions: JS_EXTS$2
|
|
1097
1105
|
},
|
|
1098
1106
|
{
|
|
1099
1107
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1326,6 +1334,12 @@ const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
|
1326
1334
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1327
1335
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
1328
1336
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1337
|
+
const isLineComment = (trimmed) => isJsComment(trimmed) || isPythonComment(trimmed);
|
|
1338
|
+
const isInMultiLineCommentRun = (lines, index) => {
|
|
1339
|
+
const prev = index > 0 ? lines[index - 1].trim() : "";
|
|
1340
|
+
const next = index + 1 < lines.length ? lines[index + 1].trim() : "";
|
|
1341
|
+
return isLineComment(prev) || isLineComment(next);
|
|
1342
|
+
};
|
|
1329
1343
|
/**
|
|
1330
1344
|
* Extract just the comment text after the comment marker.
|
|
1331
1345
|
*/
|
|
@@ -1378,6 +1392,7 @@ const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
|
1378
1392
|
const lines = content.split("\n");
|
|
1379
1393
|
for (let i = 0; i < lines.length; i++) {
|
|
1380
1394
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
1395
|
+
if (isInMultiLineCommentRun(lines, i)) continue;
|
|
1381
1396
|
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
1382
1397
|
diagnostics.push({
|
|
1383
1398
|
filePath: relativePath,
|
|
@@ -1539,6 +1554,177 @@ const detectDeadPatterns = async (context) => {
|
|
|
1539
1554
|
return diagnostics;
|
|
1540
1555
|
};
|
|
1541
1556
|
|
|
1557
|
+
//#endregion
|
|
1558
|
+
//#region src/engines/ai-slop/defensive-patterns.ts
|
|
1559
|
+
const JS_TS_EXTENSIONS = new Set([
|
|
1560
|
+
".ts",
|
|
1561
|
+
".tsx",
|
|
1562
|
+
".js",
|
|
1563
|
+
".jsx",
|
|
1564
|
+
".mjs",
|
|
1565
|
+
".cjs"
|
|
1566
|
+
]);
|
|
1567
|
+
const TS_EXTENSIONS = new Set([".ts", ".tsx"]);
|
|
1568
|
+
const COERCION_CTORS = {
|
|
1569
|
+
string: "String",
|
|
1570
|
+
number: "Number",
|
|
1571
|
+
boolean: "Boolean"
|
|
1572
|
+
};
|
|
1573
|
+
const scriptKindFor = (ext) => {
|
|
1574
|
+
switch (ext) {
|
|
1575
|
+
case ".tsx": return ts.ScriptKind.TSX;
|
|
1576
|
+
case ".jsx": return ts.ScriptKind.JSX;
|
|
1577
|
+
case ".js":
|
|
1578
|
+
case ".mjs":
|
|
1579
|
+
case ".cjs": return ts.ScriptKind.JS;
|
|
1580
|
+
default: return ts.ScriptKind.TS;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const lineFor = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
1584
|
+
const makeDiagnostic = (filePath, line, rule, message, help) => ({
|
|
1585
|
+
filePath,
|
|
1586
|
+
engine: "ai-slop",
|
|
1587
|
+
rule,
|
|
1588
|
+
severity: "warning",
|
|
1589
|
+
message,
|
|
1590
|
+
help,
|
|
1591
|
+
line,
|
|
1592
|
+
column: 0,
|
|
1593
|
+
category: "AI Slop",
|
|
1594
|
+
fixable: false
|
|
1595
|
+
});
|
|
1596
|
+
const isFunctionNode = (node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node);
|
|
1597
|
+
const primitiveKindOf = (node) => {
|
|
1598
|
+
if (!node) return null;
|
|
1599
|
+
switch (node.kind) {
|
|
1600
|
+
case ts.SyntaxKind.StringKeyword: return "string";
|
|
1601
|
+
case ts.SyntaxKind.NumberKeyword: return "number";
|
|
1602
|
+
case ts.SyntaxKind.BooleanKeyword: return "boolean";
|
|
1603
|
+
default: return null;
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
const hasExportModifier = (node) => node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
1607
|
+
const primitiveParamsOf = (node) => {
|
|
1608
|
+
const params = /* @__PURE__ */ new Map();
|
|
1609
|
+
for (const param of node.parameters) {
|
|
1610
|
+
if (!ts.isIdentifier(param.name)) continue;
|
|
1611
|
+
const kind = primitiveKindOf(param.type);
|
|
1612
|
+
if (!kind) continue;
|
|
1613
|
+
params.set(param.name.text, kind);
|
|
1614
|
+
}
|
|
1615
|
+
return params;
|
|
1616
|
+
};
|
|
1617
|
+
const isRethrowStatement = (statement, errorName) => ts.isThrowStatement(statement) && statement.expression !== void 0 && ts.isIdentifier(statement.expression) && statement.expression.text === errorName;
|
|
1618
|
+
const isPromiseRejectRethrow = (statement, errorName) => {
|
|
1619
|
+
if (!ts.isReturnStatement(statement) || !statement.expression) return false;
|
|
1620
|
+
const expression = statement.expression;
|
|
1621
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) return false;
|
|
1622
|
+
const [arg] = expression.arguments;
|
|
1623
|
+
if (!ts.isIdentifier(arg) || arg.text !== errorName) return false;
|
|
1624
|
+
if (!ts.isPropertyAccessExpression(expression.expression)) return false;
|
|
1625
|
+
const target = expression.expression;
|
|
1626
|
+
return ts.isIdentifier(target.expression) && target.expression.text === "Promise" && target.name.text === "reject";
|
|
1627
|
+
};
|
|
1628
|
+
const detectRedundantTryCatch = (sourceFile, relativePath) => {
|
|
1629
|
+
const diagnostics = [];
|
|
1630
|
+
const visit = (node) => {
|
|
1631
|
+
if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
|
|
1632
|
+
const catchNameNode = node.catchClause.variableDeclaration?.name;
|
|
1633
|
+
const [onlyStatement] = node.catchClause.block.statements;
|
|
1634
|
+
if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
|
|
1635
|
+
}
|
|
1636
|
+
ts.forEachChild(node, visit);
|
|
1637
|
+
};
|
|
1638
|
+
visit(sourceFile);
|
|
1639
|
+
return diagnostics;
|
|
1640
|
+
};
|
|
1641
|
+
const detectPrimitiveCoercions = (sourceFile, relativePath) => {
|
|
1642
|
+
const diagnostics = [];
|
|
1643
|
+
const scanFunctionBody = (node, params) => {
|
|
1644
|
+
const body = node.body;
|
|
1645
|
+
if (!body || params.size === 0) return;
|
|
1646
|
+
const visitBody = (child) => {
|
|
1647
|
+
if (child !== body && isFunctionNode(child)) return;
|
|
1648
|
+
if (ts.isCallExpression(child) && ts.isIdentifier(child.expression)) {
|
|
1649
|
+
const [arg] = child.arguments;
|
|
1650
|
+
if (arg && ts.isIdentifier(arg)) {
|
|
1651
|
+
const primitive = params.get(arg.text);
|
|
1652
|
+
if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
ts.forEachChild(child, visitBody);
|
|
1656
|
+
};
|
|
1657
|
+
visitBody(body);
|
|
1658
|
+
};
|
|
1659
|
+
const visit = (node) => {
|
|
1660
|
+
if (isFunctionNode(node)) scanFunctionBody(node, primitiveParamsOf(node));
|
|
1661
|
+
ts.forEachChild(node, visit);
|
|
1662
|
+
};
|
|
1663
|
+
visit(sourceFile);
|
|
1664
|
+
return diagnostics;
|
|
1665
|
+
};
|
|
1666
|
+
const normalizedTypeDeclaration = (sourceFile, node) => sourceFile.text.slice(node.getStart(sourceFile), node.getEnd()).replace(/\bexport\b/g, "").replace(/\bdeclare\b/g, "").replace(/\s+/g, " ").trim();
|
|
1667
|
+
const exportedTypesOf = (parsed) => {
|
|
1668
|
+
const declarations = [];
|
|
1669
|
+
const visit = (node) => {
|
|
1670
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && hasExportModifier(node)) declarations.push({
|
|
1671
|
+
name: node.name.text,
|
|
1672
|
+
signature: normalizedTypeDeclaration(parsed.sourceFile, node),
|
|
1673
|
+
filePath: parsed.relativePath,
|
|
1674
|
+
line: lineFor(parsed.sourceFile, node)
|
|
1675
|
+
});
|
|
1676
|
+
ts.forEachChild(node, visit);
|
|
1677
|
+
};
|
|
1678
|
+
visit(parsed.sourceFile);
|
|
1679
|
+
return declarations;
|
|
1680
|
+
};
|
|
1681
|
+
const duplicateTypeKeyOf = (declaration) => `${declaration.name}\0${declaration.signature}`;
|
|
1682
|
+
const detectDuplicateExportedTypes = (parsedSources) => {
|
|
1683
|
+
const diagnostics = [];
|
|
1684
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1685
|
+
for (const parsed of parsedSources) {
|
|
1686
|
+
if (!TS_EXTENSIONS.has(parsed.ext)) continue;
|
|
1687
|
+
for (const declaration of exportedTypesOf(parsed)) {
|
|
1688
|
+
const key = duplicateTypeKeyOf(declaration);
|
|
1689
|
+
const previous = seen.get(key);
|
|
1690
|
+
if (!previous) {
|
|
1691
|
+
seen.set(key, declaration);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
if (previous.filePath === declaration.filePath) continue;
|
|
1695
|
+
diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return diagnostics;
|
|
1699
|
+
};
|
|
1700
|
+
const detectDefensivePatterns = async (context) => {
|
|
1701
|
+
const diagnostics = [];
|
|
1702
|
+
const parsedSources = [];
|
|
1703
|
+
for (const filePath of getSourceFiles(context)) {
|
|
1704
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1705
|
+
let content;
|
|
1706
|
+
try {
|
|
1707
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1708
|
+
} catch {
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1712
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1713
|
+
const ext = path.extname(filePath);
|
|
1714
|
+
if (!JS_TS_EXTENSIONS.has(ext)) continue;
|
|
1715
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFor(ext));
|
|
1716
|
+
parsedSources.push({
|
|
1717
|
+
sourceFile,
|
|
1718
|
+
relativePath,
|
|
1719
|
+
ext
|
|
1720
|
+
});
|
|
1721
|
+
diagnostics.push(...detectRedundantTryCatch(sourceFile, relativePath));
|
|
1722
|
+
if (TS_EXTENSIONS.has(ext)) diagnostics.push(...detectPrimitiveCoercions(sourceFile, relativePath));
|
|
1723
|
+
}
|
|
1724
|
+
diagnostics.push(...detectDuplicateExportedTypes(parsedSources));
|
|
1725
|
+
return diagnostics;
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1542
1728
|
//#endregion
|
|
1543
1729
|
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
1544
1730
|
const JS_EXTENSIONS$3 = new Set([
|
|
@@ -1549,7 +1735,16 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1549
1735
|
".mjs",
|
|
1550
1736
|
".cjs"
|
|
1551
1737
|
]);
|
|
1552
|
-
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
1738
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1739
|
+
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1740
|
+
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1741
|
+
const isTypeOnly = (clause) => {
|
|
1742
|
+
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1743
|
+
const braces = VALUE_BINDING_RE.exec(clause);
|
|
1744
|
+
if (!braces) return false;
|
|
1745
|
+
const members = braces[1].split(",").map((member) => member.trim()).filter((member) => member.length > 0);
|
|
1746
|
+
return members.length > 0 && members.every((member) => /^type\b/.test(member));
|
|
1747
|
+
};
|
|
1553
1748
|
const extractImportLines = (content) => {
|
|
1554
1749
|
const lines = content.split("\n");
|
|
1555
1750
|
const results = [];
|
|
@@ -1558,8 +1753,9 @@ const extractImportLines = (content) => {
|
|
|
1558
1753
|
const match = IMPORT_FROM_RE$1.exec(line);
|
|
1559
1754
|
if (!match) continue;
|
|
1560
1755
|
results.push({
|
|
1561
|
-
spec: match[
|
|
1562
|
-
line: i + 1
|
|
1756
|
+
spec: match[2],
|
|
1757
|
+
line: i + 1,
|
|
1758
|
+
typeOnly: isTypeOnly(match[1])
|
|
1563
1759
|
});
|
|
1564
1760
|
}
|
|
1565
1761
|
return results;
|
|
@@ -1578,14 +1774,16 @@ const detectDuplicateImports = async (context) => {
|
|
|
1578
1774
|
}
|
|
1579
1775
|
const imports = extractImportLines(content);
|
|
1580
1776
|
if (imports.length < 2) continue;
|
|
1581
|
-
const
|
|
1777
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
1582
1778
|
for (const imp of imports) {
|
|
1583
|
-
const
|
|
1779
|
+
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1780
|
+
const list = byBucket.get(key) ?? [];
|
|
1584
1781
|
list.push(imp);
|
|
1585
|
-
|
|
1782
|
+
byBucket.set(key, list);
|
|
1586
1783
|
}
|
|
1587
1784
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1588
|
-
for (const
|
|
1785
|
+
for (const occurrences of byBucket.values()) {
|
|
1786
|
+
const { spec } = occurrences[0];
|
|
1589
1787
|
if (occurrences.length < 2) continue;
|
|
1590
1788
|
for (const dup of occurrences.slice(1)) {
|
|
1591
1789
|
const firstLine = occurrences[0].line;
|
|
@@ -1790,6 +1988,130 @@ const detectGoPatterns = async (context) => {
|
|
|
1790
1988
|
return diagnostics;
|
|
1791
1989
|
};
|
|
1792
1990
|
|
|
1991
|
+
//#endregion
|
|
1992
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
1993
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
1994
|
+
".ts",
|
|
1995
|
+
".tsx",
|
|
1996
|
+
".js",
|
|
1997
|
+
".jsx",
|
|
1998
|
+
".mjs",
|
|
1999
|
+
".cjs",
|
|
2000
|
+
".py",
|
|
2001
|
+
".go",
|
|
2002
|
+
".rs",
|
|
2003
|
+
".rb",
|
|
2004
|
+
".java",
|
|
2005
|
+
".php"
|
|
2006
|
+
]);
|
|
2007
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2008
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2009
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2010
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2011
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2012
|
+
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2013
|
+
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2014
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2015
|
+
"example.com",
|
|
2016
|
+
"example.org",
|
|
2017
|
+
"example.net"
|
|
2018
|
+
]);
|
|
2019
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2020
|
+
const HARDCODED_URL_FINDING = {
|
|
2021
|
+
rule: "ai-slop/hardcoded-url",
|
|
2022
|
+
message: "Hardcoded environment URL in production code",
|
|
2023
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2024
|
+
};
|
|
2025
|
+
const HARDCODED_ID_FINDING = {
|
|
2026
|
+
rule: "ai-slop/hardcoded-id",
|
|
2027
|
+
message: "Hardcoded provider/project ID in production code",
|
|
2028
|
+
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
2029
|
+
};
|
|
2030
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
2031
|
+
filePath,
|
|
2032
|
+
engine: "ai-slop",
|
|
2033
|
+
rule: spec.rule,
|
|
2034
|
+
severity: "warning",
|
|
2035
|
+
message: spec.message,
|
|
2036
|
+
help: spec.help,
|
|
2037
|
+
line,
|
|
2038
|
+
column: 0,
|
|
2039
|
+
category: "AI Slop",
|
|
2040
|
+
fixable: false
|
|
2041
|
+
});
|
|
2042
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2043
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
2044
|
+
const prefix = line.slice(0, index);
|
|
2045
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2046
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2047
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
2048
|
+
};
|
|
2049
|
+
const safeUrlHost = (urlText) => {
|
|
2050
|
+
try {
|
|
2051
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
2052
|
+
} catch {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2057
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2058
|
+
if (isEnvBackedLine(line)) return false;
|
|
2059
|
+
const host = safeUrlHost(urlText);
|
|
2060
|
+
if (!host) return false;
|
|
2061
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2062
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2063
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2064
|
+
};
|
|
2065
|
+
const hasUsefulIdShape = (value) => {
|
|
2066
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2067
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
2068
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2069
|
+
return /[0-9_-]/.test(value);
|
|
2070
|
+
};
|
|
2071
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2072
|
+
const diagnostics = [];
|
|
2073
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2074
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2075
|
+
let urlMatch;
|
|
2076
|
+
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
2077
|
+
const urlText = urlMatch[2];
|
|
2078
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2079
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2080
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2081
|
+
}
|
|
2082
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2083
|
+
ID_LITERAL_RE.lastIndex = 0;
|
|
2084
|
+
let idMatch;
|
|
2085
|
+
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
2086
|
+
const value = idMatch[2];
|
|
2087
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2088
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
2089
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2090
|
+
}
|
|
2091
|
+
return diagnostics;
|
|
2092
|
+
};
|
|
2093
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2094
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2095
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
2096
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2097
|
+
};
|
|
2098
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
2099
|
+
const diagnostics = [];
|
|
2100
|
+
for (const filePath of getSourceFiles(context)) {
|
|
2101
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2102
|
+
let content;
|
|
2103
|
+
try {
|
|
2104
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2105
|
+
} catch {
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2109
|
+
const ext = path.extname(filePath);
|
|
2110
|
+
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
2111
|
+
}
|
|
2112
|
+
return diagnostics;
|
|
2113
|
+
};
|
|
2114
|
+
|
|
1793
2115
|
//#endregion
|
|
1794
2116
|
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1795
2117
|
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
@@ -2057,23 +2379,87 @@ const PYTHON_STDLIB = new Set([
|
|
|
2057
2379
|
"zoneinfo"
|
|
2058
2380
|
]);
|
|
2059
2381
|
const PYTHON_IMPORT_TO_PIP = {
|
|
2060
|
-
yaml: "pyyaml",
|
|
2061
|
-
PIL: "pillow",
|
|
2062
|
-
dateutil: "python-dateutil",
|
|
2063
|
-
cv2:
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2382
|
+
yaml: ["pyyaml"],
|
|
2383
|
+
PIL: ["pillow"],
|
|
2384
|
+
dateutil: ["python-dateutil"],
|
|
2385
|
+
cv2: [
|
|
2386
|
+
"opencv-python",
|
|
2387
|
+
"opencv-python-headless",
|
|
2388
|
+
"opencv-contrib-python"
|
|
2389
|
+
],
|
|
2390
|
+
sklearn: ["scikit-learn"],
|
|
2391
|
+
bs4: ["beautifulsoup4"],
|
|
2392
|
+
typing_extensions: ["typing-extensions"],
|
|
2393
|
+
dotenv: ["python-dotenv"],
|
|
2394
|
+
genai: ["google-genai"],
|
|
2395
|
+
google: [
|
|
2396
|
+
"google-genai",
|
|
2397
|
+
"google-generativeai",
|
|
2398
|
+
"google-api-python-client",
|
|
2399
|
+
"google-cloud-storage",
|
|
2400
|
+
"google-cloud-aiplatform",
|
|
2401
|
+
"google-auth",
|
|
2402
|
+
"protobuf"
|
|
2403
|
+
],
|
|
2404
|
+
jose: ["python-jose"],
|
|
2405
|
+
jwt: ["pyjwt"],
|
|
2406
|
+
OpenSSL: ["pyopenssl"],
|
|
2407
|
+
Crypto: ["pycryptodome", "pycryptodomex"],
|
|
2408
|
+
Cryptodome: ["pycryptodomex", "pycryptodome"],
|
|
2409
|
+
magic: ["python-magic"],
|
|
2410
|
+
docx: ["python-docx"],
|
|
2411
|
+
pptx: ["python-pptx"],
|
|
2412
|
+
git: ["gitpython"],
|
|
2413
|
+
socks: ["pysocks"],
|
|
2414
|
+
redis: ["redis"],
|
|
2415
|
+
cairo: ["pycairo"],
|
|
2416
|
+
serial: ["pyserial"],
|
|
2417
|
+
usb: ["pyusb"],
|
|
2418
|
+
gi: ["pygobject"],
|
|
2419
|
+
Xlib: ["python-xlib"],
|
|
2420
|
+
ldap: ["python-ldap"],
|
|
2421
|
+
slugify: ["python-slugify"],
|
|
2422
|
+
memcache: ["python-memcached"],
|
|
2423
|
+
dns: ["dnspython"],
|
|
2424
|
+
attr: ["attrs"],
|
|
2425
|
+
attrs: ["attrs"],
|
|
2426
|
+
zoneinfo_data: ["tzdata"],
|
|
2427
|
+
pkg_resources: ["setuptools"],
|
|
2428
|
+
setuptools: ["setuptools"],
|
|
2429
|
+
wx: ["wxpython"],
|
|
2430
|
+
skimage: ["scikit-image"],
|
|
2431
|
+
OpenGL: ["pyopengl"],
|
|
2432
|
+
win32api: ["pywin32"],
|
|
2433
|
+
win32con: ["pywin32"],
|
|
2434
|
+
win32com: ["pywin32"],
|
|
2435
|
+
pythoncom: ["pywin32"],
|
|
2436
|
+
pywintypes: ["pywin32"],
|
|
2437
|
+
rest_framework: ["djangorestframework"],
|
|
2438
|
+
allauth: ["django-allauth"],
|
|
2439
|
+
corsheaders: ["django-cors-headers"],
|
|
2440
|
+
debug_toolbar: ["django-debug-toolbar"],
|
|
2441
|
+
environ: ["django-environ"],
|
|
2442
|
+
flask_cors: ["flask-cors"],
|
|
2443
|
+
flask_sqlalchemy: ["flask-sqlalchemy"],
|
|
2444
|
+
flask_migrate: ["flask-migrate"],
|
|
2445
|
+
flask_login: ["flask-login"],
|
|
2446
|
+
jwt_extended: ["flask-jwt-extended"],
|
|
2447
|
+
dateparser: ["dateparser"],
|
|
2448
|
+
yaml_include: ["pyyaml-include"],
|
|
2449
|
+
lxml_html_clean: ["lxml-html-clean"],
|
|
2450
|
+
grpc: ["grpcio"],
|
|
2451
|
+
grpc_status: ["grpcio-status"],
|
|
2452
|
+
google_crc32c: ["google-crc32c"],
|
|
2453
|
+
pkg_about: ["pkg-about"],
|
|
2454
|
+
mpl_toolkits: ["matplotlib"],
|
|
2455
|
+
dotmap: ["dotmap"],
|
|
2456
|
+
pydantic_settings: ["pydantic-settings"],
|
|
2457
|
+
telegram: ["python-telegram-bot"],
|
|
2458
|
+
discord: ["discord-py"],
|
|
2459
|
+
nacl: ["pynacl"],
|
|
2460
|
+
jwcrypto: ["jwcrypto"],
|
|
2461
|
+
humanfriendly: ["humanfriendly"],
|
|
2462
|
+
multipart: ["python-multipart"]
|
|
2077
2463
|
};
|
|
2078
2464
|
|
|
2079
2465
|
//#endregion
|
|
@@ -2112,6 +2498,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2112
2498
|
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
2113
2499
|
if (m) addPyDep(pyDeps, m[1]);
|
|
2114
2500
|
}
|
|
2501
|
+
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2502
|
+
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2115
2503
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2116
2504
|
let match = poetryRe.exec(content);
|
|
2117
2505
|
while (match !== null) {
|
|
@@ -2310,6 +2698,11 @@ const packageNameFromImport = (spec) => {
|
|
|
2310
2698
|
}
|
|
2311
2699
|
return spec.split("/")[0];
|
|
2312
2700
|
};
|
|
2701
|
+
const typesPackageName = (pkg) => {
|
|
2702
|
+
if (pkg.startsWith("@types/")) return pkg;
|
|
2703
|
+
if (pkg.startsWith("@")) return `@types/${pkg.slice(1).replace("/", "__")}`;
|
|
2704
|
+
return `@types/${pkg}`;
|
|
2705
|
+
};
|
|
2313
2706
|
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
2314
2707
|
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
2315
2708
|
const extractJsImports = (content) => {
|
|
@@ -2374,15 +2767,16 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
2374
2767
|
const realPkg = pkg.slice(7);
|
|
2375
2768
|
if (manifest.jsDeps.has(realPkg)) return null;
|
|
2376
2769
|
}
|
|
2770
|
+
if (manifest.jsDeps.has(typesPackageName(pkg))) return null;
|
|
2377
2771
|
return pkg;
|
|
2378
2772
|
};
|
|
2773
|
+
const normalizePyName = (name) => name.toLowerCase().replace(/_/g, "-");
|
|
2379
2774
|
const checkPyImport = (spec, manifest) => {
|
|
2380
2775
|
const root = spec.split(".")[0];
|
|
2381
2776
|
if (PYTHON_STDLIB.has(root)) return null;
|
|
2382
|
-
const normalized = root
|
|
2777
|
+
const normalized = normalizePyName(root);
|
|
2383
2778
|
if (manifest.pyDeps.has(normalized)) return null;
|
|
2384
|
-
|
|
2385
|
-
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
2779
|
+
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
2386
2780
|
return root;
|
|
2387
2781
|
};
|
|
2388
2782
|
const detectHallucinatedImports = async (context) => {
|
|
@@ -2532,6 +2926,85 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2532
2926
|
return blocks;
|
|
2533
2927
|
};
|
|
2534
2928
|
|
|
2929
|
+
//#endregion
|
|
2930
|
+
//#region src/engines/ai-slop/meta-comment.ts
|
|
2931
|
+
const PLAN_REFERENCE_RES = [
|
|
2932
|
+
/\b(?:stage|step|phase)\s+\d+\b(?!\s*[:.]?\s*(?:bytes|ms|seconds|of\s+\d))/i,
|
|
2933
|
+
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2934
|
+
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2935
|
+
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
2936
|
+
/\bfrom\s+the\s+(?:task|todo|plan|spec|ticket|prompt|requirements?)\b/i,
|
|
2937
|
+
/\bimplement(?:ing|s|ed)?\s+use\s*case\s+\d*/i,
|
|
2938
|
+
/\b(?:requirements?\s+doc|requirement\s+\d+)\b/i,
|
|
2939
|
+
/\bas\s+(?:instructed|specified|outlined)\s+(?:above|below|in\s+the)\b/i
|
|
2940
|
+
];
|
|
2941
|
+
const BEFORE_AFTER_RES = [
|
|
2942
|
+
/\bpreviously[,:]?\s+(?:this|we|it|the)\b/i,
|
|
2943
|
+
/\bused\s+to\s+(?:be|use|call|return|do|have|rely)\b/i,
|
|
2944
|
+
/\bchanged\s+(?:\w+\s+){0,3}from\s+.+\bto\b/i,
|
|
2945
|
+
/\bno\s+longer\s+(?:needed|used|required|necessary|calls?|returns?|does)\b/i,
|
|
2946
|
+
/\bthis\s+was\s+.+\bbut\s+now\b/i,
|
|
2947
|
+
/\bwe\s+(?:now|used\s+to)\s+(?:no\s+longer\s+)?(?:use|call|return|do|have)\b/i,
|
|
2948
|
+
/\breplaced\s+the\s+(?:old|previous|former)\b/i,
|
|
2949
|
+
/\b(?:was|were)\s+(?:renamed|moved|removed|refactored|extracted)\s+(?:from|to|out\s+of)\b/i
|
|
2950
|
+
];
|
|
2951
|
+
const WHY_OR_TODO_RE = /\b(?:because|since|otherwise|todo|fixme|hack|note:|reason:|workaround|see\s+(?:issue|#))\b/i;
|
|
2952
|
+
const looksLikeLicenseHeader$1 = (block) => {
|
|
2953
|
+
if (block.startLine !== 1) return false;
|
|
2954
|
+
const text = block.rawLines.join(" ").toLowerCase();
|
|
2955
|
+
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2956
|
+
};
|
|
2957
|
+
const looksLikeSuppressDirective$1 = (block) => block.rawLines.some((line) => /\b(?:biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(line));
|
|
2958
|
+
const matchMetaSignal = (block) => {
|
|
2959
|
+
if (looksLikeLicenseHeader$1(block)) return null;
|
|
2960
|
+
if (looksLikeSuppressDirective$1(block)) return null;
|
|
2961
|
+
if (block.kind === "jsdoc" && block.hasMeaningfulJsdocTag) return null;
|
|
2962
|
+
if (block.isRustDoc) return null;
|
|
2963
|
+
const joined = block.prose.join(" ");
|
|
2964
|
+
if (joined.trim().length === 0) return null;
|
|
2965
|
+
if (WHY_OR_TODO_RE.test(joined)) return null;
|
|
2966
|
+
if (PLAN_REFERENCE_RES.some((re) => re.test(joined))) return "plan/process reference";
|
|
2967
|
+
if (BEFORE_AFTER_RES.some((re) => re.test(joined))) return "before/after state narration";
|
|
2968
|
+
return null;
|
|
2969
|
+
};
|
|
2970
|
+
const detectMetaComments = async (context) => {
|
|
2971
|
+
const files = getSourceFiles(context);
|
|
2972
|
+
const diagnostics = [];
|
|
2973
|
+
for (const filePath of files) {
|
|
2974
|
+
const ext = path.extname(filePath);
|
|
2975
|
+
if (!SUPPORTED_EXTS.has(ext)) continue;
|
|
2976
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2977
|
+
const syntax = getCommentSyntax(ext);
|
|
2978
|
+
if (!syntax) continue;
|
|
2979
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2980
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
2981
|
+
let content;
|
|
2982
|
+
try {
|
|
2983
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2984
|
+
} catch {
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
2988
|
+
for (const block of blocks) {
|
|
2989
|
+
const reason = matchMetaSignal(block);
|
|
2990
|
+
if (!reason) continue;
|
|
2991
|
+
diagnostics.push({
|
|
2992
|
+
filePath: relativePath,
|
|
2993
|
+
engine: "ai-slop",
|
|
2994
|
+
rule: "ai-slop/meta-comment",
|
|
2995
|
+
severity: "warning",
|
|
2996
|
+
message: `Meta/plan comment (${reason})`,
|
|
2997
|
+
help: "Remove — references to the build plan or before/after code state belong in PR descriptions and commit messages, not source.",
|
|
2998
|
+
line: block.startLine,
|
|
2999
|
+
column: 0,
|
|
3000
|
+
category: "Comments",
|
|
3001
|
+
fixable: false
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return diagnostics;
|
|
3006
|
+
};
|
|
3007
|
+
|
|
2535
3008
|
//#endregion
|
|
2536
3009
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2537
3010
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
@@ -2763,6 +3236,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
2763
3236
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
2764
3237
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2765
3238
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
3239
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
3240
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
3241
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
3242
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
3243
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
2766
3244
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2767
3245
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2768
3246
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2815,6 +3293,13 @@ const pushFinding = (out, a) => {
|
|
|
2815
3293
|
fixable: false
|
|
2816
3294
|
});
|
|
2817
3295
|
};
|
|
3296
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
3297
|
+
pushFinding(out, {
|
|
3298
|
+
relPath,
|
|
3299
|
+
line,
|
|
3300
|
+
...finding
|
|
3301
|
+
});
|
|
3302
|
+
};
|
|
2818
3303
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2819
3304
|
for (let i = 0; i < lines.length; i++) {
|
|
2820
3305
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2896,6 +3381,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2896
3381
|
});
|
|
2897
3382
|
}
|
|
2898
3383
|
};
|
|
3384
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
3385
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3386
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
3387
|
+
if (!match) continue;
|
|
3388
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3389
|
+
rule: "ai-slop/python-range-len-loop",
|
|
3390
|
+
severity: "info",
|
|
3391
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
3392
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
3397
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3398
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
3399
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3400
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
3401
|
+
severity: "warning",
|
|
3402
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
3403
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
3408
|
+
let count = 1;
|
|
3409
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3410
|
+
const line = lines[i];
|
|
3411
|
+
const trimmed = line.trim();
|
|
3412
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
3413
|
+
const match = pattern.exec(line);
|
|
3414
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
3415
|
+
count++;
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
3419
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
3420
|
+
}
|
|
3421
|
+
return count;
|
|
3422
|
+
};
|
|
3423
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
3424
|
+
const reported = /* @__PURE__ */ new Set();
|
|
3425
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3426
|
+
if (reported.has(i)) continue;
|
|
3427
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
3428
|
+
if (valueMatch) {
|
|
3429
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
3430
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
3431
|
+
reported.add(i);
|
|
3432
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3433
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
3434
|
+
severity: "warning",
|
|
3435
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
3436
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
continue;
|
|
3440
|
+
}
|
|
3441
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
3442
|
+
if (!instanceMatch) continue;
|
|
3443
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
3444
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
3445
|
+
reported.add(i);
|
|
3446
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
3447
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
3448
|
+
severity: "warning",
|
|
3449
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
3450
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
};
|
|
2899
3454
|
const detectPythonPatterns = async (context) => {
|
|
2900
3455
|
const diagnostics = [];
|
|
2901
3456
|
const files = getSourceFiles(context);
|
|
@@ -2915,6 +3470,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2915
3470
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2916
3471
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2917
3472
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
3473
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
3474
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
3475
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2918
3476
|
}
|
|
2919
3477
|
return diagnostics;
|
|
2920
3478
|
};
|
|
@@ -3055,6 +3613,143 @@ const detectRustPatterns = async (context) => {
|
|
|
3055
3613
|
return diagnostics;
|
|
3056
3614
|
};
|
|
3057
3615
|
|
|
3616
|
+
//#endregion
|
|
3617
|
+
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3618
|
+
const JS_EXTS$1 = new Set([
|
|
3619
|
+
".ts",
|
|
3620
|
+
".tsx",
|
|
3621
|
+
".js",
|
|
3622
|
+
".jsx",
|
|
3623
|
+
".mjs",
|
|
3624
|
+
".cjs"
|
|
3625
|
+
]);
|
|
3626
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3627
|
+
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3628
|
+
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3629
|
+
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3630
|
+
const extractCatchBody = (content, openBraceIndex) => {
|
|
3631
|
+
let depth = 0;
|
|
3632
|
+
let inString = null;
|
|
3633
|
+
for (let i = openBraceIndex; i < content.length; i += 1) {
|
|
3634
|
+
const ch = content[i];
|
|
3635
|
+
const prev = content[i - 1];
|
|
3636
|
+
if (inString) {
|
|
3637
|
+
if (ch === inString && prev !== "\\") inString = null;
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
3641
|
+
inString = ch;
|
|
3642
|
+
continue;
|
|
3643
|
+
}
|
|
3644
|
+
if (ch === "{") depth += 1;
|
|
3645
|
+
else if (ch === "}") {
|
|
3646
|
+
depth -= 1;
|
|
3647
|
+
if (depth === 0) return content.slice(openBraceIndex + 1, i);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return null;
|
|
3651
|
+
};
|
|
3652
|
+
const isLogOnlyBody = (body) => {
|
|
3653
|
+
const statements = stripBlockComments(body).split("\n").map((line) => line.replace(/\/\/.*$/, "").trim()).filter((line) => line.length > 0 && line !== ";");
|
|
3654
|
+
if (statements.length === 0) return false;
|
|
3655
|
+
if (statements.some((line) => HANDLING_TOKEN_RE.test(line))) return false;
|
|
3656
|
+
let sawLog = false;
|
|
3657
|
+
for (const statement of statements) {
|
|
3658
|
+
const normalized = statement.replace(/;+$/, "");
|
|
3659
|
+
if (LOG_STATEMENT_RE.test(normalized)) {
|
|
3660
|
+
sawLog = true;
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
if (/^[\w$"'`{[(),.\s+:-]+$/.test(normalized) && !/[=(]\s*(?:async\s+)?\(/.test(normalized)) continue;
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
return sawLog;
|
|
3667
|
+
};
|
|
3668
|
+
const detectJsSilentRecovery = (content, relPath) => {
|
|
3669
|
+
const out = [];
|
|
3670
|
+
CATCH_HEAD_RE.lastIndex = 0;
|
|
3671
|
+
let match;
|
|
3672
|
+
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3673
|
+
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3674
|
+
if (body === null) continue;
|
|
3675
|
+
if (!isLogOnlyBody(body)) continue;
|
|
3676
|
+
const line = content.slice(0, match.index).split("\n").length;
|
|
3677
|
+
out.push({
|
|
3678
|
+
filePath: relPath,
|
|
3679
|
+
engine: "ai-slop",
|
|
3680
|
+
rule: "ai-slop/silent-recovery",
|
|
3681
|
+
severity: "warning",
|
|
3682
|
+
message: "Catch only logs then continues, leaving execution in a possibly broken state",
|
|
3683
|
+
help: "Handle the error: rethrow, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3684
|
+
line,
|
|
3685
|
+
column: 0,
|
|
3686
|
+
category: "AI Slop",
|
|
3687
|
+
fixable: false
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
return out;
|
|
3691
|
+
};
|
|
3692
|
+
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3693
|
+
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3694
|
+
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3695
|
+
const detectPySilentRecovery = (content, relPath) => {
|
|
3696
|
+
const out = [];
|
|
3697
|
+
const lines = content.split("\n");
|
|
3698
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3699
|
+
const exceptMatch = PY_EXCEPT_RE.exec(lines[i]);
|
|
3700
|
+
if (!exceptMatch) continue;
|
|
3701
|
+
const indent = exceptMatch[1].length;
|
|
3702
|
+
const bodyLines = [];
|
|
3703
|
+
let j = i + 1;
|
|
3704
|
+
for (; j < lines.length; j += 1) {
|
|
3705
|
+
const raw = lines[j];
|
|
3706
|
+
if (raw.trim() === "") continue;
|
|
3707
|
+
if (raw.length - raw.trimStart().length <= indent) break;
|
|
3708
|
+
bodyLines.push(raw.trim());
|
|
3709
|
+
}
|
|
3710
|
+
if (bodyLines.length === 0) continue;
|
|
3711
|
+
if (bodyLines.some((line) => PY_HANDLING_TOKEN_RE.test(line))) continue;
|
|
3712
|
+
if (bodyLines.some((line) => line === "pass")) continue;
|
|
3713
|
+
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3714
|
+
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3715
|
+
if (!allLogs || !sawLog) continue;
|
|
3716
|
+
out.push({
|
|
3717
|
+
filePath: relPath,
|
|
3718
|
+
engine: "ai-slop",
|
|
3719
|
+
rule: "ai-slop/silent-recovery",
|
|
3720
|
+
severity: "warning",
|
|
3721
|
+
message: "except only logs then continues, leaving execution in a possibly broken state",
|
|
3722
|
+
help: "Handle the error: re-raise, return an error value, or recover explicitly. Logging alone lets the program proceed as if nothing failed.",
|
|
3723
|
+
line: i + 1,
|
|
3724
|
+
column: 0,
|
|
3725
|
+
category: "AI Slop",
|
|
3726
|
+
fixable: false
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
return out;
|
|
3730
|
+
};
|
|
3731
|
+
const detectSilentRecovery = async (context) => {
|
|
3732
|
+
const files = getSourceFiles(context);
|
|
3733
|
+
const diagnostics = [];
|
|
3734
|
+
for (const filePath of files) {
|
|
3735
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3736
|
+
const ext = path.extname(filePath);
|
|
3737
|
+
const isJs = JS_EXTS$1.has(ext);
|
|
3738
|
+
if (!isJs && !(ext === ".py")) continue;
|
|
3739
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3740
|
+
if (isNonProductionPath(relPath)) continue;
|
|
3741
|
+
let content;
|
|
3742
|
+
try {
|
|
3743
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3744
|
+
} catch {
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
if (isJs) diagnostics.push(...detectJsSilentRecovery(content, relPath));
|
|
3748
|
+
else diagnostics.push(...detectPySilentRecovery(content, relPath));
|
|
3749
|
+
}
|
|
3750
|
+
return diagnostics;
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3058
3753
|
//#endregion
|
|
3059
3754
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
3060
3755
|
const JS_EXTENSIONS$1 = new Set([
|
|
@@ -3244,15 +3939,19 @@ const aiSlopEngine = {
|
|
|
3244
3939
|
const results = await Promise.allSettled([
|
|
3245
3940
|
detectTrivialComments(context),
|
|
3246
3941
|
detectSwallowedExceptions(context),
|
|
3942
|
+
detectDefensivePatterns(context),
|
|
3247
3943
|
detectOverAbstraction(context),
|
|
3248
3944
|
detectDeadPatterns(context),
|
|
3249
3945
|
detectUnusedImports(context),
|
|
3250
3946
|
detectNarrativeComments(context),
|
|
3251
3947
|
detectDuplicateImports(context),
|
|
3948
|
+
detectHardcodedConfigLiterals(context),
|
|
3252
3949
|
detectPythonPatterns(context),
|
|
3253
3950
|
detectGoPatterns(context),
|
|
3254
3951
|
detectRustPatterns(context),
|
|
3255
|
-
detectHallucinatedImports(context)
|
|
3952
|
+
detectHallucinatedImports(context),
|
|
3953
|
+
detectSilentRecovery(context),
|
|
3954
|
+
detectMetaComments(context)
|
|
3256
3955
|
]);
|
|
3257
3956
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
3258
3957
|
return {
|
|
@@ -6151,7 +6850,7 @@ const RISKY_PATTERNS = [
|
|
|
6151
6850
|
help: "Use JSON, MessagePack, or other safe serialization formats for untrusted data"
|
|
6152
6851
|
},
|
|
6153
6852
|
{
|
|
6154
|
-
pattern: new RegExp(
|
|
6853
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\bexec\\s*\\(`, "g"),
|
|
6155
6854
|
extensions: [".py"],
|
|
6156
6855
|
name: "python-exec",
|
|
6157
6856
|
message: "Use of exec() can execute arbitrary code",
|
|
@@ -6855,7 +7554,7 @@ const parseClaudeStdin = (raw) => {
|
|
|
6855
7554
|
return {};
|
|
6856
7555
|
}
|
|
6857
7556
|
};
|
|
6858
|
-
const readStdin$
|
|
7557
|
+
const readStdin$3 = async () => {
|
|
6859
7558
|
if (process.stdin.isTTY) return "";
|
|
6860
7559
|
const chunks = [];
|
|
6861
7560
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -6873,7 +7572,7 @@ const renderClaudeOutput = (additional, block) => {
|
|
|
6873
7572
|
return out;
|
|
6874
7573
|
};
|
|
6875
7574
|
const runClaudeHook = async (deps = {}) => {
|
|
6876
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7575
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
6877
7576
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6878
7577
|
const input = parseClaudeStdin(await getStdin());
|
|
6879
7578
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -6920,7 +7619,7 @@ const parseClaudeFileChangedStdin = (raw) => {
|
|
|
6920
7619
|
}
|
|
6921
7620
|
};
|
|
6922
7621
|
const runClaudeFileChangedHook = async (deps = {}) => {
|
|
6923
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7622
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
6924
7623
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6925
7624
|
const input = parseClaudeFileChangedStdin(await getStdin());
|
|
6926
7625
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -6956,7 +7655,7 @@ const parseClaudeStopStdin = (raw) => {
|
|
|
6956
7655
|
}
|
|
6957
7656
|
};
|
|
6958
7657
|
const runClaudeStopHook = async (deps = {}) => {
|
|
6959
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7658
|
+
const getStdin = deps.stdin ?? readStdin$3;
|
|
6960
7659
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
6961
7660
|
const input = parseClaudeStopStdin(await getStdin());
|
|
6962
7661
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7017,14 +7716,14 @@ const renderCursorOutput = (additional, event = "afterFileEdit") => ({ hookSpeci
|
|
|
7017
7716
|
hookEventName: event,
|
|
7018
7717
|
additionalContext: additional
|
|
7019
7718
|
} });
|
|
7020
|
-
const readStdin$
|
|
7719
|
+
const readStdin$2 = async () => {
|
|
7021
7720
|
if (process.stdin.isTTY) return "";
|
|
7022
7721
|
const chunks = [];
|
|
7023
7722
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7024
7723
|
return Buffer.concat(chunks).toString("utf-8");
|
|
7025
7724
|
};
|
|
7026
7725
|
const runCursorHook = async (deps = {}) => {
|
|
7027
|
-
const getStdin = deps.stdin ?? readStdin$
|
|
7726
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
7028
7727
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7029
7728
|
const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
|
|
7030
7729
|
const input = parseCursorStdin(await getStdin());
|
|
@@ -7080,14 +7779,14 @@ const renderGeminiOutput = (additional) => ({ hookSpecificOutput: {
|
|
|
7080
7779
|
hookEventName: "AfterTool",
|
|
7081
7780
|
additionalContext: additional
|
|
7082
7781
|
} });
|
|
7083
|
-
const readStdin = async () => {
|
|
7782
|
+
const readStdin$1 = async () => {
|
|
7084
7783
|
if (process.stdin.isTTY) return "";
|
|
7085
7784
|
const chunks = [];
|
|
7086
7785
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7087
7786
|
return Buffer.concat(chunks).toString("utf-8");
|
|
7088
7787
|
};
|
|
7089
7788
|
const runGeminiHook = async (deps = {}) => {
|
|
7090
|
-
const getStdin = deps.stdin ?? readStdin;
|
|
7789
|
+
const getStdin = deps.stdin ?? readStdin$1;
|
|
7091
7790
|
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7092
7791
|
const input = parseGeminiStdin(await getStdin());
|
|
7093
7792
|
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
@@ -7119,6 +7818,76 @@ const runGeminiHook = async (deps = {}) => {
|
|
|
7119
7818
|
}
|
|
7120
7819
|
};
|
|
7121
7820
|
|
|
7821
|
+
//#endregion
|
|
7822
|
+
//#region src/hooks/adapters/pi.ts
|
|
7823
|
+
const parsePiStdin = (raw) => {
|
|
7824
|
+
if (!raw.trim()) return {};
|
|
7825
|
+
try {
|
|
7826
|
+
return JSON.parse(raw);
|
|
7827
|
+
} catch {
|
|
7828
|
+
return {};
|
|
7829
|
+
}
|
|
7830
|
+
};
|
|
7831
|
+
const readStdin = async () => {
|
|
7832
|
+
if (process.stdin.isTTY) return "";
|
|
7833
|
+
const chunks = [];
|
|
7834
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7835
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
7836
|
+
};
|
|
7837
|
+
const formatPiMessage = (feedback) => {
|
|
7838
|
+
if (feedback.counts.total === 0 && !feedback.regressed) return "";
|
|
7839
|
+
const { error, warning } = feedback.counts;
|
|
7840
|
+
const header = `aislop: score ${feedback.score}/100${feedback.baseline != null ? ` (baseline ${feedback.baseline})` : ""}, ${error} error${error === 1 ? "" : "s"}, ${warning} warning${warning === 1 ? "" : "s"}.`;
|
|
7841
|
+
const lines = feedback.findings.map((f) => ` - ${f.file}:${f.line} [${f.severity}] ${f.ruleId}: ${f.message}`);
|
|
7842
|
+
if (feedback.elided && feedback.elided > 0) lines.push(` ...and ${feedback.elided} more.`);
|
|
7843
|
+
const tail = feedback.nextSteps.length > 0 ? `\n${feedback.nextSteps.join("\n")}` : "";
|
|
7844
|
+
return `${header}\n${lines.join("\n")}${tail}`;
|
|
7845
|
+
};
|
|
7846
|
+
const runPiHook = async (deps = {}) => {
|
|
7847
|
+
const getStdin = deps.stdin ?? readStdin;
|
|
7848
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
7849
|
+
const input = parsePiStdin(await getStdin());
|
|
7850
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
7851
|
+
const files = resolveHookFiles(cwd, input.file_path ? [input.file_path] : []);
|
|
7852
|
+
if (files.length === 0) return 0;
|
|
7853
|
+
const release = acquireHookLock(cwd);
|
|
7854
|
+
if (!release) return 0;
|
|
7855
|
+
try {
|
|
7856
|
+
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
7857
|
+
const baseline = readBaseline(cwd);
|
|
7858
|
+
appendSessionFiles(cwd, files);
|
|
7859
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
|
|
7860
|
+
score: baseline.score,
|
|
7861
|
+
findingFingerprints: baseline.findingFingerprints
|
|
7862
|
+
} : void 0, {
|
|
7863
|
+
agent: "pi",
|
|
7864
|
+
touchedFiles: files
|
|
7865
|
+
});
|
|
7866
|
+
track({
|
|
7867
|
+
event: "hook_scan_completed",
|
|
7868
|
+
properties: buildHookScanCompletedProps({
|
|
7869
|
+
agent: "pi",
|
|
7870
|
+
score,
|
|
7871
|
+
scoreDelta: baseline ? score - baseline.score : null,
|
|
7872
|
+
findingCount: diagnostics.length,
|
|
7873
|
+
fileCount: files.length
|
|
7874
|
+
})
|
|
7875
|
+
});
|
|
7876
|
+
const output = {
|
|
7877
|
+
schema: "aislop.hook.v2",
|
|
7878
|
+
block: feedback.counts.error > 0 || feedback.regressed,
|
|
7879
|
+
message: formatPiMessage(feedback),
|
|
7880
|
+
feedback
|
|
7881
|
+
};
|
|
7882
|
+
write(JSON.stringify(output));
|
|
7883
|
+
return 0;
|
|
7884
|
+
} catch {
|
|
7885
|
+
return 0;
|
|
7886
|
+
} finally {
|
|
7887
|
+
release();
|
|
7888
|
+
}
|
|
7889
|
+
};
|
|
7890
|
+
|
|
7122
7891
|
//#endregion
|
|
7123
7892
|
//#region src/hooks/assets.ts
|
|
7124
7893
|
const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
@@ -7689,6 +8458,67 @@ const uninstallKilocode = (opts) => {
|
|
|
7689
8458
|
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
7690
8459
|
};
|
|
7691
8460
|
|
|
8461
|
+
//#endregion
|
|
8462
|
+
//#region src/hooks/install/pi.ts
|
|
8463
|
+
const resolvePiPaths = (opts) => {
|
|
8464
|
+
return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
|
|
8465
|
+
};
|
|
8466
|
+
const PI_EXTENSION_SOURCE = `// aislop — auto-generated pi extension. Do not edit by hand.
|
|
8467
|
+
// Reinstall with: aislop hook install --pi
|
|
8468
|
+
import { spawnSync } from "node:child_process";
|
|
8469
|
+
|
|
8470
|
+
export default function (pi) {
|
|
8471
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
8472
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
8473
|
+
if (event.isError) return;
|
|
8474
|
+
const filePath = event.input && event.input.path;
|
|
8475
|
+
if (typeof filePath !== "string" || filePath.length === 0) return;
|
|
8476
|
+
|
|
8477
|
+
const bin = process.env.AISLOP_BIN || "aislop";
|
|
8478
|
+
const payload = JSON.stringify({
|
|
8479
|
+
cwd: ctx.cwd,
|
|
8480
|
+
file_path: filePath,
|
|
8481
|
+
tool_name: event.toolName,
|
|
8482
|
+
});
|
|
8483
|
+
|
|
8484
|
+
let out;
|
|
8485
|
+
try {
|
|
8486
|
+
const res = spawnSync(bin, ["hook", "pi"], {
|
|
8487
|
+
input: payload,
|
|
8488
|
+
encoding: "utf-8",
|
|
8489
|
+
timeout: 15000,
|
|
8490
|
+
});
|
|
8491
|
+
if (res.status !== 0 || !res.stdout) return;
|
|
8492
|
+
out = JSON.parse(res.stdout);
|
|
8493
|
+
} catch {
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
if (!out || !out.message) return;
|
|
8497
|
+
|
|
8498
|
+
return {
|
|
8499
|
+
content: [...event.content, { type: "text", text: out.message }],
|
|
8500
|
+
isError: event.isError,
|
|
8501
|
+
};
|
|
8502
|
+
});
|
|
8503
|
+
}
|
|
8504
|
+
`;
|
|
8505
|
+
const installPi = (opts) => {
|
|
8506
|
+
const paths = resolvePiPaths(opts);
|
|
8507
|
+
const result = emptyResult();
|
|
8508
|
+
applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi aislop extension");
|
|
8509
|
+
return result;
|
|
8510
|
+
};
|
|
8511
|
+
const uninstallPi = (opts) => {
|
|
8512
|
+
const paths = resolvePiPaths(opts);
|
|
8513
|
+
const result = {
|
|
8514
|
+
removed: [],
|
|
8515
|
+
skipped: []
|
|
8516
|
+
};
|
|
8517
|
+
if (readIfExists(paths.extension) != null) applyRemoval(result, opts, paths.extension, null);
|
|
8518
|
+
else result.skipped.push(paths.extension);
|
|
8519
|
+
return result;
|
|
8520
|
+
};
|
|
8521
|
+
|
|
7692
8522
|
//#endregion
|
|
7693
8523
|
//#region src/hooks/install/windsurf.ts
|
|
7694
8524
|
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
@@ -7717,6 +8547,7 @@ const ALL_AGENTS = [
|
|
|
7717
8547
|
"claude",
|
|
7718
8548
|
"cursor",
|
|
7719
8549
|
"gemini",
|
|
8550
|
+
"pi",
|
|
7720
8551
|
"codex",
|
|
7721
8552
|
"windsurf",
|
|
7722
8553
|
"cline",
|
|
@@ -7735,6 +8566,7 @@ const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
|
7735
8566
|
"claude",
|
|
7736
8567
|
"cursor",
|
|
7737
8568
|
"gemini",
|
|
8569
|
+
"pi",
|
|
7738
8570
|
"codex"
|
|
7739
8571
|
];
|
|
7740
8572
|
const paths = {
|
|
@@ -7758,6 +8590,7 @@ const paths = {
|
|
|
7758
8590
|
p.geminiMd
|
|
7759
8591
|
];
|
|
7760
8592
|
},
|
|
8593
|
+
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
7761
8594
|
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
7762
8595
|
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
7763
8596
|
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
@@ -7781,6 +8614,11 @@ const REGISTRY = {
|
|
|
7781
8614
|
uninstall: uninstallGemini,
|
|
7782
8615
|
paths: paths.gemini
|
|
7783
8616
|
},
|
|
8617
|
+
pi: {
|
|
8618
|
+
install: installPi,
|
|
8619
|
+
uninstall: uninstallPi,
|
|
8620
|
+
paths: paths.pi
|
|
8621
|
+
},
|
|
7784
8622
|
codex: {
|
|
7785
8623
|
install: installCodex,
|
|
7786
8624
|
uninstall: uninstallCodex,
|
|
@@ -7901,6 +8739,10 @@ const AGENT_LABELS = {
|
|
|
7901
8739
|
label: "Gemini CLI",
|
|
7902
8740
|
hint: "AfterTool, runtime"
|
|
7903
8741
|
},
|
|
8742
|
+
pi: {
|
|
8743
|
+
label: "pi",
|
|
8744
|
+
hint: "extension, runtime"
|
|
8745
|
+
},
|
|
7904
8746
|
codex: {
|
|
7905
8747
|
label: "Codex CLI",
|
|
7906
8748
|
hint: "rules-only"
|
|
@@ -8007,6 +8849,7 @@ const hookRun = async (agent, flags) => {
|
|
|
8007
8849
|
else exitCode = await runClaudeHook();
|
|
8008
8850
|
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
8009
8851
|
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
8852
|
+
else if (agent === "pi") exitCode = await runPiHook();
|
|
8010
8853
|
else {
|
|
8011
8854
|
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
8012
8855
|
process.exit(0);
|
|
@@ -8069,6 +8912,7 @@ const AGENT_NAMES = [
|
|
|
8069
8912
|
"claude",
|
|
8070
8913
|
"cursor",
|
|
8071
8914
|
"gemini",
|
|
8915
|
+
"pi",
|
|
8072
8916
|
"codex",
|
|
8073
8917
|
"windsurf",
|
|
8074
8918
|
"cline",
|
|
@@ -8191,6 +9035,9 @@ const registerCallbacks = (hook) => {
|
|
|
8191
9035
|
hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
8192
9036
|
await hookRun("gemini");
|
|
8193
9037
|
});
|
|
9038
|
+
hook.command("pi").description("Internal: pi extension tool_result callback (reads stdin)").action(async () => {
|
|
9039
|
+
await hookRun("pi");
|
|
9040
|
+
});
|
|
8194
9041
|
};
|
|
8195
9042
|
const registerHookCommand = (program) => {
|
|
8196
9043
|
const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
|
|
@@ -8523,6 +9370,30 @@ const printEngineStatus = (result) => {
|
|
|
8523
9370
|
}
|
|
8524
9371
|
};
|
|
8525
9372
|
|
|
9373
|
+
//#endregion
|
|
9374
|
+
//#region src/scoring/rule-severity.ts
|
|
9375
|
+
/**
|
|
9376
|
+
* Apply per-rule severity overrides from config: "off" drops the diagnostic,
|
|
9377
|
+
* "error"/"warning" rewrite its severity before scoring and rendering.
|
|
9378
|
+
*/
|
|
9379
|
+
const applyRuleSeverities = (diagnostics, overrides) => {
|
|
9380
|
+
if (Object.keys(overrides).length === 0) return diagnostics;
|
|
9381
|
+
const result = [];
|
|
9382
|
+
for (const diagnostic of diagnostics) {
|
|
9383
|
+
const override = overrides[diagnostic.rule];
|
|
9384
|
+
if (!override) {
|
|
9385
|
+
result.push(diagnostic);
|
|
9386
|
+
continue;
|
|
9387
|
+
}
|
|
9388
|
+
if (override === "off") continue;
|
|
9389
|
+
result.push(override === diagnostic.severity ? diagnostic : {
|
|
9390
|
+
...diagnostic,
|
|
9391
|
+
severity: override
|
|
9392
|
+
});
|
|
9393
|
+
}
|
|
9394
|
+
return result;
|
|
9395
|
+
};
|
|
9396
|
+
|
|
8526
9397
|
//#endregion
|
|
8527
9398
|
//#region src/ui/header.ts
|
|
8528
9399
|
const TAGLINE = "the quality gate for agentic coding";
|
|
@@ -8700,6 +9571,11 @@ const RULE_LABELS = {
|
|
|
8700
9571
|
"knip/types": "Unused type",
|
|
8701
9572
|
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
8702
9573
|
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
9574
|
+
"ai-slop/silent-recovery": "Catch logs then continues",
|
|
9575
|
+
"ai-slop/meta-comment": "Meta/plan comment",
|
|
9576
|
+
"ai-slop/redundant-try-catch": "Redundant try/catch",
|
|
9577
|
+
"ai-slop/redundant-type-coercion": "Redundant type coercion",
|
|
9578
|
+
"ai-slop/duplicate-type-declaration": "Duplicate exported type",
|
|
8703
9579
|
"ai-slop/thin-wrapper": "Thin function wrapper",
|
|
8704
9580
|
"ai-slop/generic-naming": "Generic/vague identifier name",
|
|
8705
9581
|
"ai-slop/unused-import": "Unused import",
|
|
@@ -8713,10 +9589,16 @@ const RULE_LABELS = {
|
|
|
8713
9589
|
"ai-slop/ts-directive": "@ts-ignore / @ts-expect-error",
|
|
8714
9590
|
"ai-slop/narrative-comment": "Narrative comment block",
|
|
8715
9591
|
"ai-slop/duplicate-import": "Duplicate import statement",
|
|
9592
|
+
"ai-slop/hardcoded-url": "Hardcoded URL",
|
|
9593
|
+
"ai-slop/hardcoded-id": "Hardcoded provider ID",
|
|
8716
9594
|
"ai-slop/python-bare-except": "Bare except",
|
|
8717
9595
|
"ai-slop/python-broad-except": "Broad except",
|
|
8718
9596
|
"ai-slop/python-mutable-default": "Mutable default argument",
|
|
8719
9597
|
"ai-slop/python-print-debug": "print() left in code",
|
|
9598
|
+
"ai-slop/python-range-len-loop": "range(len(...)) loop",
|
|
9599
|
+
"ai-slop/python-chained-dict-get": "Chained dict get",
|
|
9600
|
+
"ai-slop/python-repetitive-dispatch": "Repetitive dispatch ladder",
|
|
9601
|
+
"ai-slop/python-isinstance-ladder": "isinstance ladder",
|
|
8720
9602
|
"ai-slop/go-library-panic": "panic() in Go library code",
|
|
8721
9603
|
"ai-slop/rust-non-test-unwrap": "Rust .unwrap() in production code",
|
|
8722
9604
|
"ai-slop/rust-todo-stub": "Rust todo!() stub",
|
|
@@ -8820,6 +9702,9 @@ const renderSummary = (input, deps = {}) => {
|
|
|
8820
9702
|
}
|
|
8821
9703
|
return lines.join("\n");
|
|
8822
9704
|
};
|
|
9705
|
+
const renderStarCta = (deps = {}) => {
|
|
9706
|
+
return `\n ${style(deps.theme ?? theme, "muted", "★ Found this useful? Star us at github.com/scanaislop/aislop")}\n`;
|
|
9707
|
+
};
|
|
8823
9708
|
const renderCleanRun = (input, deps = {}) => {
|
|
8824
9709
|
const t = deps.theme ?? theme;
|
|
8825
9710
|
const s = deps.symbols ?? symbols;
|
|
@@ -8832,8 +9717,54 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
8832
9717
|
return `\n ${parts.join(` ${sep} `)}\n`;
|
|
8833
9718
|
};
|
|
8834
9719
|
|
|
9720
|
+
//#endregion
|
|
9721
|
+
//#region src/utils/history.ts
|
|
9722
|
+
const HISTORY_FILE = "history.jsonl";
|
|
9723
|
+
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
9724
|
+
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
9725
|
+
/**
|
|
9726
|
+
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
9727
|
+
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
9728
|
+
*/
|
|
9729
|
+
const appendHistory = (input) => {
|
|
9730
|
+
if (isHistoryDisabled()) return;
|
|
9731
|
+
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
9732
|
+
if (!fs.existsSync(configDir)) return;
|
|
9733
|
+
const record = {
|
|
9734
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9735
|
+
score: input.score,
|
|
9736
|
+
errors: input.errors,
|
|
9737
|
+
warnings: input.warnings,
|
|
9738
|
+
files: input.files,
|
|
9739
|
+
cliVersion: APP_VERSION
|
|
9740
|
+
};
|
|
9741
|
+
try {
|
|
9742
|
+
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
9743
|
+
} catch {}
|
|
9744
|
+
};
|
|
9745
|
+
const isHistoryRecord = (value) => {
|
|
9746
|
+
if (!value || typeof value !== "object") return false;
|
|
9747
|
+
const record = value;
|
|
9748
|
+
return typeof record.timestamp === "string" && typeof record.score === "number" && typeof record.errors === "number" && typeof record.warnings === "number" && typeof record.files === "number" && typeof record.cliVersion === "string";
|
|
9749
|
+
};
|
|
9750
|
+
const readHistory = (directory) => {
|
|
9751
|
+
const file = historyPath(directory);
|
|
9752
|
+
if (!fs.existsSync(file)) return [];
|
|
9753
|
+
const records = [];
|
|
9754
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
9755
|
+
const trimmed = line.trim();
|
|
9756
|
+
if (!trimmed) continue;
|
|
9757
|
+
try {
|
|
9758
|
+
const parsed = JSON.parse(trimmed);
|
|
9759
|
+
if (isHistoryRecord(parsed)) records.push(parsed);
|
|
9760
|
+
} catch {}
|
|
9761
|
+
}
|
|
9762
|
+
return records;
|
|
9763
|
+
};
|
|
9764
|
+
|
|
8835
9765
|
//#endregion
|
|
8836
9766
|
//#region src/commands/scan.ts
|
|
9767
|
+
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
8837
9768
|
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
8838
9769
|
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
8839
9770
|
const BREAKDOWN_TOP_N = 10;
|
|
@@ -8889,11 +9820,12 @@ const buildScanRender = (input) => {
|
|
|
8889
9820
|
const warnings = input.diagnostics.filter((d) => d.severity === "warning").length;
|
|
8890
9821
|
const fixable = input.diagnostics.filter((d) => d.fixable).length;
|
|
8891
9822
|
const hasVulnerableDeps = input.diagnostics.some((d) => d.rule === "security/vulnerable-dependency");
|
|
9823
|
+
const starCta = input.printBrand !== false ? renderStarCta(deps) : "";
|
|
8892
9824
|
if (input.diagnostics.length === 0 && input.score.score === 100) return `${header}${renderCleanRun({
|
|
8893
9825
|
score: input.score.score,
|
|
8894
9826
|
label: input.score.label,
|
|
8895
9827
|
elapsedMs: input.elapsedMs
|
|
8896
|
-
}, deps)}`;
|
|
9828
|
+
}, deps)}${starCta}`;
|
|
8897
9829
|
const diagBlock = input.diagnostics.length === 0 ? "" : renderDiagnostics(input.diagnostics, input.verbose);
|
|
8898
9830
|
const nextSteps = [];
|
|
8899
9831
|
if (fixable > 0) nextSteps.push({
|
|
@@ -8920,7 +9852,7 @@ const buildScanRender = (input) => {
|
|
|
8920
9852
|
nextSteps,
|
|
8921
9853
|
breakdown: computeBreakdown(input.diagnostics),
|
|
8922
9854
|
thresholds: input.thresholds
|
|
8923
|
-
}, deps)}`;
|
|
9855
|
+
}, deps)}${starCta}`;
|
|
8924
9856
|
};
|
|
8925
9857
|
const scanCommand = async (directory, config, options) => {
|
|
8926
9858
|
const resolvedDir = path.resolve(directory);
|
|
@@ -8947,17 +9879,18 @@ const scanCommand = async (directory, config, options) => {
|
|
|
8947
9879
|
const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
8948
9880
|
const startTime = performance.now();
|
|
8949
9881
|
const showHeader = options.showHeader !== false;
|
|
8950
|
-
const
|
|
9882
|
+
const machineOutput = isMachineOutput(options);
|
|
9883
|
+
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
8951
9884
|
let files;
|
|
8952
9885
|
if (options.staged) {
|
|
8953
9886
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
|
|
8954
|
-
if (!
|
|
9887
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
8955
9888
|
} else if (options.changes) {
|
|
8956
9889
|
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], config.exclude);
|
|
8957
|
-
if (!
|
|
9890
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
|
|
8958
9891
|
} else {
|
|
8959
9892
|
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], config.exclude);
|
|
8960
|
-
if (!
|
|
9893
|
+
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
8961
9894
|
}
|
|
8962
9895
|
const configDir = findConfigDir(resolvedDir);
|
|
8963
9896
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
@@ -8974,7 +9907,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
8974
9907
|
}));
|
|
8975
9908
|
const progressRenderer = useLiveProgress ? new LiveGrid(gridRows) : null;
|
|
8976
9909
|
progressRenderer?.start();
|
|
8977
|
-
const
|
|
9910
|
+
const rawResults = await runEngines({
|
|
8978
9911
|
rootDirectory: resolvedDir,
|
|
8979
9912
|
languages: projectInfo.languages,
|
|
8980
9913
|
frameworks: projectInfo.frameworks,
|
|
@@ -9007,9 +9940,13 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9007
9940
|
elapsedMs: result.elapsed
|
|
9008
9941
|
});
|
|
9009
9942
|
}
|
|
9010
|
-
if (!
|
|
9943
|
+
if (!machineOutput && !progressRenderer) printEngineStatus(result);
|
|
9011
9944
|
});
|
|
9012
9945
|
progressRenderer?.stop();
|
|
9946
|
+
const results = rawResults.map((result) => ({
|
|
9947
|
+
...result,
|
|
9948
|
+
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
9949
|
+
}));
|
|
9013
9950
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
9014
9951
|
const elapsedMs = performance.now() - startTime;
|
|
9015
9952
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
@@ -9030,12 +9967,24 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9030
9967
|
engineIssues,
|
|
9031
9968
|
engineTimings
|
|
9032
9969
|
};
|
|
9970
|
+
if (options.sarif) {
|
|
9971
|
+
const { buildSarifLog } = await import("./sarif-CZVuavf_.js");
|
|
9972
|
+
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
9973
|
+
return completion;
|
|
9974
|
+
}
|
|
9033
9975
|
if (options.json) {
|
|
9034
9976
|
const { buildJsonOutput } = await import("./json-OIzja7OM.js");
|
|
9035
9977
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
9036
9978
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
9037
9979
|
return completion;
|
|
9038
9980
|
}
|
|
9981
|
+
if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
|
|
9982
|
+
directory: resolvedDir,
|
|
9983
|
+
score: scoreResult.score,
|
|
9984
|
+
errors: completion.errorCount,
|
|
9985
|
+
warnings: completion.warningCount,
|
|
9986
|
+
files: projectInfo.sourceFileCount
|
|
9987
|
+
});
|
|
9039
9988
|
const projectName = projectInfo.projectName ?? "project";
|
|
9040
9989
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
9041
9990
|
process.stdout.write(buildScanRender({
|
|
@@ -9062,7 +10011,8 @@ const ciCommand = async (directory, config, options = {}) => {
|
|
|
9062
10011
|
changes: false,
|
|
9063
10012
|
staged: false,
|
|
9064
10013
|
verbose: false,
|
|
9065
|
-
json: !options.human,
|
|
10014
|
+
json: !options.human && !options.sarif,
|
|
10015
|
+
sarif: options.sarif,
|
|
9066
10016
|
command: "ci"
|
|
9067
10017
|
});
|
|
9068
10018
|
} catch (error) {
|
|
@@ -9588,6 +10538,16 @@ const AGENT_CONFIGS = {
|
|
|
9588
10538
|
bin: "goose",
|
|
9589
10539
|
args: (p) => ["run", p]
|
|
9590
10540
|
},
|
|
10541
|
+
pi: {
|
|
10542
|
+
type: "cli",
|
|
10543
|
+
bin: "pi",
|
|
10544
|
+
args: (p) => ["-p", p]
|
|
10545
|
+
},
|
|
10546
|
+
crush: {
|
|
10547
|
+
type: "cli",
|
|
10548
|
+
bin: "crush",
|
|
10549
|
+
args: (p) => ["run", p]
|
|
10550
|
+
},
|
|
9591
10551
|
cursor: {
|
|
9592
10552
|
type: "editor",
|
|
9593
10553
|
bin: "cursor"
|
|
@@ -11442,6 +12402,11 @@ const BUILTIN_RULES = [
|
|
|
11442
12402
|
rules: [
|
|
11443
12403
|
"ai-slop/trivial-comment",
|
|
11444
12404
|
"ai-slop/swallowed-exception",
|
|
12405
|
+
"ai-slop/silent-recovery",
|
|
12406
|
+
"ai-slop/meta-comment",
|
|
12407
|
+
"ai-slop/redundant-try-catch",
|
|
12408
|
+
"ai-slop/redundant-type-coercion",
|
|
12409
|
+
"ai-slop/duplicate-type-declaration",
|
|
11445
12410
|
"ai-slop/thin-wrapper",
|
|
11446
12411
|
"ai-slop/generic-naming",
|
|
11447
12412
|
"ai-slop/unused-import",
|
|
@@ -11455,10 +12420,16 @@ const BUILTIN_RULES = [
|
|
|
11455
12420
|
"ai-slop/ts-directive",
|
|
11456
12421
|
"ai-slop/narrative-comment",
|
|
11457
12422
|
"ai-slop/duplicate-import",
|
|
12423
|
+
"ai-slop/hardcoded-url",
|
|
12424
|
+
"ai-slop/hardcoded-id",
|
|
11458
12425
|
"ai-slop/python-bare-except",
|
|
11459
12426
|
"ai-slop/python-broad-except",
|
|
11460
12427
|
"ai-slop/python-mutable-default",
|
|
11461
12428
|
"ai-slop/python-print-debug",
|
|
12429
|
+
"ai-slop/python-range-len-loop",
|
|
12430
|
+
"ai-slop/python-chained-dict-get",
|
|
12431
|
+
"ai-slop/python-repetitive-dispatch",
|
|
12432
|
+
"ai-slop/python-isinstance-ladder",
|
|
11462
12433
|
"ai-slop/go-library-panic",
|
|
11463
12434
|
"ai-slop/rust-non-test-unwrap",
|
|
11464
12435
|
"ai-slop/rust-todo-stub",
|
|
@@ -11617,6 +12588,73 @@ const interactiveCommand = async (directory, config) => {
|
|
|
11617
12588
|
}
|
|
11618
12589
|
};
|
|
11619
12590
|
|
|
12591
|
+
//#endregion
|
|
12592
|
+
//#region src/commands/trend.ts
|
|
12593
|
+
const SPARK_TICKS = [
|
|
12594
|
+
"▁",
|
|
12595
|
+
"▂",
|
|
12596
|
+
"▃",
|
|
12597
|
+
"▄",
|
|
12598
|
+
"▅",
|
|
12599
|
+
"▆",
|
|
12600
|
+
"▇",
|
|
12601
|
+
"█"
|
|
12602
|
+
];
|
|
12603
|
+
const DEFAULT_LIMIT = 20;
|
|
12604
|
+
const renderSparkline = (scores) => {
|
|
12605
|
+
if (scores.length === 0) return "";
|
|
12606
|
+
const min = Math.min(...scores);
|
|
12607
|
+
const span = Math.max(...scores) - min;
|
|
12608
|
+
return scores.map((score) => {
|
|
12609
|
+
if (span === 0) return SPARK_TICKS[SPARK_TICKS.length - 1];
|
|
12610
|
+
const ratio = (score - min) / span;
|
|
12611
|
+
return SPARK_TICKS[Math.round(ratio * (SPARK_TICKS.length - 1))];
|
|
12612
|
+
}).join("");
|
|
12613
|
+
};
|
|
12614
|
+
const formatDate = (timestamp) => {
|
|
12615
|
+
const date = new Date(timestamp);
|
|
12616
|
+
if (Number.isNaN(date.getTime())) return timestamp;
|
|
12617
|
+
return date.toISOString().slice(0, 16).replace("T", " ");
|
|
12618
|
+
};
|
|
12619
|
+
const delta = (current, previous) => {
|
|
12620
|
+
if (previous === void 0) return "";
|
|
12621
|
+
const diff = current - previous;
|
|
12622
|
+
if (diff > 0) return style(theme, "success", `+${diff}`);
|
|
12623
|
+
if (diff < 0) return style(theme, "danger", `${diff}`);
|
|
12624
|
+
return style(theme, "muted", "0");
|
|
12625
|
+
};
|
|
12626
|
+
const buildTrendRender = (input) => {
|
|
12627
|
+
const header = renderHeader({
|
|
12628
|
+
version: APP_VERSION,
|
|
12629
|
+
command: "trend",
|
|
12630
|
+
context: [],
|
|
12631
|
+
brand: input.printBrand !== false
|
|
12632
|
+
});
|
|
12633
|
+
if (input.records.length === 0) return `${header}\n ${style(theme, "muted", "No score history yet. Run a scan to start tracking trends.")}\n`;
|
|
12634
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
12635
|
+
const recent = input.records.slice(-limit);
|
|
12636
|
+
const scores = recent.map((r) => r.score);
|
|
12637
|
+
const lines = [header];
|
|
12638
|
+
lines.push(` ${style(theme, "dim", padEnd("Date", 18))}${style(theme, "dim", padEnd("Score", 8))}${style(theme, "dim", padEnd("Δ", 6))}${style(theme, "dim", padEnd("Err", 6))}${style(theme, "dim", "Warn")}`);
|
|
12639
|
+
recent.forEach((record, index) => {
|
|
12640
|
+
const previous = index > 0 ? recent[index - 1]?.score : void 0;
|
|
12641
|
+
lines.push(` ${padEnd(formatDate(record.timestamp), 18)}${padEnd(String(record.score), 8)}${padEnd(delta(record.score, previous), 6)}${padEnd(String(record.errors), 6)}${record.warnings}`);
|
|
12642
|
+
});
|
|
12643
|
+
const latest = recent[recent.length - 1];
|
|
12644
|
+
lines.push("");
|
|
12645
|
+
lines.push(` ${style(theme, "accent", renderSparkline(scores))}`);
|
|
12646
|
+
lines.push(` ${style(theme, "muted", `${recent.length} run(s), latest score ${latest?.score}`)}`);
|
|
12647
|
+
lines.push(renderHintLine("Run aislop scan to add a new data point").trimEnd());
|
|
12648
|
+
return `${lines.join("\n")}\n`;
|
|
12649
|
+
};
|
|
12650
|
+
const trendCommand = (directory, limit) => {
|
|
12651
|
+
const records = readHistory(directory);
|
|
12652
|
+
process.stdout.write(buildTrendRender({
|
|
12653
|
+
records,
|
|
12654
|
+
limit
|
|
12655
|
+
}));
|
|
12656
|
+
};
|
|
12657
|
+
|
|
11620
12658
|
//#endregion
|
|
11621
12659
|
//#region src/cli.ts
|
|
11622
12660
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -11632,17 +12670,22 @@ const commaSeparatedParser = (value, previous = []) => {
|
|
|
11632
12670
|
const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
11633
12671
|
return [...previous, ...parts];
|
|
11634
12672
|
};
|
|
12673
|
+
const wantsSarif = (flags) => Boolean(flags.sarif) || flags.format === "sarif";
|
|
12674
|
+
const wantsJson = (flags) => Boolean(flags.json) || flags.format === "json";
|
|
11635
12675
|
const runScan = async (directory, flags) => {
|
|
11636
12676
|
const config = loadConfig(directory);
|
|
11637
|
-
const
|
|
12677
|
+
const finalConfig = {
|
|
11638
12678
|
...config,
|
|
11639
12679
|
exclude: [...config.exclude ?? [], ...flags.exclude ?? []],
|
|
11640
12680
|
include: [...config.include ?? [], ...flags.include ?? []]
|
|
11641
|
-
}
|
|
12681
|
+
};
|
|
12682
|
+
const sarif = wantsSarif(flags);
|
|
12683
|
+
const { exitCode } = await scanCommand(directory, finalConfig, {
|
|
11642
12684
|
changes: Boolean(flags.changes),
|
|
11643
12685
|
staged: Boolean(flags.staged),
|
|
11644
12686
|
verbose: Boolean(flags.verbose),
|
|
11645
|
-
json:
|
|
12687
|
+
json: !sarif && wantsJson(flags),
|
|
12688
|
+
sarif,
|
|
11646
12689
|
exclude: flags.exclude,
|
|
11647
12690
|
include: flags.include
|
|
11648
12691
|
});
|
|
@@ -11651,8 +12694,8 @@ const runScan = async (directory, flags) => {
|
|
|
11651
12694
|
process.exitCode = exitCode;
|
|
11652
12695
|
}
|
|
11653
12696
|
};
|
|
11654
|
-
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
|
|
11655
|
-
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
|
|
12697
|
+
const noFlagsPassed = (flags) => !flags.changes && !flags.staged && !flags.verbose && !flags.json && !flags.sarif && !flags.format && !(flags.exclude && flags.exclude.length > 0) && !(flags.include && flags.include.length > 0);
|
|
12698
|
+
const program = new Command().name("aislop").description("The unified code quality CLI").version(APP_VERSION, "-v, --version").argument("[directory]", "project directory to scan", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON instead of terminal UI").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory, flags) => {
|
|
11656
12699
|
if (noFlagsPassed(flags) && process.stdin.isTTY) try {
|
|
11657
12700
|
await interactiveCommand(directory, loadConfig(directory));
|
|
11658
12701
|
return;
|
|
@@ -11670,6 +12713,7 @@ ${style(theme, "dim", "Commands:")}
|
|
|
11670
12713
|
npx aislop doctor [dir] Check installed tools
|
|
11671
12714
|
npx aislop ci [dir] CI-friendly JSON output
|
|
11672
12715
|
npx aislop rules [dir] List all rules
|
|
12716
|
+
npx aislop trend [dir] Show score history trend
|
|
11673
12717
|
|
|
11674
12718
|
${style(theme, "dim", "Examples:")}
|
|
11675
12719
|
npx aislop Interactive menu
|
|
@@ -11683,12 +12727,14 @@ ${style(theme, "dim", "Examples:")}
|
|
|
11683
12727
|
npx aislop fix --cursor Open Cursor + copy prompt to clipboard
|
|
11684
12728
|
npx aislop fix -p Print a prompt to paste into any coding agent
|
|
11685
12729
|
npx aislop ci JSON output for CI pipelines
|
|
12730
|
+
npx aislop scan --sarif SARIF 2.1.0 for GitHub code scanning
|
|
12731
|
+
npx aislop trend Show score history over time
|
|
11686
12732
|
npx aislop scan --exclude node_modules
|
|
11687
12733
|
npx aislop scan --exclude node_modules,dist,file.txt
|
|
11688
12734
|
npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
|
|
11689
12735
|
${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
|
|
11690
12736
|
`);
|
|
11691
|
-
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
|
|
12737
|
+
program.command("scan [directory]").description("Run full code quality scan").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("-d, --verbose", "show file details per rule").option("--json", "output JSON").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").option("--exclude <patterns>", "comma-separated or repeatable list of paths and files to exclude", commaSeparatedParser, []).option("--include <patterns>", "comma-separated or repeatable list of paths and files to include", commaSeparatedParser, []).action(async (directory = ".", _flags, command) => {
|
|
11692
12738
|
await runScan(directory, command.optsWithGlobals());
|
|
11693
12739
|
});
|
|
11694
12740
|
const FIX_AGENT_FLAGS = [
|
|
@@ -11761,6 +12807,16 @@ const FIX_AGENT_FLAGS = [
|
|
|
11761
12807
|
flag: "goose",
|
|
11762
12808
|
name: "goose",
|
|
11763
12809
|
help: "open Goose to fix remaining issues"
|
|
12810
|
+
},
|
|
12811
|
+
{
|
|
12812
|
+
flag: "pi",
|
|
12813
|
+
name: "pi",
|
|
12814
|
+
help: "open pi to fix remaining issues"
|
|
12815
|
+
},
|
|
12816
|
+
{
|
|
12817
|
+
flag: "crush",
|
|
12818
|
+
name: "crush",
|
|
12819
|
+
help: "open Crush to fix remaining issues"
|
|
11764
12820
|
}
|
|
11765
12821
|
];
|
|
11766
12822
|
const matchFixAgent = (flags) => {
|
|
@@ -11796,9 +12852,12 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
11796
12852
|
return { exitCode: 0 };
|
|
11797
12853
|
});
|
|
11798
12854
|
});
|
|
11799
|
-
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").action(async (directory = ".", _flags, command) => {
|
|
12855
|
+
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").option("--sarif", "output SARIF 2.1.0 (for GitHub code scanning)").option("--format <format>", "output format: json or sarif").action(async (directory = ".", _flags, command) => {
|
|
11800
12856
|
const flags = command.optsWithGlobals();
|
|
11801
|
-
const { exitCode } = await ciCommand(directory, loadConfig(directory), {
|
|
12857
|
+
const { exitCode } = await ciCommand(directory, loadConfig(directory), {
|
|
12858
|
+
human: Boolean(flags.human),
|
|
12859
|
+
sarif: Boolean(flags.sarif) || flags.format === "sarif"
|
|
12860
|
+
});
|
|
11802
12861
|
if (exitCode !== 0) {
|
|
11803
12862
|
await flushTelemetry();
|
|
11804
12863
|
process.exitCode = exitCode;
|
|
@@ -11834,6 +12893,16 @@ program.command("badge [directory]").description("Print the public score badge U
|
|
|
11834
12893
|
process.exit(1);
|
|
11835
12894
|
}
|
|
11836
12895
|
});
|
|
12896
|
+
program.command("trend [directory]").description("Show score history trend from .aislop/history.jsonl").option("--limit <n>", "number of recent runs to show", (v) => Number.parseInt(v, 10)).action(async (directory = ".", _flags, command) => {
|
|
12897
|
+
const flags = command.optsWithGlobals();
|
|
12898
|
+
await withCommandLifecycle({
|
|
12899
|
+
command: "trend",
|
|
12900
|
+
config: loadConfig(directory).telemetry
|
|
12901
|
+
}, async () => {
|
|
12902
|
+
trendCommand(directory, flags.limit);
|
|
12903
|
+
return { exitCode: 0 };
|
|
12904
|
+
});
|
|
12905
|
+
});
|
|
11837
12906
|
registerHookCommand(program);
|
|
11838
12907
|
const main = async () => {
|
|
11839
12908
|
fireInstalledOnce();
|