aislop 0.10.2 → 0.11.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 +151 -69
- package/dist/cli.js +1803 -374
- package/dist/{expo-doctor-T4DswmX5.js → expo-doctor-BM2JR6f6.js} +1 -1
- package/dist/{expo-doctor-c-jE6pR2.js → expo-doctor-BwLKXF__.js} +2 -2
- package/dist/finding-assessment-PCl1fnok.js +149 -0
- package/dist/{generic-BsQa13CS.js → generic-D_T4cUaC.js} +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +1015 -215
- package/dist/{json-B01i-GOz.js → json-0lJPTrwO.js} +5 -3
- package/dist/{json-CXV4D0Ib.js → json-pHsqtKkz.js} +4 -2
- package/dist/mcp.js +540 -97
- package/dist/{sarif-cy5SiDDq.js → sarif-BXUicqQU.js} +1 -1
- package/dist/{sarif-CZVuavf_.js → sarif-CjxSBcqx.js} +1 -1
- package/dist/{typecheck-BdQ7uFyK.js → typecheck-DQSzG8fX.js} +1 -1
- package/dist/{typecheck-wVSohmOX.js → typecheck-yOGXIIGU.js} +1 -1
- package/dist/version-BJA3AcRM.js +7 -0
- package/package.json +2 -2
- package/dist/engine-info-Cpt36DqZ.js +0 -31
- package/dist/version-BfJVwhN2.js +0 -5
- /package/dist/{subprocess-0uXz8HdE.js → subprocess-CQUJDGgn.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import crypto, { randomUUID } from "node:crypto";
|
|
8
9
|
import { performance } from "node:perf_hooks";
|
|
9
10
|
import YAML from "yaml";
|
|
@@ -11,10 +12,11 @@ import { z } from "zod/v4";
|
|
|
11
12
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
12
13
|
import micromatch from "micromatch";
|
|
13
14
|
import ts from "typescript";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
15
|
+
import * as readline from "node:readline";
|
|
16
|
+
import { Writable } from "node:stream";
|
|
16
17
|
import pc from "picocolors";
|
|
17
18
|
import wcwidth from "wcwidth";
|
|
19
|
+
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
18
20
|
|
|
19
21
|
//#region \0rolldown/runtime.js
|
|
20
22
|
var __defProp = Object.defineProperty;
|
|
@@ -34,7 +36,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
36
|
|
|
35
37
|
//#endregion
|
|
36
38
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.
|
|
39
|
+
const APP_VERSION = "0.11.0";
|
|
38
40
|
|
|
39
41
|
//#endregion
|
|
40
42
|
//#region src/telemetry/env.ts
|
|
@@ -441,7 +443,7 @@ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
|
|
|
441
443
|
actions.push({
|
|
442
444
|
id: "run_aislop_fix",
|
|
443
445
|
label: `Run aislop fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
|
|
444
|
-
command: "
|
|
446
|
+
command: "aislop fix",
|
|
445
447
|
rationale: "These findings have deterministic fixes (formatting, unused imports, trivial comments). Running this before any manual work avoids burning agent tokens on what the CLI handles for free.",
|
|
446
448
|
ruleIds
|
|
447
449
|
});
|
|
@@ -601,7 +603,8 @@ const DEFAULT_CONFIG = {
|
|
|
601
603
|
good: 75,
|
|
602
604
|
ok: 50
|
|
603
605
|
},
|
|
604
|
-
smoothing: 20
|
|
606
|
+
smoothing: 20,
|
|
607
|
+
maxPerRule: 40
|
|
605
608
|
},
|
|
606
609
|
ci: {
|
|
607
610
|
failBelow: 70,
|
|
@@ -624,9 +627,9 @@ jobs:
|
|
|
624
627
|
runs-on: ubuntu-latest
|
|
625
628
|
steps:
|
|
626
629
|
- uses: actions/checkout@v4
|
|
627
|
-
- uses: scanaislop/aislop@
|
|
630
|
+
- uses: scanaislop/aislop@v1
|
|
628
631
|
with:
|
|
629
|
-
version:
|
|
632
|
+
version: latest
|
|
630
633
|
`;
|
|
631
634
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
632
635
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -725,7 +728,8 @@ const ScoringSchema = z.object({
|
|
|
725
728
|
good: 75,
|
|
726
729
|
ok: 50
|
|
727
730
|
})),
|
|
728
|
-
smoothing: z.number().nonnegative().default(20)
|
|
731
|
+
smoothing: z.number().nonnegative().default(20),
|
|
732
|
+
maxPerRule: z.number().positive().default(40)
|
|
729
733
|
});
|
|
730
734
|
const CiSchema = z.object({
|
|
731
735
|
failBelow: z.number().default(70),
|
|
@@ -765,7 +769,8 @@ const AislopConfigSchema = z.object({
|
|
|
765
769
|
good: 75,
|
|
766
770
|
ok: 50
|
|
767
771
|
},
|
|
768
|
-
smoothing: 20
|
|
772
|
+
smoothing: 20,
|
|
773
|
+
maxPerRule: 40
|
|
769
774
|
})),
|
|
770
775
|
ci: CiSchema.default(() => ({
|
|
771
776
|
failBelow: 70,
|
|
@@ -1543,8 +1548,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1543
1548
|
|
|
1544
1549
|
//#endregion
|
|
1545
1550
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1546
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1547
|
-
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]
|
|
1551
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|prototypes?|experiments?|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1552
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:(?:prototype|experiment)(?:[-_.][^/]*)?|(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*)\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example|prototype|experiment)\.[mc]?[jt]sx?$/i;
|
|
1548
1553
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1549
1554
|
|
|
1550
1555
|
//#endregion
|
|
@@ -1660,7 +1665,7 @@ const JS_EXTENSIONS$4 = new Set([
|
|
|
1660
1665
|
".mjs",
|
|
1661
1666
|
".cjs"
|
|
1662
1667
|
]);
|
|
1663
|
-
const
|
|
1668
|
+
const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
|
|
1664
1669
|
const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
1665
1670
|
filePath,
|
|
1666
1671
|
engine: "ai-slop",
|
|
@@ -1674,20 +1679,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1674
1679
|
fixable
|
|
1675
1680
|
});
|
|
1676
1681
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1682
|
+
const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
|
|
1683
|
+
const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
|
|
1684
|
+
const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
|
|
1685
|
+
const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
|
|
1686
|
+
const shouldFlagConsoleCall = (trimmed) => {
|
|
1687
|
+
const match = CONSOLE_CALL_PATTERN.exec(trimmed);
|
|
1688
|
+
if (!match) return false;
|
|
1689
|
+
const method = match[1];
|
|
1690
|
+
if (method === "trace" || method === "dir" || method === "table") return true;
|
|
1691
|
+
if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
|
|
1692
|
+
if (method === "info" || method === "log") {
|
|
1693
|
+
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
|
|
1694
|
+
if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
return false;
|
|
1698
|
+
};
|
|
1677
1699
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1678
1700
|
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
1679
1701
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1680
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
1702
|
+
if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
|
|
1703
|
+
if (content.startsWith("#!")) return [];
|
|
1704
|
+
if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
|
|
1681
1705
|
const diagnostics = [];
|
|
1682
1706
|
const lines = content.split("\n");
|
|
1683
1707
|
for (let i = 0; i < lines.length; i++) {
|
|
1684
1708
|
const trimmed = lines[i].trim();
|
|
1685
1709
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1686
|
-
if (
|
|
1687
|
-
if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
|
|
1688
|
-
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
|
|
1689
|
-
diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1690
|
-
}
|
|
1710
|
+
if (shouldFlagConsoleCall(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1691
1711
|
}
|
|
1692
1712
|
return diagnostics;
|
|
1693
1713
|
};
|
|
@@ -1715,6 +1735,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
|
1715
1735
|
const control = contextLines.join(" ");
|
|
1716
1736
|
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1717
1737
|
};
|
|
1738
|
+
const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
|
|
1718
1739
|
const detectTodoStubs = (content, relativePath) => {
|
|
1719
1740
|
const diagnostics = [];
|
|
1720
1741
|
const lines = content.split("\n");
|
|
@@ -1736,7 +1757,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1736
1757
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1737
1758
|
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
|
|
1738
1759
|
if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
|
|
1739
|
-
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
1760
|
+
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ") && !isPropertyNoopAssignment(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
1740
1761
|
}
|
|
1741
1762
|
return diagnostics;
|
|
1742
1763
|
};
|
|
@@ -2234,7 +2255,7 @@ const JS_RESOLUTION_EXTENSIONS = [
|
|
|
2234
2255
|
"/index.js",
|
|
2235
2256
|
"/index.jsx"
|
|
2236
2257
|
];
|
|
2237
|
-
const readJson$
|
|
2258
|
+
const readJson$3 = (filePath) => {
|
|
2238
2259
|
try {
|
|
2239
2260
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2240
2261
|
} catch {
|
|
@@ -2249,7 +2270,7 @@ const buildAliasMatcher = (key) => {
|
|
|
2249
2270
|
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
2250
2271
|
};
|
|
2251
2272
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
2252
|
-
const opts = readJson$
|
|
2273
|
+
const opts = readJson$3(configPath)?.compilerOptions;
|
|
2253
2274
|
if (!opts || typeof opts !== "object") return;
|
|
2254
2275
|
const configDir = path.dirname(configPath);
|
|
2255
2276
|
const paths = opts.paths;
|
|
@@ -2272,7 +2293,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
|
2272
2293
|
|
|
2273
2294
|
//#endregion
|
|
2274
2295
|
//#region src/engines/ai-slop/js-workspaces.ts
|
|
2275
|
-
const readJson$
|
|
2296
|
+
const readJson$2 = (filePath) => {
|
|
2276
2297
|
try {
|
|
2277
2298
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2278
2299
|
} catch {
|
|
@@ -2292,7 +2313,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
2292
2313
|
}
|
|
2293
2314
|
}
|
|
2294
2315
|
}
|
|
2295
|
-
const lerna = readJson$
|
|
2316
|
+
const lerna = readJson$2(path.join(rootDir, "lerna.json"));
|
|
2296
2317
|
if (lerna && Array.isArray(lerna.packages)) {
|
|
2297
2318
|
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
2298
2319
|
}
|
|
@@ -2693,7 +2714,7 @@ const JS_EXTENSIONS$2 = new Set([
|
|
|
2693
2714
|
".cjs"
|
|
2694
2715
|
]);
|
|
2695
2716
|
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
2696
|
-
const readJson = (filePath) => {
|
|
2717
|
+
const readJson$1 = (filePath) => {
|
|
2697
2718
|
try {
|
|
2698
2719
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2699
2720
|
} catch {
|
|
@@ -2737,7 +2758,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2737
2758
|
const full = path.join(dir, entry.name);
|
|
2738
2759
|
if (entry.isDirectory()) walk(full, depth + 1);
|
|
2739
2760
|
else if (entry.name === "package.json" && depth > 0) {
|
|
2740
|
-
const wsPkg = readJson(full);
|
|
2761
|
+
const wsPkg = readJson$1(full);
|
|
2741
2762
|
if (!wsPkg) continue;
|
|
2742
2763
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2743
2764
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2749,13 +2770,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2749
2770
|
const collectJsDeps = (rootDir, jsDeps) => {
|
|
2750
2771
|
const pkgPath = path.join(rootDir, "package.json");
|
|
2751
2772
|
if (!fs.existsSync(pkgPath)) return false;
|
|
2752
|
-
const pkg = readJson(pkgPath);
|
|
2773
|
+
const pkg = readJson$1(pkgPath);
|
|
2753
2774
|
if (!pkg || typeof pkg !== "object") return false;
|
|
2754
2775
|
addDepsFromPkg(pkg, jsDeps);
|
|
2755
2776
|
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2756
2777
|
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2757
2778
|
for (const wsDir of workspaceDirs) {
|
|
2758
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2779
|
+
const wsPkg = readJson$1(path.join(wsDir, "package.json"));
|
|
2759
2780
|
if (!wsPkg) continue;
|
|
2760
2781
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2761
2782
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2784,7 +2805,11 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
2784
2805
|
"astro:",
|
|
2785
2806
|
"virtual:",
|
|
2786
2807
|
"bun:",
|
|
2787
|
-
"file:"
|
|
2808
|
+
"file:",
|
|
2809
|
+
"http:",
|
|
2810
|
+
"https:",
|
|
2811
|
+
"jsr:",
|
|
2812
|
+
"npm:"
|
|
2788
2813
|
];
|
|
2789
2814
|
const isJsVirtualModule = (spec, manifest) => {
|
|
2790
2815
|
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
@@ -2913,7 +2938,7 @@ const checkPyImport = (spec, manifest) => {
|
|
|
2913
2938
|
return root;
|
|
2914
2939
|
};
|
|
2915
2940
|
const detectHallucinatedImports = async (context) => {
|
|
2916
|
-
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2941
|
+
const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
|
|
2917
2942
|
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2918
2943
|
const manifest = loadManifest(context.rootDirectory);
|
|
2919
2944
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
@@ -3018,6 +3043,9 @@ const VENDOR_API_DOMAINS = [
|
|
|
3018
3043
|
];
|
|
3019
3044
|
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
3020
3045
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
3046
|
+
const PROVIDER_ID_RE = /^(?:price|prod|cus|sub|acct|org|app|tenant|workspace|project|client|key|tok|token|sk|pk)_[A-Za-z0-9][A-Za-z0-9_-]{7,}$/i;
|
|
3047
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3048
|
+
const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
|
|
3021
3049
|
const HARDCODED_URL_FINDING = {
|
|
3022
3050
|
rule: "ai-slop/hardcoded-url",
|
|
3023
3051
|
message: "Hardcoded environment URL in production code",
|
|
@@ -3055,8 +3083,10 @@ const safeUrlHost = (urlText) => {
|
|
|
3055
3083
|
}
|
|
3056
3084
|
};
|
|
3057
3085
|
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
3086
|
+
const TEMPLATE_INTERPOLATION_START = "${";
|
|
3058
3087
|
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
3059
3088
|
if (isEnvBackedLine(line)) return false;
|
|
3089
|
+
if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
|
|
3060
3090
|
const host = safeUrlHost(urlText);
|
|
3061
3091
|
if (!host) return false;
|
|
3062
3092
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
@@ -3071,7 +3101,11 @@ const hasUsefulIdShape = (value) => {
|
|
|
3071
3101
|
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
3072
3102
|
if (/^https?:\/\//i.test(value)) return false;
|
|
3073
3103
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
3074
|
-
|
|
3104
|
+
if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
|
|
3105
|
+
if (PROVIDER_ID_RE.test(value)) return true;
|
|
3106
|
+
if (UUID_RE.test(value)) return true;
|
|
3107
|
+
if (!/[0-9]/.test(value)) return false;
|
|
3108
|
+
return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
|
|
3075
3109
|
};
|
|
3076
3110
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
3077
3111
|
const diagnostics = [];
|
|
@@ -5153,8 +5187,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
|
|
|
5153
5187
|
return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
|
|
5154
5188
|
};
|
|
5155
5189
|
const DEPENDENCY_HELP = {
|
|
5156
|
-
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
5157
|
-
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
5190
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
5191
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
5158
5192
|
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
5159
5193
|
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
5160
5194
|
binaries: "This binary is used but its package is not in package.json."
|
|
@@ -5503,7 +5537,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
5503
5537
|
rule: "formatting",
|
|
5504
5538
|
severity,
|
|
5505
5539
|
message,
|
|
5506
|
-
help: "Run `
|
|
5540
|
+
help: "Run `aislop fix` to auto-format",
|
|
5507
5541
|
line: entry.location?.start?.line ?? 0,
|
|
5508
5542
|
column: entry.location?.start?.column ?? 0,
|
|
5509
5543
|
category: "Format",
|
|
@@ -5542,7 +5576,7 @@ const FORMATTERS = {
|
|
|
5542
5576
|
rule: "rust-formatting",
|
|
5543
5577
|
severity: "warning",
|
|
5544
5578
|
message: "Rust file is not formatted correctly",
|
|
5545
|
-
help: "Run `
|
|
5579
|
+
help: "Run `aislop fix` to auto-format with rustfmt",
|
|
5546
5580
|
line: parseInt(match[2], 10),
|
|
5547
5581
|
column: 0,
|
|
5548
5582
|
category: "Format",
|
|
@@ -5575,7 +5609,7 @@ const FORMATTERS = {
|
|
|
5575
5609
|
rule: offense.cop_name ?? "ruby-formatting",
|
|
5576
5610
|
severity: "warning",
|
|
5577
5611
|
message: offense.message ?? "Ruby formatting issue",
|
|
5578
|
-
help: "Run `
|
|
5612
|
+
help: "Run `aislop fix` to auto-format",
|
|
5579
5613
|
line: offense.location?.start_line ?? 0,
|
|
5580
5614
|
column: offense.location?.start_column ?? 0,
|
|
5581
5615
|
category: "Format",
|
|
@@ -5606,7 +5640,7 @@ const FORMATTERS = {
|
|
|
5606
5640
|
rule: "php-formatting",
|
|
5607
5641
|
severity: "warning",
|
|
5608
5642
|
message: "PHP file is not formatted correctly",
|
|
5609
|
-
help: "Run `
|
|
5643
|
+
help: "Run `aislop fix` to auto-format",
|
|
5610
5644
|
line: 0,
|
|
5611
5645
|
column: 0,
|
|
5612
5646
|
category: "Format",
|
|
@@ -5659,7 +5693,7 @@ const runGofmt = async (context) => {
|
|
|
5659
5693
|
rule: "go-formatting",
|
|
5660
5694
|
severity: "warning",
|
|
5661
5695
|
message: "Go file is not formatted correctly",
|
|
5662
|
-
help: "Run `
|
|
5696
|
+
help: "Run `aislop fix` to auto-format with gofmt",
|
|
5663
5697
|
line: 0,
|
|
5664
5698
|
column: 0,
|
|
5665
5699
|
category: "Format",
|
|
@@ -5758,7 +5792,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
5758
5792
|
rule: "python-formatting",
|
|
5759
5793
|
severity: "warning",
|
|
5760
5794
|
message: "Python file is not formatted correctly",
|
|
5761
|
-
help: "Run `
|
|
5795
|
+
help: "Run `aislop fix` to auto-format with ruff",
|
|
5762
5796
|
line: 0,
|
|
5763
5797
|
column: 0,
|
|
5764
5798
|
category: "Format",
|
|
@@ -6275,6 +6309,7 @@ const AMBIENT_GLOBAL_DEPS = [
|
|
|
6275
6309
|
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
6276
6310
|
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
6277
6311
|
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
6312
|
+
const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
|
|
6278
6313
|
const detectAmbientSources = (rootDir) => {
|
|
6279
6314
|
const found = /* @__PURE__ */ new Set();
|
|
6280
6315
|
const skipDirs = new Set([
|
|
@@ -6319,6 +6354,36 @@ const detectAmbientSources = (rootDir) => {
|
|
|
6319
6354
|
const extractNoUndefIdentifier = (message) => {
|
|
6320
6355
|
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
6321
6356
|
};
|
|
6357
|
+
const looksLikeChromeExtensionManifest = (filePath) => {
|
|
6358
|
+
try {
|
|
6359
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
6360
|
+
return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
|
|
6361
|
+
} catch {
|
|
6362
|
+
return false;
|
|
6363
|
+
}
|
|
6364
|
+
};
|
|
6365
|
+
const chromeExtensionFileCache = /* @__PURE__ */ new Map();
|
|
6366
|
+
const isChromeExtensionFile = (rootDir, relativeFilePath) => {
|
|
6367
|
+
const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
|
|
6368
|
+
const cached = chromeExtensionFileCache.get(cacheKey);
|
|
6369
|
+
if (cached !== void 0) return cached;
|
|
6370
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
6371
|
+
const root = path.resolve(rootDir);
|
|
6372
|
+
let dir = path.dirname(path.resolve(absolute));
|
|
6373
|
+
let matched = false;
|
|
6374
|
+
while (true) {
|
|
6375
|
+
const relativeToRoot = path.relative(root, dir);
|
|
6376
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
|
|
6377
|
+
if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
|
|
6378
|
+
matched = true;
|
|
6379
|
+
break;
|
|
6380
|
+
}
|
|
6381
|
+
if (dir === root) break;
|
|
6382
|
+
dir = path.dirname(dir);
|
|
6383
|
+
}
|
|
6384
|
+
chromeExtensionFileCache.set(cacheKey, matched);
|
|
6385
|
+
return matched;
|
|
6386
|
+
};
|
|
6322
6387
|
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
6323
6388
|
if (rule !== "eslint/no-undef") return false;
|
|
6324
6389
|
const ident = extractNoUndefIdentifier(message);
|
|
@@ -6327,9 +6392,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
6327
6392
|
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
6328
6393
|
return false;
|
|
6329
6394
|
};
|
|
6395
|
+
const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
|
|
6396
|
+
if (rule !== "eslint/no-undef") return false;
|
|
6397
|
+
const ident = extractNoUndefIdentifier(message);
|
|
6398
|
+
if (!ident) return false;
|
|
6399
|
+
const normalized = relativeFilePath.split(path.sep).join("/");
|
|
6400
|
+
if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
|
|
6401
|
+
if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
|
|
6402
|
+
return false;
|
|
6403
|
+
};
|
|
6330
6404
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
6331
6405
|
const clearSstReferenceCache = () => {
|
|
6332
6406
|
sstReferencedFiles.clear();
|
|
6407
|
+
chromeExtensionFileCache.clear();
|
|
6333
6408
|
};
|
|
6334
6409
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
6335
6410
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
@@ -6381,6 +6456,32 @@ const collectPackageNames = (dir) => {
|
|
|
6381
6456
|
}
|
|
6382
6457
|
return names;
|
|
6383
6458
|
};
|
|
6459
|
+
const readJson = (filePath) => {
|
|
6460
|
+
const raw = readTextFile$1(filePath);
|
|
6461
|
+
if (!raw) return null;
|
|
6462
|
+
try {
|
|
6463
|
+
const parsed = JSON.parse(raw);
|
|
6464
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
6465
|
+
} catch {
|
|
6466
|
+
return null;
|
|
6467
|
+
}
|
|
6468
|
+
};
|
|
6469
|
+
const hasBunRuntime = (rootDir, projectFiles) => {
|
|
6470
|
+
if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
|
|
6471
|
+
const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
|
|
6472
|
+
const pkg = readJson(path.join(rootDir, "package.json"));
|
|
6473
|
+
if (!pkg) return hasBunFiles;
|
|
6474
|
+
if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
|
|
6475
|
+
const scripts = pkg.scripts;
|
|
6476
|
+
if (scripts && typeof scripts === "object") {
|
|
6477
|
+
for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
|
|
6478
|
+
}
|
|
6479
|
+
return hasBunFiles;
|
|
6480
|
+
};
|
|
6481
|
+
const hasDenoRuntime = (rootDir, projectFiles) => {
|
|
6482
|
+
if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
|
|
6483
|
+
return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
|
|
6484
|
+
};
|
|
6384
6485
|
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
6385
6486
|
const collectAmbientGlobals = (rootDir) => {
|
|
6386
6487
|
const globals = /* @__PURE__ */ new Set();
|
|
@@ -6392,7 +6493,8 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
6392
6493
|
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
6393
6494
|
}
|
|
6394
6495
|
const deps = collectPackageNames(rootDir);
|
|
6395
|
-
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
6496
|
+
if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
|
|
6497
|
+
if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
|
|
6396
6498
|
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
6397
6499
|
"$app",
|
|
6398
6500
|
"$config",
|
|
@@ -6548,6 +6650,37 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
|
|
|
6548
6650
|
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
6549
6651
|
}
|
|
6550
6652
|
};
|
|
6653
|
+
const toDiagnostic = (d) => {
|
|
6654
|
+
const { plugin, rule } = parseRuleCode(d.code);
|
|
6655
|
+
const label = d.labels[0];
|
|
6656
|
+
return {
|
|
6657
|
+
filePath: d.filename,
|
|
6658
|
+
engine: "lint",
|
|
6659
|
+
rule: `${plugin}/${rule}`,
|
|
6660
|
+
severity: d.severity,
|
|
6661
|
+
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
6662
|
+
help: d.help || "",
|
|
6663
|
+
line: label?.span.line ?? 0,
|
|
6664
|
+
column: label?.span.column ?? 0,
|
|
6665
|
+
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
6666
|
+
fixable: false
|
|
6667
|
+
};
|
|
6668
|
+
};
|
|
6669
|
+
const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
|
|
6670
|
+
const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
|
|
6671
|
+
if (isExcludedFromScan(relativePath)) return false;
|
|
6672
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
6673
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
6674
|
+
if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
|
|
6675
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
6676
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
6677
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
6678
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
6679
|
+
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
6680
|
+
if (seen.has(key)) return false;
|
|
6681
|
+
seen.add(key);
|
|
6682
|
+
return true;
|
|
6683
|
+
};
|
|
6551
6684
|
const runOxlint = async (context) => {
|
|
6552
6685
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
6553
6686
|
const framework = context.frameworks.find((f) => f !== "none");
|
|
@@ -6584,34 +6717,7 @@ const runOxlint = async (context) => {
|
|
|
6584
6717
|
return [];
|
|
6585
6718
|
}
|
|
6586
6719
|
const seen = /* @__PURE__ */ new Set();
|
|
6587
|
-
return output.diagnostics.map((d) =>
|
|
6588
|
-
const { plugin, rule } = parseRuleCode(d.code);
|
|
6589
|
-
const label = d.labels[0];
|
|
6590
|
-
return {
|
|
6591
|
-
filePath: d.filename,
|
|
6592
|
-
engine: "lint",
|
|
6593
|
-
rule: `${plugin}/${rule}`,
|
|
6594
|
-
severity: d.severity,
|
|
6595
|
-
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
6596
|
-
help: d.help || "",
|
|
6597
|
-
line: label?.span.line ?? 0,
|
|
6598
|
-
column: label?.span.column ?? 0,
|
|
6599
|
-
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
6600
|
-
fixable: false
|
|
6601
|
-
};
|
|
6602
|
-
}).filter((d) => {
|
|
6603
|
-
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
6604
|
-
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
6605
|
-
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
6606
|
-
if (isSolidRefFalsePositive(context, d)) return false;
|
|
6607
|
-
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
6608
|
-
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
6609
|
-
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
6610
|
-
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
6611
|
-
if (seen.has(key)) return false;
|
|
6612
|
-
seen.add(key);
|
|
6613
|
-
return true;
|
|
6614
|
-
});
|
|
6720
|
+
return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
|
|
6615
6721
|
} finally {
|
|
6616
6722
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
6617
6723
|
}
|
|
@@ -6737,7 +6843,7 @@ const lintEngine = {
|
|
|
6737
6843
|
const promises = [];
|
|
6738
6844
|
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
6739
6845
|
promises.push(runOxlint(context));
|
|
6740
|
-
if (context.config.lint.typecheck) promises.push(import("./typecheck-
|
|
6846
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-yOGXIIGU.js").then((mod) => mod.runTypecheck(context)));
|
|
6741
6847
|
}
|
|
6742
6848
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
6743
6849
|
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
@@ -6757,7 +6863,7 @@ const lintEngine = {
|
|
|
6757
6863
|
|
|
6758
6864
|
//#endregion
|
|
6759
6865
|
//#region src/ui/invocation.ts
|
|
6760
|
-
const detectInvocation = () => "
|
|
6866
|
+
const detectInvocation = () => "aislop";
|
|
6761
6867
|
|
|
6762
6868
|
//#endregion
|
|
6763
6869
|
//#region src/engines/security/audit.ts
|
|
@@ -6909,7 +7015,7 @@ const parseJsAudit = (output, source) => {
|
|
|
6909
7015
|
rule: "security/dependency-audit-skipped",
|
|
6910
7016
|
severity: "info",
|
|
6911
7017
|
message: `Dependency audit skipped (${source}): lockfile is missing`,
|
|
6912
|
-
help: error.detail ?? "Generate a lockfile, then re-run `
|
|
7018
|
+
help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
|
|
6913
7019
|
line: 0,
|
|
6914
7020
|
column: 0,
|
|
6915
7021
|
category: "Security",
|
|
@@ -7026,6 +7132,194 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
7026
7132
|
}
|
|
7027
7133
|
};
|
|
7028
7134
|
|
|
7135
|
+
//#endregion
|
|
7136
|
+
//#region src/engines/security/html-safety.ts
|
|
7137
|
+
const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
|
|
7138
|
+
const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
|
|
7139
|
+
const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
|
|
7140
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
7141
|
+
const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
|
|
7142
|
+
const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
|
|
7143
|
+
const NUMERICISH_NAME_RE = /(?:^|\.)(?:count|length|size|width|height|top|right|bottom|left|duration|elapsed|timestamp|time|ms|port|pid|attempt|attempts|index|total|x|y)$|(?:count|length|size|width|height|duration|elapsed|timestamp|time|port|pid|attempt|index|total)$/i;
|
|
7144
|
+
const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
|
|
7145
|
+
const consumeQuotedLiteral = (content, startIndex, quote) => {
|
|
7146
|
+
let i = startIndex + 1;
|
|
7147
|
+
while (i < content.length) {
|
|
7148
|
+
const char = content[i];
|
|
7149
|
+
if (char === "\\") {
|
|
7150
|
+
i += 2;
|
|
7151
|
+
continue;
|
|
7152
|
+
}
|
|
7153
|
+
if (char === quote) return { endIndex: i };
|
|
7154
|
+
if (char === "\n") return null;
|
|
7155
|
+
i++;
|
|
7156
|
+
}
|
|
7157
|
+
return null;
|
|
7158
|
+
};
|
|
7159
|
+
const consumeTemplateLiteral = (content, startIndex) => {
|
|
7160
|
+
const openIndex = content.indexOf("`", startIndex);
|
|
7161
|
+
if (openIndex === -1) return null;
|
|
7162
|
+
let i = openIndex + 1;
|
|
7163
|
+
while (i < content.length) {
|
|
7164
|
+
const char = content[i];
|
|
7165
|
+
if (char === "\\") {
|
|
7166
|
+
i += 2;
|
|
7167
|
+
continue;
|
|
7168
|
+
}
|
|
7169
|
+
if (char === "`") return {
|
|
7170
|
+
body: content.slice(openIndex + 1, i),
|
|
7171
|
+
endIndex: i
|
|
7172
|
+
};
|
|
7173
|
+
i++;
|
|
7174
|
+
}
|
|
7175
|
+
return null;
|
|
7176
|
+
};
|
|
7177
|
+
const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
|
|
7178
|
+
const assignmentRhsStart = (content, matchIndex) => {
|
|
7179
|
+
const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
|
|
7180
|
+
return match ? matchIndex + match[0].length : null;
|
|
7181
|
+
};
|
|
7182
|
+
const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
7183
|
+
const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
|
|
7184
|
+
const splitTopLevelTernary = (expr) => {
|
|
7185
|
+
let quote = null;
|
|
7186
|
+
let depth = 0;
|
|
7187
|
+
let question = -1;
|
|
7188
|
+
let colon = -1;
|
|
7189
|
+
for (let i = 0; i < expr.length; i++) {
|
|
7190
|
+
const char = expr[i];
|
|
7191
|
+
if (char === "\\") {
|
|
7192
|
+
i++;
|
|
7193
|
+
continue;
|
|
7194
|
+
}
|
|
7195
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
7196
|
+
quote = char;
|
|
7197
|
+
continue;
|
|
7198
|
+
}
|
|
7199
|
+
if (char === quote) {
|
|
7200
|
+
quote = null;
|
|
7201
|
+
continue;
|
|
7202
|
+
}
|
|
7203
|
+
if (quote) continue;
|
|
7204
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
7205
|
+
else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
|
|
7206
|
+
else if (char === "?" && depth === 0 && question === -1) question = i;
|
|
7207
|
+
else if (char === ":" && depth === 0 && question !== -1) {
|
|
7208
|
+
colon = i;
|
|
7209
|
+
break;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
if (question === -1 || colon === -1) return null;
|
|
7213
|
+
return {
|
|
7214
|
+
whenTrue: expr.slice(question + 1, colon).trim(),
|
|
7215
|
+
whenFalse: expr.slice(colon + 1).trim()
|
|
7216
|
+
};
|
|
7217
|
+
};
|
|
7218
|
+
const isNumericishExpression = (expr) => {
|
|
7219
|
+
const normalized = expr.trim();
|
|
7220
|
+
if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
|
|
7221
|
+
if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
|
|
7222
|
+
return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
|
|
7223
|
+
};
|
|
7224
|
+
const isSafeTemplateLiteralExpression = (expr, safeNames) => {
|
|
7225
|
+
if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
|
|
7226
|
+
return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
|
|
7227
|
+
};
|
|
7228
|
+
const collectSafeHtmlNames = (content, matchIndex) => {
|
|
7229
|
+
const safeNames = /* @__PURE__ */ new Set();
|
|
7230
|
+
const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
|
|
7231
|
+
for (const rawLine of prefix.split("\n")) {
|
|
7232
|
+
const line = rawLine.trim();
|
|
7233
|
+
let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
7234
|
+
if (match) {
|
|
7235
|
+
const [, name, expr] = match;
|
|
7236
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7237
|
+
else safeNames.delete(name);
|
|
7238
|
+
continue;
|
|
7239
|
+
}
|
|
7240
|
+
match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
|
|
7241
|
+
if (match) {
|
|
7242
|
+
const [, name, expr] = match;
|
|
7243
|
+
if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7244
|
+
else safeNames.delete(name);
|
|
7245
|
+
continue;
|
|
7246
|
+
}
|
|
7247
|
+
match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
7248
|
+
if (match) {
|
|
7249
|
+
const [, name, expr] = match;
|
|
7250
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7251
|
+
else safeNames.delete(name);
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
return safeNames;
|
|
7255
|
+
};
|
|
7256
|
+
const isSafeHtmlExpression = (expr, safeNames) => {
|
|
7257
|
+
const normalized = expr.trim();
|
|
7258
|
+
if (SANITIZER_EXPR_RE.test(normalized)) return true;
|
|
7259
|
+
if (STATIC_STRING_RE.test(normalized)) return true;
|
|
7260
|
+
if (staticTernaryRe.test(expr)) return true;
|
|
7261
|
+
if (isNumericishExpression(normalized)) return true;
|
|
7262
|
+
if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
|
|
7263
|
+
if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
|
|
7264
|
+
const ternary = splitTopLevelTernary(normalized);
|
|
7265
|
+
if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
|
|
7266
|
+
const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
|
|
7267
|
+
if (formatCall) return formatCall[1].split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0).every((arg) => isNumericishExpression(arg) || IDENT_RE.test(arg) && safeNames.has(arg));
|
|
7268
|
+
return false;
|
|
7269
|
+
};
|
|
7270
|
+
const readSingleLineRhs = (content, rhsStart) => {
|
|
7271
|
+
const lineEnd = content.indexOf("\n", rhsStart);
|
|
7272
|
+
const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
|
|
7273
|
+
let quote = null;
|
|
7274
|
+
for (let i = 0; i < line.length; i++) {
|
|
7275
|
+
const char = line[i];
|
|
7276
|
+
if (char === "\\") {
|
|
7277
|
+
i++;
|
|
7278
|
+
continue;
|
|
7279
|
+
}
|
|
7280
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
7281
|
+
quote = char;
|
|
7282
|
+
continue;
|
|
7283
|
+
}
|
|
7284
|
+
if (char === quote) {
|
|
7285
|
+
quote = null;
|
|
7286
|
+
continue;
|
|
7287
|
+
}
|
|
7288
|
+
if (char === ";" && quote === null) return line.slice(0, i).trim();
|
|
7289
|
+
}
|
|
7290
|
+
return line.trim();
|
|
7291
|
+
};
|
|
7292
|
+
const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
|
|
7293
|
+
const head = content.slice(rhsStart);
|
|
7294
|
+
const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
|
|
7295
|
+
if (!mapMatch) return false;
|
|
7296
|
+
const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
|
|
7297
|
+
if (!template) return false;
|
|
7298
|
+
if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
|
|
7299
|
+
const safeNames = collectSafeHtmlNames(content, rhsStart);
|
|
7300
|
+
return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
7301
|
+
};
|
|
7302
|
+
const isSafeInnerHtmlAssignment = (content, matchIndex) => {
|
|
7303
|
+
const tail = content.slice(matchIndex);
|
|
7304
|
+
if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
|
|
7305
|
+
const rhsStart = assignmentRhsStart(content, matchIndex);
|
|
7306
|
+
if (rhsStart === null) return false;
|
|
7307
|
+
const first = content[rhsStart];
|
|
7308
|
+
const safeNames = collectSafeHtmlNames(content, matchIndex);
|
|
7309
|
+
if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
|
|
7310
|
+
if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
|
|
7311
|
+
if (first === "'" || first === "\"") {
|
|
7312
|
+
const quoted = consumeQuotedLiteral(content, rhsStart, first);
|
|
7313
|
+
return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
|
|
7314
|
+
}
|
|
7315
|
+
if (first !== "`") return false;
|
|
7316
|
+
const template = consumeTemplateLiteral(content, rhsStart);
|
|
7317
|
+
if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
|
|
7318
|
+
const expressions = templateExpressions(template.body);
|
|
7319
|
+
if (expressions.length === 0) return true;
|
|
7320
|
+
return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
7321
|
+
};
|
|
7322
|
+
|
|
7029
7323
|
//#endregion
|
|
7030
7324
|
//#region src/engines/security/risky.ts
|
|
7031
7325
|
const ev = "eval";
|
|
@@ -7151,6 +7445,30 @@ const isStructuredDataScript = (content, matchIndex) => {
|
|
|
7151
7445
|
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
7152
7446
|
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
7153
7447
|
};
|
|
7448
|
+
const isSafeShellSpawnArray = (content, matchIndex) => /^spawn\s*\(\s*\[/.test(content.slice(matchIndex)) && !/^\s*spawn\s*\(\s*\[\s*["'](?:sh|bash|zsh|cmd|cmd\.exe|powershell|pwsh)["']\s*,\s*["'](?:-c|\/c|\/C)["']/i.test(content.slice(matchIndex)) && !/shell\s*:\s*true\b/.test(content.slice(matchIndex, matchIndex + 500));
|
|
7449
|
+
const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
|
|
7450
|
+
const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
|
|
7451
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7452
|
+
const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
|
|
7453
|
+
const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
|
|
7454
|
+
if (!name) return false;
|
|
7455
|
+
const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
|
|
7456
|
+
const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
|
|
7457
|
+
const declaration = [...prefix.matchAll(declarationRe)].at(-1);
|
|
7458
|
+
if (!declaration) return false;
|
|
7459
|
+
const expr = declaration[1];
|
|
7460
|
+
if (!/\.join\s*\(/.test(expr)) return false;
|
|
7461
|
+
return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
|
|
7462
|
+
};
|
|
7463
|
+
const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
|
|
7464
|
+
const template = consumeTemplateLiteral(content, matchIndex);
|
|
7465
|
+
if (!template) return false;
|
|
7466
|
+
const afterTemplate = content.slice(template.endIndex + 1);
|
|
7467
|
+
if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
|
|
7468
|
+
const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
7469
|
+
if (expressions.length === 0) return false;
|
|
7470
|
+
return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
|
|
7471
|
+
};
|
|
7154
7472
|
const detectRiskyConstructs = async (context) => {
|
|
7155
7473
|
const files = getSourceFiles(context);
|
|
7156
7474
|
const diagnostics = [];
|
|
@@ -7175,8 +7493,11 @@ const detectRiskyConstructs = async (context) => {
|
|
|
7175
7493
|
const line = content.slice(0, match.index).split("\n").length;
|
|
7176
7494
|
if (name === "innerhtml") {
|
|
7177
7495
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
7496
|
+
if (isSafeInnerHtmlAssignment(content, match.index)) continue;
|
|
7178
7497
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
7179
7498
|
}
|
|
7499
|
+
if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
|
|
7500
|
+
if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
|
|
7180
7501
|
if (name === "dangerously-set-innerhtml") {
|
|
7181
7502
|
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
7182
7503
|
if (isStructuredDataScript(content, match.index)) continue;
|
|
@@ -7273,7 +7594,28 @@ const PLACEHOLDER_EXACT = new Set([
|
|
|
7273
7594
|
"todo",
|
|
7274
7595
|
"replace_me"
|
|
7275
7596
|
]);
|
|
7597
|
+
const PLACEHOLDER_URL_PARTS = new Set([
|
|
7598
|
+
"example",
|
|
7599
|
+
"host",
|
|
7600
|
+
"localhost",
|
|
7601
|
+
"pass",
|
|
7602
|
+
"password",
|
|
7603
|
+
"pw",
|
|
7604
|
+
"user",
|
|
7605
|
+
"username"
|
|
7606
|
+
]);
|
|
7607
|
+
const isPlaceholderCredentialUrl = (matchedText) => {
|
|
7608
|
+
const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
|
|
7609
|
+
if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
|
|
7610
|
+
try {
|
|
7611
|
+
const parsed = new URL(matchedText);
|
|
7612
|
+
return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
|
|
7613
|
+
} catch {
|
|
7614
|
+
return false;
|
|
7615
|
+
}
|
|
7616
|
+
};
|
|
7276
7617
|
const isPlaceholderValue = (matchedText) => {
|
|
7618
|
+
if (isPlaceholderCredentialUrl(matchedText)) return true;
|
|
7277
7619
|
if (/env\(/i.test(matchedText)) return true;
|
|
7278
7620
|
if (matchedText.includes("process.env")) return true;
|
|
7279
7621
|
if (matchedText.includes("os.environ")) return true;
|
|
@@ -7394,23 +7736,36 @@ const STYLE_RULES = new Set([
|
|
|
7394
7736
|
"complexity/function-too-long"
|
|
7395
7737
|
]);
|
|
7396
7738
|
const STYLE_WEIGHT = .5;
|
|
7739
|
+
const COMMENT_STYLE_RULE_CAP = 12;
|
|
7740
|
+
const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
|
|
7397
7741
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7398
7742
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7399
7743
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
7400
7744
|
return Math.max(1, filesWithDiagnostics);
|
|
7401
7745
|
};
|
|
7402
|
-
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
7746
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
|
|
7403
7747
|
if (diagnostics.length === 0) return {
|
|
7404
7748
|
score: PERFECT_SCORE,
|
|
7405
7749
|
label: "Healthy"
|
|
7406
7750
|
};
|
|
7407
|
-
|
|
7751
|
+
const deductionsByRule = /* @__PURE__ */ new Map();
|
|
7408
7752
|
for (const d of diagnostics) {
|
|
7409
7753
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7410
7754
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7411
7755
|
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7412
|
-
|
|
7413
|
-
|
|
7756
|
+
const key = `${d.engine}:${d.rule}`;
|
|
7757
|
+
deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
|
|
7758
|
+
}
|
|
7759
|
+
const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
|
|
7760
|
+
const capForRule = (key) => {
|
|
7761
|
+
const rule = key.slice(key.indexOf(":") + 1);
|
|
7762
|
+
if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
|
|
7763
|
+
return defaultRuleCap;
|
|
7764
|
+
};
|
|
7765
|
+
const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
|
|
7766
|
+
const cap = capForRule(key);
|
|
7767
|
+
return total + (cap ? Math.min(value, cap) : value);
|
|
7768
|
+
}, 0);
|
|
7414
7769
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7415
7770
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
7416
7771
|
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
@@ -7622,6 +7977,19 @@ const discoverProject = async (directory, excludePatterns = []) => {
|
|
|
7622
7977
|
//#endregion
|
|
7623
7978
|
//#region src/utils/git.ts
|
|
7624
7979
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
7980
|
+
const baseRefExists = (cwd, ref) => {
|
|
7981
|
+
const result = spawnSync("git", [
|
|
7982
|
+
"rev-parse",
|
|
7983
|
+
"--verify",
|
|
7984
|
+
"--quiet",
|
|
7985
|
+
`${ref}^{commit}`
|
|
7986
|
+
], {
|
|
7987
|
+
cwd,
|
|
7988
|
+
encoding: "utf-8",
|
|
7989
|
+
maxBuffer: MAX_BUFFER
|
|
7990
|
+
});
|
|
7991
|
+
return !result.error && result.status === 0;
|
|
7992
|
+
};
|
|
7625
7993
|
const getChangedFiles = (cwd, base) => {
|
|
7626
7994
|
const diff = spawnSync("git", [
|
|
7627
7995
|
"diff",
|
|
@@ -7707,7 +8075,7 @@ const runScopedScan = async (cwd, filePaths) => {
|
|
|
7707
8075
|
architecture: config.engines.architecture,
|
|
7708
8076
|
security: false
|
|
7709
8077
|
})).flatMap((r) => r.diagnostics);
|
|
7710
|
-
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
8078
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
7711
8079
|
return {
|
|
7712
8080
|
diagnostics,
|
|
7713
8081
|
score,
|
|
@@ -7790,10 +8158,10 @@ const captureBaseline = async (cwd) => {
|
|
|
7790
8158
|
security: false
|
|
7791
8159
|
});
|
|
7792
8160
|
const diagnostics = results.flatMap((r) => r.diagnostics);
|
|
7793
|
-
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
8161
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
7794
8162
|
const byEngine = {};
|
|
7795
8163
|
for (const r of results) {
|
|
7796
|
-
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
8164
|
+
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
7797
8165
|
byEngine[r.engine] = engineScore;
|
|
7798
8166
|
}
|
|
7799
8167
|
const findingFingerprints = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").map((d) => fingerprintDiagnostic(d, project.rootDirectory));
|
|
@@ -8972,6 +9340,40 @@ const detectInstalledAgents = (opts) => {
|
|
|
8972
9340
|
return hits;
|
|
8973
9341
|
};
|
|
8974
9342
|
|
|
9343
|
+
//#endregion
|
|
9344
|
+
//#region src/ui/symbols.ts
|
|
9345
|
+
const TTY = {
|
|
9346
|
+
stepActive: "◇",
|
|
9347
|
+
stepDone: "◆",
|
|
9348
|
+
rail: "│",
|
|
9349
|
+
railEnd: "└",
|
|
9350
|
+
bullet: "●",
|
|
9351
|
+
hint: "→",
|
|
9352
|
+
pass: "✓",
|
|
9353
|
+
fail: "✗",
|
|
9354
|
+
warn: "!",
|
|
9355
|
+
pending: "•",
|
|
9356
|
+
engineActive: "⏵",
|
|
9357
|
+
neutral: "─"
|
|
9358
|
+
};
|
|
9359
|
+
const PLAIN = {
|
|
9360
|
+
stepActive: "*",
|
|
9361
|
+
stepDone: "*",
|
|
9362
|
+
rail: "|",
|
|
9363
|
+
railEnd: "+",
|
|
9364
|
+
bullet: "-",
|
|
9365
|
+
hint: "->",
|
|
9366
|
+
pass: "[ok]",
|
|
9367
|
+
fail: "[x]",
|
|
9368
|
+
warn: "[!]",
|
|
9369
|
+
pending: "-",
|
|
9370
|
+
engineActive: ">",
|
|
9371
|
+
neutral: "-"
|
|
9372
|
+
};
|
|
9373
|
+
const createSymbols = (opts = {}) => opts.plain ? PLAIN : TTY;
|
|
9374
|
+
const isPlain = () => process.env.THEME === "plain" || Boolean(process.env.NO_COLOR) || !process.stdout.isTTY;
|
|
9375
|
+
const symbols = createSymbols({ plain: isPlain() });
|
|
9376
|
+
|
|
8975
9377
|
//#endregion
|
|
8976
9378
|
//#region src/ui/theme.ts
|
|
8977
9379
|
const TRUECOLOR = {
|
|
@@ -9032,6 +9434,250 @@ const createTheme = (opts = {}) => {
|
|
|
9032
9434
|
const style = (theme, token, text) => theme.paint[token](text);
|
|
9033
9435
|
const theme = createTheme();
|
|
9034
9436
|
|
|
9437
|
+
//#endregion
|
|
9438
|
+
//#region src/ui/width.ts
|
|
9439
|
+
const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
|
|
9440
|
+
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
9441
|
+
const stringWidth = (s) => {
|
|
9442
|
+
const bare = stripAnsi(s);
|
|
9443
|
+
let total = 0;
|
|
9444
|
+
for (const ch of bare) {
|
|
9445
|
+
const w = wcwidth(ch.codePointAt(0) ?? 0);
|
|
9446
|
+
total += w > 0 ? w : 1;
|
|
9447
|
+
}
|
|
9448
|
+
return total;
|
|
9449
|
+
};
|
|
9450
|
+
const padEnd = (s, target, fill = " ") => {
|
|
9451
|
+
const w = stringWidth(s);
|
|
9452
|
+
if (w >= target) return s;
|
|
9453
|
+
return s + fill.repeat(target - w);
|
|
9454
|
+
};
|
|
9455
|
+
const padStart = (s, target, fill = " ") => {
|
|
9456
|
+
const w = stringWidth(s);
|
|
9457
|
+
if (w >= target) return s;
|
|
9458
|
+
return fill.repeat(target - w) + s;
|
|
9459
|
+
};
|
|
9460
|
+
const truncate = (s, max, ellipsis = "…") => {
|
|
9461
|
+
if (stringWidth(s) <= max) return s;
|
|
9462
|
+
const limit = Math.max(0, max - stringWidth(ellipsis));
|
|
9463
|
+
let out = "";
|
|
9464
|
+
let w = 0;
|
|
9465
|
+
for (const ch of s) {
|
|
9466
|
+
const cw = wcwidth(ch.codePointAt(0) ?? 0);
|
|
9467
|
+
if (w + cw > limit) break;
|
|
9468
|
+
out += ch;
|
|
9469
|
+
w += cw;
|
|
9470
|
+
}
|
|
9471
|
+
return out + ellipsis;
|
|
9472
|
+
};
|
|
9473
|
+
|
|
9474
|
+
//#endregion
|
|
9475
|
+
//#region src/ui/search-select.ts
|
|
9476
|
+
const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
|
|
9477
|
+
callback();
|
|
9478
|
+
} });
|
|
9479
|
+
const filterSearchItems = (items, query) => {
|
|
9480
|
+
const q = query.trim().toLowerCase();
|
|
9481
|
+
if (q.length === 0) return items;
|
|
9482
|
+
return items.map((item, index) => {
|
|
9483
|
+
const label = item.label.toLowerCase();
|
|
9484
|
+
const value = String(item.value).toLowerCase();
|
|
9485
|
+
const hint = item.hint?.toLowerCase() ?? "";
|
|
9486
|
+
const keywords = (item.keywords ?? []).join(" ").toLowerCase();
|
|
9487
|
+
const haystack = [
|
|
9488
|
+
label,
|
|
9489
|
+
value,
|
|
9490
|
+
hint,
|
|
9491
|
+
keywords
|
|
9492
|
+
].filter((v) => v.length > 0).join(" ");
|
|
9493
|
+
if (!q.split(/\s+/).every((part) => haystack.includes(part))) return null;
|
|
9494
|
+
let rank = 80;
|
|
9495
|
+
if (label === q || value === q) rank = 0;
|
|
9496
|
+
else if (label.startsWith(q) || value.startsWith(q)) rank = 10;
|
|
9497
|
+
else if (label.includes(q) || value.includes(q)) rank = 20;
|
|
9498
|
+
else if (keywords.includes(q)) rank = 40;
|
|
9499
|
+
else if (hint.includes(q)) rank = 60;
|
|
9500
|
+
return {
|
|
9501
|
+
item,
|
|
9502
|
+
index,
|
|
9503
|
+
rank
|
|
9504
|
+
};
|
|
9505
|
+
}).filter((entry) => entry !== null).sort((a, b) => {
|
|
9506
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
9507
|
+
return a.index - b.index;
|
|
9508
|
+
}).map((entry) => entry.item);
|
|
9509
|
+
};
|
|
9510
|
+
const countRows = (lines, columns) => {
|
|
9511
|
+
const width = columns && columns > 0 ? columns : 80;
|
|
9512
|
+
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(stringWidth(line) / width)), 0);
|
|
9513
|
+
};
|
|
9514
|
+
const renderSearchLines = (options) => {
|
|
9515
|
+
const maxVisible = options.maxVisible ?? 8;
|
|
9516
|
+
const filtered = filterSearchItems(options.items, options.query);
|
|
9517
|
+
const cursor = Math.max(0, Math.min(options.cursor, Math.max(0, filtered.length - 1)));
|
|
9518
|
+
const start = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
9519
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
9520
|
+
const lines = [];
|
|
9521
|
+
const marker = options.state === "cancel" ? style(theme, "danger", symbols.fail) : options.state === "submit" ? style(theme, "success", symbols.stepDone) : style(theme, "accent", symbols.stepActive);
|
|
9522
|
+
lines.push(` ${marker} ${style(theme, "bold", options.message)}`);
|
|
9523
|
+
if (options.state === "cancel") {
|
|
9524
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "Cancelled")}`);
|
|
9525
|
+
return lines;
|
|
9526
|
+
}
|
|
9527
|
+
if (options.state === "submit") {
|
|
9528
|
+
const selected = options.items.filter((item) => options.selected.has(item.value));
|
|
9529
|
+
const label = selected.length > 0 ? selected.map((item) => item.label).join(", ") : "No selection";
|
|
9530
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", label)}`);
|
|
9531
|
+
return lines;
|
|
9532
|
+
}
|
|
9533
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "Search:")} ${options.query}${style(theme, "dim", "_")}`);
|
|
9534
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", options.mode === "multi" ? "type to filter, arrows move, space toggles, enter confirms" : "type to filter, arrows move, enter selects")}`);
|
|
9535
|
+
lines.push(` ${style(theme, "muted", symbols.rail)}`);
|
|
9536
|
+
if (visible.length === 0) lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "No matches")}`);
|
|
9537
|
+
else for (const [offset, item] of visible.entries()) {
|
|
9538
|
+
const active = start + offset === cursor;
|
|
9539
|
+
const selected = options.selected.has(item.value);
|
|
9540
|
+
const pointer = active ? style(theme, "info", symbols.engineActive) : " ";
|
|
9541
|
+
const radio = options.mode === "multi" ? selected ? style(theme, "success", symbols.pass) : style(theme, "muted", symbols.pending) : active ? style(theme, "accent", symbols.bullet) : style(theme, "muted", symbols.pending);
|
|
9542
|
+
const label = active ? style(theme, "bold", item.label) : item.label;
|
|
9543
|
+
const hint = item.hint ? ` ${style(theme, "muted", truncate(item.hint, 72))}` : "";
|
|
9544
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${pointer} ${radio} ${label}${hint}`);
|
|
9545
|
+
}
|
|
9546
|
+
const hiddenBefore = start;
|
|
9547
|
+
const hiddenAfter = Math.max(0, filtered.length - (start + visible.length));
|
|
9548
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
9549
|
+
const parts = [];
|
|
9550
|
+
if (hiddenBefore > 0) parts.push(`up ${hiddenBefore} more`);
|
|
9551
|
+
if (hiddenAfter > 0) parts.push(`down ${hiddenAfter} more`);
|
|
9552
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", parts.join(" · "))}`);
|
|
9553
|
+
}
|
|
9554
|
+
if (options.mode === "multi") {
|
|
9555
|
+
const picked = options.items.filter((item) => options.selected.has(item.value));
|
|
9556
|
+
const summary = picked.length === 0 ? "Selected: none" : picked.length <= 3 ? `Selected: ${picked.map((item) => item.label).join(", ")}` : `Selected: ${picked.slice(0, 3).map((item) => item.label).join(", ")} +${picked.length - 3} more`;
|
|
9557
|
+
lines.push(` ${style(theme, "muted", symbols.rail)}`);
|
|
9558
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "success", summary)}`);
|
|
9559
|
+
}
|
|
9560
|
+
lines.push(` ${style(theme, "muted", symbols.railEnd)}`);
|
|
9561
|
+
return lines;
|
|
9562
|
+
};
|
|
9563
|
+
const runSearchPrompt = async (options) => {
|
|
9564
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return options.mode === "multi" ? options.initialSelected ?? [] : null;
|
|
9565
|
+
return new Promise((resolve) => {
|
|
9566
|
+
const rl = readline.createInterface({
|
|
9567
|
+
input: process.stdin,
|
|
9568
|
+
output: silentOutput,
|
|
9569
|
+
terminal: false
|
|
9570
|
+
});
|
|
9571
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
9572
|
+
process.stdin.setRawMode(true);
|
|
9573
|
+
let query = "";
|
|
9574
|
+
let cursor = 0;
|
|
9575
|
+
let lastRows = 0;
|
|
9576
|
+
const selected = new Set(options.initialSelected ?? []);
|
|
9577
|
+
const clear = () => {
|
|
9578
|
+
if (lastRows === 0) return;
|
|
9579
|
+
process.stdout.write(`\x1b[${lastRows}A`);
|
|
9580
|
+
for (let i = 0; i < lastRows; i++) process.stdout.write("\x1B[2K\x1B[1B");
|
|
9581
|
+
process.stdout.write(`\x1b[${lastRows}A`);
|
|
9582
|
+
};
|
|
9583
|
+
const render = (state = "active") => {
|
|
9584
|
+
clear();
|
|
9585
|
+
const lines = renderSearchLines({
|
|
9586
|
+
...options,
|
|
9587
|
+
query,
|
|
9588
|
+
cursor,
|
|
9589
|
+
selected,
|
|
9590
|
+
state
|
|
9591
|
+
});
|
|
9592
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
9593
|
+
lastRows = countRows(lines, process.stdout.columns);
|
|
9594
|
+
};
|
|
9595
|
+
const cleanup = () => {
|
|
9596
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
9597
|
+
process.stdin.setRawMode(false);
|
|
9598
|
+
rl.close();
|
|
9599
|
+
};
|
|
9600
|
+
const submit = () => {
|
|
9601
|
+
const item = filterSearchItems(options.items, query)[cursor];
|
|
9602
|
+
if (options.mode === "single") {
|
|
9603
|
+
if (!item) {
|
|
9604
|
+
if (options.required) return;
|
|
9605
|
+
render("cancel");
|
|
9606
|
+
cleanup();
|
|
9607
|
+
resolve(null);
|
|
9608
|
+
return;
|
|
9609
|
+
}
|
|
9610
|
+
selected.clear();
|
|
9611
|
+
selected.add(item.value);
|
|
9612
|
+
render("submit");
|
|
9613
|
+
cleanup();
|
|
9614
|
+
resolve(item.value);
|
|
9615
|
+
return;
|
|
9616
|
+
}
|
|
9617
|
+
if (options.required && selected.size === 0) return;
|
|
9618
|
+
render("submit");
|
|
9619
|
+
cleanup();
|
|
9620
|
+
resolve([...selected]);
|
|
9621
|
+
};
|
|
9622
|
+
const cancel = () => {
|
|
9623
|
+
render("cancel");
|
|
9624
|
+
cleanup();
|
|
9625
|
+
resolve(null);
|
|
9626
|
+
};
|
|
9627
|
+
const onKeypress = (_str, key) => {
|
|
9628
|
+
if (!key) return;
|
|
9629
|
+
const filtered = filterSearchItems(options.items, query);
|
|
9630
|
+
if (key.name === "return") {
|
|
9631
|
+
submit();
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9634
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
9635
|
+
cancel();
|
|
9636
|
+
return;
|
|
9637
|
+
}
|
|
9638
|
+
if (key.name === "up") {
|
|
9639
|
+
cursor = Math.max(0, cursor - 1);
|
|
9640
|
+
render();
|
|
9641
|
+
return;
|
|
9642
|
+
}
|
|
9643
|
+
if (key.name === "down") {
|
|
9644
|
+
cursor = Math.min(Math.max(0, filtered.length - 1), cursor + 1);
|
|
9645
|
+
render();
|
|
9646
|
+
return;
|
|
9647
|
+
}
|
|
9648
|
+
if (key.name === "space" && options.mode === "multi") {
|
|
9649
|
+
const item = filtered[cursor];
|
|
9650
|
+
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
9651
|
+
else selected.add(item.value);
|
|
9652
|
+
render();
|
|
9653
|
+
return;
|
|
9654
|
+
}
|
|
9655
|
+
if (key.name === "backspace") {
|
|
9656
|
+
query = query.slice(0, -1);
|
|
9657
|
+
cursor = 0;
|
|
9658
|
+
render();
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
9662
|
+
query += key.sequence;
|
|
9663
|
+
cursor = 0;
|
|
9664
|
+
render();
|
|
9665
|
+
}
|
|
9666
|
+
};
|
|
9667
|
+
process.stdin.on("keypress", onKeypress);
|
|
9668
|
+
render();
|
|
9669
|
+
});
|
|
9670
|
+
};
|
|
9671
|
+
const searchSelect = async (options) => await runSearchPrompt({
|
|
9672
|
+
...options,
|
|
9673
|
+
mode: "single"
|
|
9674
|
+
});
|
|
9675
|
+
const searchMultiselect = async (options) => await runSearchPrompt({
|
|
9676
|
+
...options,
|
|
9677
|
+
mode: "multi",
|
|
9678
|
+
initialSelected: options.initialSelected
|
|
9679
|
+
});
|
|
9680
|
+
|
|
9035
9681
|
//#endregion
|
|
9036
9682
|
//#region src/commands/hook.ts
|
|
9037
9683
|
const HOOK_FLUSH_TIMEOUT_MS = 1500;
|
|
@@ -9202,18 +9848,17 @@ const promptAgentSelection = async (mode, deps = {}) => {
|
|
|
9202
9848
|
const pool = mode === "uninstall" ? installed : ALL_AGENTS;
|
|
9203
9849
|
if (pool.length === 0) return [];
|
|
9204
9850
|
const preChecked = mode === "uninstall" ? installed : AGENTS_SUPPORTING_BOTH_SCOPES;
|
|
9205
|
-
|
|
9851
|
+
return await searchMultiselect({
|
|
9206
9852
|
message: mode === "install" ? "Which agents should get aislop hooks?" : "Which agent hooks should be removed?",
|
|
9207
|
-
|
|
9853
|
+
items: pool.map((a) => ({
|
|
9208
9854
|
value: a,
|
|
9209
9855
|
label: AGENT_LABELS[a].label,
|
|
9210
|
-
hint: AGENT_LABELS[a].hint
|
|
9856
|
+
hint: AGENT_LABELS[a].hint,
|
|
9857
|
+
keywords: [a]
|
|
9211
9858
|
})),
|
|
9212
|
-
|
|
9859
|
+
initialSelected: preChecked.filter((a) => pool.includes(a)),
|
|
9213
9860
|
required: false
|
|
9214
9861
|
});
|
|
9215
|
-
if (isCancel(choice)) return null;
|
|
9216
|
-
return choice;
|
|
9217
9862
|
};
|
|
9218
9863
|
|
|
9219
9864
|
//#endregion
|
|
@@ -9272,50 +9917,58 @@ const pickAgents = async (mode, opts, positional) => {
|
|
|
9272
9917
|
if (!process.stdin.isTTY) return defaultInstallTargets();
|
|
9273
9918
|
return mode === "uninstall" ? promptForUninstall() : promptForInstall();
|
|
9274
9919
|
};
|
|
9275
|
-
const
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9920
|
+
const addAgentShortcutOptions = (command) => {
|
|
9921
|
+
for (const a of AGENT_NAMES) command.option(`--${a}`, `shortcut for --agent ${a}`);
|
|
9922
|
+
return command;
|
|
9923
|
+
};
|
|
9924
|
+
const addInstallOptions = (command) => addAgentShortcutOptions(command.option("--agent <names>", "comma-separated agent list (claude,cursor,gemini,codex,windsurf,cline,kilocode,antigravity,copilot)").option("-g, --global", "install to the user-scope config (default)").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)"));
|
|
9925
|
+
const addUninstallOptions = (command) => addAgentShortcutOptions(command.option("--agent <names>", "comma-separated agent list").option("-g, --global", "uninstall from user-scope config").option("--project", "uninstall from project-scope config").option("--dry-run", "print the planned removal without writing"));
|
|
9926
|
+
const runInstallAction = async (positional, opts) => {
|
|
9927
|
+
const agents = await pickAgents("install", opts, positional);
|
|
9928
|
+
if (agents === null || agents.length === 0) return;
|
|
9929
|
+
await withCommandLifecycle({
|
|
9930
|
+
command: "hook_install",
|
|
9931
|
+
config: loadConfig(process.cwd()).telemetry
|
|
9932
|
+
}, async () => {
|
|
9933
|
+
await hookInstall({
|
|
9934
|
+
agents,
|
|
9935
|
+
scope: resolveScope(opts),
|
|
9936
|
+
dryRun: Boolean(opts.dryRun),
|
|
9937
|
+
yes: Boolean(opts.yes),
|
|
9938
|
+
qualityGate: Boolean(opts.qualityGate)
|
|
9293
9939
|
});
|
|
9940
|
+
return { exitCode: 0 };
|
|
9294
9941
|
});
|
|
9295
9942
|
};
|
|
9296
|
-
const
|
|
9297
|
-
const
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
dryRun: Boolean(opts.dryRun),
|
|
9310
|
-
yes: true,
|
|
9311
|
-
qualityGate: false
|
|
9312
|
-
});
|
|
9313
|
-
return { exitCode: 0 };
|
|
9943
|
+
const runUninstallAction = async (positional, opts) => {
|
|
9944
|
+
const agents = await pickAgents("uninstall", opts, positional);
|
|
9945
|
+
if (agents === null || agents.length === 0) return;
|
|
9946
|
+
await withCommandLifecycle({
|
|
9947
|
+
command: "hook_uninstall",
|
|
9948
|
+
config: loadConfig(process.cwd()).telemetry
|
|
9949
|
+
}, async () => {
|
|
9950
|
+
await hookUninstall({
|
|
9951
|
+
agents,
|
|
9952
|
+
scope: resolveScope(opts),
|
|
9953
|
+
dryRun: Boolean(opts.dryRun),
|
|
9954
|
+
yes: true,
|
|
9955
|
+
qualityGate: false
|
|
9314
9956
|
});
|
|
9957
|
+
return { exitCode: 0 };
|
|
9315
9958
|
});
|
|
9316
9959
|
};
|
|
9960
|
+
const normalizeHookAliasAgents = (agents) => {
|
|
9961
|
+
const [first, ...rest] = agents;
|
|
9962
|
+
return first === "hook" || first === "hooks" ? rest : agents;
|
|
9963
|
+
};
|
|
9964
|
+
const registerInstall = (hook) => {
|
|
9965
|
+
addInstallOptions(hook.command("install [agents...]").description("Install hooks for one or more coding agents. Use positional agents, per-agent flags, or --agent.")).action(runInstallAction);
|
|
9966
|
+
};
|
|
9967
|
+
const registerUninstall = (hook) => {
|
|
9968
|
+
addUninstallOptions(hook.command("uninstall [agents...]").description("Remove hooks for one or more coding agents. Use positional agents, per-agent flags, or --agent.")).action(runUninstallAction);
|
|
9969
|
+
};
|
|
9317
9970
|
const registerCallbacks = (hook) => {
|
|
9318
|
-
hook.command("status").description("Show
|
|
9971
|
+
hook.command("status").description("Show installed agent hooks").action(async () => {
|
|
9319
9972
|
await withCommandLifecycle({
|
|
9320
9973
|
command: "hook_status",
|
|
9321
9974
|
config: loadConfig(process.cwd()).telemetry
|
|
@@ -9324,7 +9977,7 @@ const registerCallbacks = (hook) => {
|
|
|
9324
9977
|
return { exitCode: 0 };
|
|
9325
9978
|
});
|
|
9326
9979
|
});
|
|
9327
|
-
hook.command("baseline").description("Capture the current
|
|
9980
|
+
hook.command("baseline").description("Capture the current score as the hook baseline").action(async () => {
|
|
9328
9981
|
await withCommandLifecycle({
|
|
9329
9982
|
command: "hook_baseline",
|
|
9330
9983
|
config: loadConfig(process.cwd()).telemetry
|
|
@@ -9333,28 +9986,36 @@ const registerCallbacks = (hook) => {
|
|
|
9333
9986
|
return { exitCode: 0 };
|
|
9334
9987
|
});
|
|
9335
9988
|
});
|
|
9336
|
-
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
|
|
9989
|
+
hook.command("claude", { hidden: true }).description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
|
|
9337
9990
|
await hookRun("claude", {
|
|
9338
9991
|
stop: Boolean(opts.stop),
|
|
9339
9992
|
onFileChanged: Boolean(opts.onFileChanged)
|
|
9340
9993
|
});
|
|
9341
9994
|
});
|
|
9342
|
-
hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
9995
|
+
hook.command("cursor", { hidden: true }).description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
9343
9996
|
await hookRun("cursor");
|
|
9344
9997
|
});
|
|
9345
|
-
hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
9998
|
+
hook.command("gemini", { hidden: true }).description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
|
|
9346
9999
|
await hookRun("gemini");
|
|
9347
10000
|
});
|
|
9348
|
-
hook.command("pi").description("Internal: pi extension tool_result callback (reads stdin)").action(async () => {
|
|
10001
|
+
hook.command("pi", { hidden: true }).description("Internal: pi extension tool_result callback (reads stdin)").action(async () => {
|
|
9349
10002
|
await hookRun("pi");
|
|
9350
10003
|
});
|
|
9351
10004
|
};
|
|
9352
10005
|
const registerHookCommand = (program) => {
|
|
9353
|
-
const hook = program.command("hook").description("
|
|
10006
|
+
const hook = program.command("hook").alias("hooks").description("Manage per-edit coding-agent hooks");
|
|
9354
10007
|
registerInstall(hook);
|
|
9355
10008
|
registerUninstall(hook);
|
|
9356
10009
|
registerCallbacks(hook);
|
|
9357
10010
|
};
|
|
10011
|
+
const registerHookAliases = (program) => {
|
|
10012
|
+
addInstallOptions(program.command("install [agents...]").description("Install coding-agent hooks (alias: hook install)")).action(async (agents, opts) => {
|
|
10013
|
+
await runInstallAction(normalizeHookAliasAgents(agents), opts);
|
|
10014
|
+
});
|
|
10015
|
+
addUninstallOptions(program.command("uninstall [agents...]").description("Remove coding-agent hooks (alias: hook uninstall)")).action(async (agents, opts) => {
|
|
10016
|
+
await runUninstallAction(normalizeHookAliasAgents(agents), opts);
|
|
10017
|
+
});
|
|
10018
|
+
};
|
|
9358
10019
|
|
|
9359
10020
|
//#endregion
|
|
9360
10021
|
//#region src/commands/badge.ts
|
|
@@ -9433,40 +10094,6 @@ const badgeCommand = async (options = {}) => {
|
|
|
9433
10094
|
};
|
|
9434
10095
|
};
|
|
9435
10096
|
|
|
9436
|
-
//#endregion
|
|
9437
|
-
//#region src/ui/symbols.ts
|
|
9438
|
-
const TTY = {
|
|
9439
|
-
stepActive: "◇",
|
|
9440
|
-
stepDone: "◆",
|
|
9441
|
-
rail: "│",
|
|
9442
|
-
railEnd: "└",
|
|
9443
|
-
bullet: "●",
|
|
9444
|
-
hint: "→",
|
|
9445
|
-
pass: "✓",
|
|
9446
|
-
fail: "✗",
|
|
9447
|
-
warn: "!",
|
|
9448
|
-
pending: "•",
|
|
9449
|
-
engineActive: "⏵",
|
|
9450
|
-
neutral: "─"
|
|
9451
|
-
};
|
|
9452
|
-
const PLAIN = {
|
|
9453
|
-
stepActive: "*",
|
|
9454
|
-
stepDone: "*",
|
|
9455
|
-
rail: "|",
|
|
9456
|
-
railEnd: "+",
|
|
9457
|
-
bullet: "-",
|
|
9458
|
-
hint: "->",
|
|
9459
|
-
pass: "[ok]",
|
|
9460
|
-
fail: "[x]",
|
|
9461
|
-
warn: "[!]",
|
|
9462
|
-
pending: "-",
|
|
9463
|
-
engineActive: ">",
|
|
9464
|
-
neutral: "-"
|
|
9465
|
-
};
|
|
9466
|
-
const createSymbols = (opts = {}) => opts.plain ? PLAIN : TTY;
|
|
9467
|
-
const isPlain = () => process.env.THEME === "plain" || Boolean(process.env.NO_COLOR) || !process.stdout.isTTY;
|
|
9468
|
-
const symbols = createSymbols({ plain: isPlain() });
|
|
9469
|
-
|
|
9470
10097
|
//#endregion
|
|
9471
10098
|
//#region src/ui/error.ts
|
|
9472
10099
|
const renderError = (input, deps = {}) => {
|
|
@@ -9721,30 +10348,6 @@ const renderHeader = (input, _deps = {}) => {
|
|
|
9721
10348
|
return subLine ? `${brandLine}\n\n${subLine}\n\n` : `${brandLine}\n\n`;
|
|
9722
10349
|
};
|
|
9723
10350
|
|
|
9724
|
-
//#endregion
|
|
9725
|
-
//#region src/ui/width.ts
|
|
9726
|
-
const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
|
|
9727
|
-
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
9728
|
-
const stringWidth = (s) => {
|
|
9729
|
-
const bare = stripAnsi(s);
|
|
9730
|
-
let total = 0;
|
|
9731
|
-
for (const ch of bare) {
|
|
9732
|
-
const w = wcwidth(ch.codePointAt(0) ?? 0);
|
|
9733
|
-
total += w > 0 ? w : 1;
|
|
9734
|
-
}
|
|
9735
|
-
return total;
|
|
9736
|
-
};
|
|
9737
|
-
const padEnd = (s, target, fill = " ") => {
|
|
9738
|
-
const w = stringWidth(s);
|
|
9739
|
-
if (w >= target) return s;
|
|
9740
|
-
return s + fill.repeat(target - w);
|
|
9741
|
-
};
|
|
9742
|
-
const padStart = (s, target, fill = " ") => {
|
|
9743
|
-
const w = stringWidth(s);
|
|
9744
|
-
if (w >= target) return s;
|
|
9745
|
-
return fill.repeat(target - w) + s;
|
|
9746
|
-
};
|
|
9747
|
-
|
|
9748
10351
|
//#endregion
|
|
9749
10352
|
//#region src/ui/live-grid.ts
|
|
9750
10353
|
const SPINNER = [
|
|
@@ -9862,6 +10465,199 @@ var LiveGrid = class {
|
|
|
9862
10465
|
}
|
|
9863
10466
|
};
|
|
9864
10467
|
|
|
10468
|
+
//#endregion
|
|
10469
|
+
//#region src/utils/history.ts
|
|
10470
|
+
const HISTORY_FILE = "history.jsonl";
|
|
10471
|
+
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
10472
|
+
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
10473
|
+
/**
|
|
10474
|
+
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
10475
|
+
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
10476
|
+
*/
|
|
10477
|
+
const appendHistory = (input) => {
|
|
10478
|
+
if (isHistoryDisabled()) return;
|
|
10479
|
+
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
10480
|
+
if (!fs.existsSync(configDir)) return;
|
|
10481
|
+
const record = {
|
|
10482
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10483
|
+
score: input.score,
|
|
10484
|
+
errors: input.errors,
|
|
10485
|
+
warnings: input.warnings,
|
|
10486
|
+
files: input.files,
|
|
10487
|
+
cliVersion: APP_VERSION
|
|
10488
|
+
};
|
|
10489
|
+
try {
|
|
10490
|
+
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
10491
|
+
} catch {}
|
|
10492
|
+
};
|
|
10493
|
+
const isHistoryRecord = (value) => {
|
|
10494
|
+
if (!value || typeof value !== "object") return false;
|
|
10495
|
+
const record = value;
|
|
10496
|
+
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";
|
|
10497
|
+
};
|
|
10498
|
+
const readHistory = (directory) => {
|
|
10499
|
+
const file = historyPath(directory);
|
|
10500
|
+
if (!fs.existsSync(file)) return [];
|
|
10501
|
+
const records = [];
|
|
10502
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
10503
|
+
const trimmed = line.trim();
|
|
10504
|
+
if (!trimmed) continue;
|
|
10505
|
+
try {
|
|
10506
|
+
const parsed = JSON.parse(trimmed);
|
|
10507
|
+
if (isHistoryRecord(parsed)) records.push(parsed);
|
|
10508
|
+
} catch {}
|
|
10509
|
+
}
|
|
10510
|
+
return records;
|
|
10511
|
+
};
|
|
10512
|
+
|
|
10513
|
+
//#endregion
|
|
10514
|
+
//#region src/commands/scan-coverage.ts
|
|
10515
|
+
const coverageReason = (c) => {
|
|
10516
|
+
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which aislop does not analyze. No score — it would not reflect this code.`;
|
|
10517
|
+
if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
|
|
10518
|
+
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
10519
|
+
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
10520
|
+
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
10521
|
+
};
|
|
10522
|
+
const renderCoverageNotice = (projectInfo, includeHeader) => {
|
|
10523
|
+
const deps = {
|
|
10524
|
+
theme: createTheme(),
|
|
10525
|
+
symbols: createSymbols({ plain: false })
|
|
10526
|
+
};
|
|
10527
|
+
return `${includeHeader === false ? "" : renderHeader({
|
|
10528
|
+
version: APP_VERSION,
|
|
10529
|
+
command: "Scan result",
|
|
10530
|
+
context: [
|
|
10531
|
+
projectInfo.projectName,
|
|
10532
|
+
projectInfo.languages[0] ?? "unknown",
|
|
10533
|
+
`${projectInfo.sourceFileCount} files`
|
|
10534
|
+
],
|
|
10535
|
+
brand: true
|
|
10536
|
+
}, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
|
|
10537
|
+
};
|
|
10538
|
+
|
|
10539
|
+
//#endregion
|
|
10540
|
+
//#region src/commands/scan-exit-code.ts
|
|
10541
|
+
const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
10542
|
+
|
|
10543
|
+
//#endregion
|
|
10544
|
+
//#region src/output/finding-assessment.ts
|
|
10545
|
+
const KNIP_FORCE_RULES = new Set([
|
|
10546
|
+
"knip/files",
|
|
10547
|
+
"knip/dependencies",
|
|
10548
|
+
"knip/devDependencies"
|
|
10549
|
+
]);
|
|
10550
|
+
const isForceFixable = (diagnostic) => {
|
|
10551
|
+
if (diagnostic.fixable) return false;
|
|
10552
|
+
if (KNIP_FORCE_RULES.has(diagnostic.rule)) return true;
|
|
10553
|
+
if (diagnostic.rule === "security/vulnerable-dependency") return diagnostic.detail === "npm" || diagnostic.detail === "pnpm";
|
|
10554
|
+
if (diagnostic.rule.startsWith("expo-doctor/")) return diagnostic.rule !== "expo-doctor/config-error";
|
|
10555
|
+
return false;
|
|
10556
|
+
};
|
|
10557
|
+
const FINDING_KIND_LABELS = {
|
|
10558
|
+
"confirmed-defect": "confirmed defects",
|
|
10559
|
+
"conservative-security": "conservative security",
|
|
10560
|
+
"style-policy": "style/policy",
|
|
10561
|
+
"ai-slop-indicator": "AI-slop indicators"
|
|
10562
|
+
};
|
|
10563
|
+
const STYLE_POLICY_RULES = new Set([
|
|
10564
|
+
"ai-slop/trivial-comment",
|
|
10565
|
+
"ai-slop/narrative-comment",
|
|
10566
|
+
"ai-slop/meta-comment",
|
|
10567
|
+
"ai-slop/console-leftover",
|
|
10568
|
+
"ai-slop/ts-directive",
|
|
10569
|
+
"complexity/file-too-large",
|
|
10570
|
+
"complexity/function-too-long",
|
|
10571
|
+
"complexity/deep-nesting",
|
|
10572
|
+
"complexity/too-many-params",
|
|
10573
|
+
"code-quality/duplicate-block",
|
|
10574
|
+
"eslint/no-empty",
|
|
10575
|
+
"eslint/no-unused-vars",
|
|
10576
|
+
"eslint/no-useless-escape",
|
|
10577
|
+
"eslint/no-unused-expressions",
|
|
10578
|
+
"unicorn/no-useless-fallback-in-spread",
|
|
10579
|
+
"unicorn/prefer-string-starts-ends-with",
|
|
10580
|
+
"unicorn/no-new-array",
|
|
10581
|
+
"unicorn/no-useless-spread"
|
|
10582
|
+
]);
|
|
10583
|
+
const CONFIRMED_DEFECT_RULES = new Set([
|
|
10584
|
+
"ai-slop/hallucinated-import",
|
|
10585
|
+
"eslint/no-undef",
|
|
10586
|
+
"eslint/no-unreachable",
|
|
10587
|
+
"security/vulnerable-dependency"
|
|
10588
|
+
]);
|
|
10589
|
+
const LOW_CONFIDENCE_SECURITY_RULES = new Set(["security/innerhtml", "security/dangerously-set-innerhtml"]);
|
|
10590
|
+
const confidenceFor = (diagnostic, kind) => {
|
|
10591
|
+
if (kind === "confirmed-defect") return "high";
|
|
10592
|
+
if (kind === "style-policy") return "medium";
|
|
10593
|
+
if (kind === "conservative-security") {
|
|
10594
|
+
if (LOW_CONFIDENCE_SECURITY_RULES.has(diagnostic.rule)) return "medium";
|
|
10595
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
10596
|
+
}
|
|
10597
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
10598
|
+
};
|
|
10599
|
+
const classifyKind = (diagnostic) => {
|
|
10600
|
+
if (CONFIRMED_DEFECT_RULES.has(diagnostic.rule)) return "confirmed-defect";
|
|
10601
|
+
if (diagnostic.engine === "security") return "conservative-security";
|
|
10602
|
+
if (STYLE_POLICY_RULES.has(diagnostic.rule)) return "style-policy";
|
|
10603
|
+
if (diagnostic.engine === "format" || diagnostic.engine === "code-quality") return "style-policy";
|
|
10604
|
+
if (diagnostic.engine === "ai-slop") return "ai-slop-indicator";
|
|
10605
|
+
if (diagnostic.severity === "error") return "confirmed-defect";
|
|
10606
|
+
return "style-policy";
|
|
10607
|
+
};
|
|
10608
|
+
const assessDiagnostic = (diagnostic) => {
|
|
10609
|
+
const kind = classifyKind(diagnostic);
|
|
10610
|
+
return {
|
|
10611
|
+
kind,
|
|
10612
|
+
confidence: confidenceFor(diagnostic, kind),
|
|
10613
|
+
label: FINDING_KIND_LABELS[kind]
|
|
10614
|
+
};
|
|
10615
|
+
};
|
|
10616
|
+
const withFindingAssessments = (diagnostics) => diagnostics.map((diagnostic) => ({
|
|
10617
|
+
...diagnostic,
|
|
10618
|
+
assessment: assessDiagnostic(diagnostic),
|
|
10619
|
+
forceFixable: isForceFixable(diagnostic)
|
|
10620
|
+
}));
|
|
10621
|
+
const summarizeFindingAssessments = (diagnostics) => {
|
|
10622
|
+
const byKind = {
|
|
10623
|
+
"confirmed-defect": 0,
|
|
10624
|
+
"conservative-security": 0,
|
|
10625
|
+
"style-policy": 0,
|
|
10626
|
+
"ai-slop-indicator": 0
|
|
10627
|
+
};
|
|
10628
|
+
const byConfidence = {
|
|
10629
|
+
high: 0,
|
|
10630
|
+
medium: 0,
|
|
10631
|
+
low: 0
|
|
10632
|
+
};
|
|
10633
|
+
const rows = /* @__PURE__ */ new Map();
|
|
10634
|
+
for (const diagnostic of diagnostics) {
|
|
10635
|
+
const assessment = assessDiagnostic(diagnostic);
|
|
10636
|
+
byKind[assessment.kind]++;
|
|
10637
|
+
byConfidence[assessment.confidence]++;
|
|
10638
|
+
const row = rows.get(assessment.kind) ?? {
|
|
10639
|
+
kind: assessment.kind,
|
|
10640
|
+
label: assessment.label,
|
|
10641
|
+
count: 0,
|
|
10642
|
+
errors: 0,
|
|
10643
|
+
warnings: 0,
|
|
10644
|
+
info: 0,
|
|
10645
|
+
fixable: 0
|
|
10646
|
+
};
|
|
10647
|
+
row.count++;
|
|
10648
|
+
if (diagnostic.severity === "error") row.errors++;
|
|
10649
|
+
else if (diagnostic.severity === "warning") row.warnings++;
|
|
10650
|
+
else row.info++;
|
|
10651
|
+
if (diagnostic.fixable) row.fixable++;
|
|
10652
|
+
rows.set(assessment.kind, row);
|
|
10653
|
+
}
|
|
10654
|
+
return {
|
|
10655
|
+
rows: [...rows.values()].sort((a, b) => b.count - a.count),
|
|
10656
|
+
byKind,
|
|
10657
|
+
byConfidence
|
|
10658
|
+
};
|
|
10659
|
+
};
|
|
10660
|
+
|
|
9865
10661
|
//#endregion
|
|
9866
10662
|
//#region src/output/rule-labels.ts
|
|
9867
10663
|
const RULE_LABELS = {
|
|
@@ -9946,11 +10742,85 @@ const RULE_LABELS = {
|
|
|
9946
10742
|
"unicorn/no-useless-spread": "Useless spread",
|
|
9947
10743
|
"unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
|
|
9948
10744
|
};
|
|
10745
|
+
const RULE_DESCRIPTIONS = {
|
|
10746
|
+
formatting: "File needs standard formatter output.",
|
|
10747
|
+
"code-quality/duplicate-block": "Large repeated code block should be shared or simplified.",
|
|
10748
|
+
"code-quality/repeated-chained-call": "Same chained call is repeated instead of stored once.",
|
|
10749
|
+
"code-quality/unused-declaration": "Declared symbol is not referenced.",
|
|
10750
|
+
"complexity/file-too-large": "File is large enough to be hard to review safely.",
|
|
10751
|
+
"complexity/function-too-long": "Function is doing too much in one body.",
|
|
10752
|
+
"complexity/deep-nesting": "Nested branches make the path hard to follow.",
|
|
10753
|
+
"complexity/too-many-params": "Function takes more arguments than readers can track.",
|
|
10754
|
+
"knip/files": "Source file is not imported or referenced.",
|
|
10755
|
+
"knip/dependencies": "Production dependency is listed but unused.",
|
|
10756
|
+
"knip/devDependencies": "Dev dependency is listed but unused.",
|
|
10757
|
+
"knip/unlisted": "Code imports a package missing from package.json.",
|
|
10758
|
+
"knip/unresolved": "Import cannot be resolved from the project.",
|
|
10759
|
+
"knip/binaries": "Package binary is listed but unused.",
|
|
10760
|
+
"knip/exports": "Exported value is not imported anywhere.",
|
|
10761
|
+
"knip/types": "Exported type is not imported anywhere.",
|
|
10762
|
+
"knip/duplicates": "Same export is declared more than once.",
|
|
10763
|
+
"ai-slop/trivial-comment": "Comment repeats obvious code instead of explaining intent.",
|
|
10764
|
+
"ai-slop/swallowed-exception": "Catch block hides an error without handling it.",
|
|
10765
|
+
"ai-slop/silent-recovery": "Error path logs or defaults, then continues as if safe.",
|
|
10766
|
+
"ai-slop/meta-comment": "Comment describes editing steps, plans, or generated-code process.",
|
|
10767
|
+
"ai-slop/redundant-try-catch": "try/catch only rethrows or adds no useful handling.",
|
|
10768
|
+
"ai-slop/redundant-type-coercion": "Conversion does not change the value meaningfully.",
|
|
10769
|
+
"ai-slop/duplicate-type-declaration": "Same exported type shape appears more than once.",
|
|
10770
|
+
"ai-slop/thin-wrapper": "Wrapper function adds no behavior or clearer contract.",
|
|
10771
|
+
"ai-slop/generic-naming": "Name is too vague to explain its role.",
|
|
10772
|
+
"ai-slop/unused-import": "Imported symbol is never used.",
|
|
10773
|
+
"ai-slop/console-leftover": "console/debug output was left in application code.",
|
|
10774
|
+
"ai-slop/todo-stub": "TODO/FIXME/stub marks unfinished behavior.",
|
|
10775
|
+
"ai-slop/unreachable-code": "Code path cannot execute.",
|
|
10776
|
+
"ai-slop/constant-condition": "Condition is always true or always false.",
|
|
10777
|
+
"ai-slop/empty-function": "Function body is empty or placeholder-only.",
|
|
10778
|
+
"ai-slop/unsafe-type-assertion": "Type assertion bypasses useful checking.",
|
|
10779
|
+
"ai-slop/double-type-assertion": "Value is cast through unknown/any to force a type.",
|
|
10780
|
+
"ai-slop/ts-directive": "TypeScript error is suppressed with a directive.",
|
|
10781
|
+
"ai-slop/narrative-comment": "Comment narrates implementation instead of adding context.",
|
|
10782
|
+
"ai-slop/duplicate-import": "Same module is imported more than once.",
|
|
10783
|
+
"ai-slop/hardcoded-url": "URL-like value is embedded directly in code.",
|
|
10784
|
+
"ai-slop/hardcoded-id": "Provider/account/test ID is embedded directly in code.",
|
|
10785
|
+
"ai-slop/python-bare-except": "Bare except catches everything, including system exits.",
|
|
10786
|
+
"ai-slop/python-broad-except": "Broad exception catch hides specific failure modes.",
|
|
10787
|
+
"ai-slop/python-mutable-default": "Mutable default argument is shared across calls.",
|
|
10788
|
+
"ai-slop/python-print-debug": "print/debug output was left in Python source.",
|
|
10789
|
+
"ai-slop/python-range-len-loop": "Loop uses indexes where direct iteration is clearer.",
|
|
10790
|
+
"ai-slop/python-chained-dict-get": "Nested get chain hides shape assumptions.",
|
|
10791
|
+
"ai-slop/python-repetitive-dispatch": "Repeated if/elif dispatch should be table-driven.",
|
|
10792
|
+
"ai-slop/python-isinstance-ladder": "Long isinstance ladder is brittle polymorphism.",
|
|
10793
|
+
"ai-slop/go-library-panic": "Library code panics instead of returning an error.",
|
|
10794
|
+
"ai-slop/rust-non-test-unwrap": "Production Rust uses unwrap instead of handling failure.",
|
|
10795
|
+
"ai-slop/rust-todo-stub": "Rust todo!/unimplemented! leaves behavior unfinished.",
|
|
10796
|
+
"ai-slop/hallucinated-import": "Import names a package not declared by the project.",
|
|
10797
|
+
"security/hardcoded-secret": "Secret-looking token is embedded in source.",
|
|
10798
|
+
"security/vulnerable-dependency": "Dependency audit reported a known vulnerability.",
|
|
10799
|
+
"security/dependency-audit-skipped": "Audit could not run because inputs/tools are missing.",
|
|
10800
|
+
"security/eval": "Dynamic code execution can run attacker-controlled input.",
|
|
10801
|
+
"security/innerhtml": "Raw HTML assignment can introduce XSS.",
|
|
10802
|
+
"security/dangerously-set-innerhtml": "React raw HTML escape hatch can introduce XSS.",
|
|
10803
|
+
"security/sql-injection": "SQL is built from interpolated or concatenated input.",
|
|
10804
|
+
"security/shell-injection": "Shell command is built from unsanitized input.",
|
|
10805
|
+
"oxlint/*": "JavaScript/TypeScript lint finding from oxlint.",
|
|
10806
|
+
"ruff/*": "Python lint finding from ruff.",
|
|
10807
|
+
"go/*": "Go lint finding from bundled checks.",
|
|
10808
|
+
"clippy/*": "Rust lint finding from clippy.",
|
|
10809
|
+
"rubocop/*": "Ruby lint finding from rubocop.",
|
|
10810
|
+
"typescript/*": "TypeScript compiler finding.",
|
|
10811
|
+
"import-order": "Imports need deterministic ordering.",
|
|
10812
|
+
"python-formatting": "Python file needs ruff formatting.",
|
|
10813
|
+
"go-formatting": "Go file needs gofmt.",
|
|
10814
|
+
"rust-formatting": "Rust file needs rustfmt.",
|
|
10815
|
+
"ruby-formatting": "Ruby file needs rubocop formatting.",
|
|
10816
|
+
"php-formatting": "PHP file needs php-cs-fixer formatting."
|
|
10817
|
+
};
|
|
9949
10818
|
const prettifyFallback = (ruleId) => {
|
|
9950
10819
|
const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
|
|
9951
10820
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
9952
10821
|
};
|
|
9953
10822
|
const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
|
|
10823
|
+
const descriptionForRule = (ruleId) => RULE_DESCRIPTIONS[ruleId] ?? labelForRule(ruleId);
|
|
9954
10824
|
|
|
9955
10825
|
//#endregion
|
|
9956
10826
|
//#region src/ui/summary.ts
|
|
@@ -9960,6 +10830,18 @@ const scoreToken = (score, thresholds) => {
|
|
|
9960
10830
|
if (score >= thresholds.ok) return "warn";
|
|
9961
10831
|
return "danger";
|
|
9962
10832
|
};
|
|
10833
|
+
const renderFindingAssessment = (assessment, t, sep) => {
|
|
10834
|
+
if (assessment.rows.length === 0) return [];
|
|
10835
|
+
const parts = assessment.rows.filter((row) => row.count > 0).map((row) => `${row.count} ${row.label}`);
|
|
10836
|
+
if (parts.length === 0) return [];
|
|
10837
|
+
const high = assessment.byConfidence.high;
|
|
10838
|
+
const medium = assessment.byConfidence.medium;
|
|
10839
|
+
const confidenceParts = [];
|
|
10840
|
+
if (high > 0) confidenceParts.push(`${high} high-confidence`);
|
|
10841
|
+
if (medium > 0) confidenceParts.push(`${medium} medium-confidence`);
|
|
10842
|
+
const confidence = confidenceParts.length > 0 ? ` ${sep} ${style(t, "muted", confidenceParts.join(", "))}` : "";
|
|
10843
|
+
return [` ${style(t, "muted", "Verdict mix:")} ${parts.join(` ${sep} `)}${confidence}`];
|
|
10844
|
+
};
|
|
9963
10845
|
const renderSummary = (input, deps = {}) => {
|
|
9964
10846
|
const t = deps.theme ?? theme;
|
|
9965
10847
|
const s = deps.symbols ?? symbols;
|
|
@@ -9978,6 +10860,10 @@ const renderSummary = (input, deps = {}) => {
|
|
|
9978
10860
|
` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
|
|
9979
10861
|
""
|
|
9980
10862
|
];
|
|
10863
|
+
if (input.findingAssessment) {
|
|
10864
|
+
lines.push(...renderFindingAssessment(input.findingAssessment, t, sep));
|
|
10865
|
+
lines.push("");
|
|
10866
|
+
}
|
|
9981
10867
|
if (input.breakdown && input.breakdown.rows.length > 0) {
|
|
9982
10868
|
lines.push(` ${style(t, "bold", "Top findings")}`);
|
|
9983
10869
|
const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
|
|
@@ -10032,85 +10918,7 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
10032
10918
|
};
|
|
10033
10919
|
|
|
10034
10920
|
//#endregion
|
|
10035
|
-
//#region src/
|
|
10036
|
-
const HISTORY_FILE = "history.jsonl";
|
|
10037
|
-
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
10038
|
-
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
10039
|
-
/**
|
|
10040
|
-
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
10041
|
-
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
10042
|
-
*/
|
|
10043
|
-
const appendHistory = (input) => {
|
|
10044
|
-
if (isHistoryDisabled()) return;
|
|
10045
|
-
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
10046
|
-
if (!fs.existsSync(configDir)) return;
|
|
10047
|
-
const record = {
|
|
10048
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10049
|
-
score: input.score,
|
|
10050
|
-
errors: input.errors,
|
|
10051
|
-
warnings: input.warnings,
|
|
10052
|
-
files: input.files,
|
|
10053
|
-
cliVersion: APP_VERSION
|
|
10054
|
-
};
|
|
10055
|
-
try {
|
|
10056
|
-
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
10057
|
-
} catch {}
|
|
10058
|
-
};
|
|
10059
|
-
const isHistoryRecord = (value) => {
|
|
10060
|
-
if (!value || typeof value !== "object") return false;
|
|
10061
|
-
const record = value;
|
|
10062
|
-
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";
|
|
10063
|
-
};
|
|
10064
|
-
const readHistory = (directory) => {
|
|
10065
|
-
const file = historyPath(directory);
|
|
10066
|
-
if (!fs.existsSync(file)) return [];
|
|
10067
|
-
const records = [];
|
|
10068
|
-
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
10069
|
-
const trimmed = line.trim();
|
|
10070
|
-
if (!trimmed) continue;
|
|
10071
|
-
try {
|
|
10072
|
-
const parsed = JSON.parse(trimmed);
|
|
10073
|
-
if (isHistoryRecord(parsed)) records.push(parsed);
|
|
10074
|
-
} catch {}
|
|
10075
|
-
}
|
|
10076
|
-
return records;
|
|
10077
|
-
};
|
|
10078
|
-
|
|
10079
|
-
//#endregion
|
|
10080
|
-
//#region src/commands/scan-coverage.ts
|
|
10081
|
-
const coverageReason = (c) => {
|
|
10082
|
-
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which aislop does not analyze. No score — it would not reflect this code.`;
|
|
10083
|
-
if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
|
|
10084
|
-
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
10085
|
-
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
10086
|
-
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
10087
|
-
};
|
|
10088
|
-
const renderCoverageNotice = (projectInfo, includeHeader) => {
|
|
10089
|
-
const deps = {
|
|
10090
|
-
theme: createTheme(),
|
|
10091
|
-
symbols: createSymbols({ plain: false })
|
|
10092
|
-
};
|
|
10093
|
-
return `${includeHeader === false ? "" : renderHeader({
|
|
10094
|
-
version: APP_VERSION,
|
|
10095
|
-
command: "scan",
|
|
10096
|
-
context: [
|
|
10097
|
-
projectInfo.projectName,
|
|
10098
|
-
projectInfo.languages[0] ?? "unknown",
|
|
10099
|
-
`${projectInfo.sourceFileCount} files`
|
|
10100
|
-
],
|
|
10101
|
-
brand: true
|
|
10102
|
-
}, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
|
|
10103
|
-
};
|
|
10104
|
-
|
|
10105
|
-
//#endregion
|
|
10106
|
-
//#region src/commands/scan-exit-code.ts
|
|
10107
|
-
const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
10108
|
-
|
|
10109
|
-
//#endregion
|
|
10110
|
-
//#region src/commands/scan.ts
|
|
10111
|
-
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
10112
|
-
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
10113
|
-
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
10921
|
+
//#region src/commands/scan-render.ts
|
|
10114
10922
|
const BREAKDOWN_TOP_N = 10;
|
|
10115
10923
|
const computeBreakdown = (diagnostics) => {
|
|
10116
10924
|
const byRule = /* @__PURE__ */ new Map();
|
|
@@ -10152,7 +10960,7 @@ const buildScanRender = (input) => {
|
|
|
10152
10960
|
const invocation = detectInvocation();
|
|
10153
10961
|
const header = input.includeHeader === false ? "" : renderHeader({
|
|
10154
10962
|
version: APP_VERSION,
|
|
10155
|
-
command: "
|
|
10963
|
+
command: "Scan result",
|
|
10156
10964
|
context: [
|
|
10157
10965
|
input.projectName,
|
|
10158
10966
|
input.language,
|
|
@@ -10195,9 +11003,16 @@ const buildScanRender = (input) => {
|
|
|
10195
11003
|
elapsedMs: input.elapsedMs,
|
|
10196
11004
|
nextSteps,
|
|
10197
11005
|
breakdown: computeBreakdown(input.diagnostics),
|
|
11006
|
+
findingAssessment: summarizeFindingAssessments(input.diagnostics),
|
|
10198
11007
|
thresholds: input.thresholds
|
|
10199
11008
|
}, deps)}${starCta}`;
|
|
10200
11009
|
};
|
|
11010
|
+
|
|
11011
|
+
//#endregion
|
|
11012
|
+
//#region src/commands/scan.ts
|
|
11013
|
+
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
11014
|
+
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
11015
|
+
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
10201
11016
|
const scanCommand = async (directory, config, options) => {
|
|
10202
11017
|
const resolvedDir = path.resolve(directory);
|
|
10203
11018
|
if (!fs.existsSync(resolvedDir)) {
|
|
@@ -10212,6 +11027,12 @@ const scanCommand = async (directory, config, options) => {
|
|
|
10212
11027
|
else log.error(msg);
|
|
10213
11028
|
return { exitCode: 1 };
|
|
10214
11029
|
}
|
|
11030
|
+
if (options.changes && options.base && !baseRefExists(resolvedDir, options.base)) {
|
|
11031
|
+
const msg = `Could not resolve base ref "${options.base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${options.base}\`).`;
|
|
11032
|
+
if (options.json) console.log(JSON.stringify({ error: msg }, null, 2));
|
|
11033
|
+
else log.error(msg);
|
|
11034
|
+
return { exitCode: 1 };
|
|
11035
|
+
}
|
|
10215
11036
|
const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
|
|
10216
11037
|
return withCommandLifecycle({
|
|
10217
11038
|
command: options.command ?? "scan",
|
|
@@ -10225,14 +11046,30 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10225
11046
|
const showHeader = options.showHeader !== false;
|
|
10226
11047
|
const machineOutput = isMachineOutput(options);
|
|
10227
11048
|
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
11049
|
+
const projectName = projectInfo.projectName ?? "project";
|
|
11050
|
+
const language = projectInfo.languages[0] ?? "unknown";
|
|
11051
|
+
const printedHumanHeader = !machineOutput && showHeader;
|
|
11052
|
+
if (printedHumanHeader) process.stdout.write(renderHeader({
|
|
11053
|
+
version: APP_VERSION,
|
|
11054
|
+
command: "Scan result",
|
|
11055
|
+
context: [
|
|
11056
|
+
projectName,
|
|
11057
|
+
language,
|
|
11058
|
+
`${projectInfo.sourceFileCount} files`
|
|
11059
|
+
],
|
|
11060
|
+
brand: options.printBrand !== false
|
|
11061
|
+
}));
|
|
10228
11062
|
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
10229
11063
|
let files;
|
|
10230
11064
|
if (options.staged) {
|
|
10231
11065
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
|
|
10232
11066
|
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
10233
11067
|
} else if (options.changes) {
|
|
10234
|
-
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
|
|
10235
|
-
if (!machineOutput)
|
|
11068
|
+
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir, options.base), [], excludePatterns);
|
|
11069
|
+
if (!machineOutput) {
|
|
11070
|
+
const scope = options.base ? `changed vs ${options.base}` : "changed";
|
|
11071
|
+
log.muted(`Scope: ${files.length} ${scope} file(s)`);
|
|
11072
|
+
}
|
|
10236
11073
|
} else {
|
|
10237
11074
|
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
|
|
10238
11075
|
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
@@ -10295,7 +11132,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10295
11132
|
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
|
|
10296
11133
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
10297
11134
|
const elapsedMs = performance.now() - startTime;
|
|
10298
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
11135
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
10299
11136
|
const scoreable = projectInfo.coverage.scoreable;
|
|
10300
11137
|
const exitCode = computeScanExitCode({
|
|
10301
11138
|
hasErrors: allDiagnostics.some((d) => d.severity === "error"),
|
|
@@ -10321,19 +11158,19 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10321
11158
|
engineTimings
|
|
10322
11159
|
};
|
|
10323
11160
|
if (options.sarif) {
|
|
10324
|
-
const { buildSarifLog } = await import("./sarif-
|
|
11161
|
+
const { buildSarifLog } = await import("./sarif-CjxSBcqx.js");
|
|
10325
11162
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
10326
11163
|
return completion;
|
|
10327
11164
|
}
|
|
10328
11165
|
if (options.json) {
|
|
10329
|
-
const { buildJsonOutput } = await import("./json-
|
|
11166
|
+
const { buildJsonOutput } = await import("./json-pHsqtKkz.js");
|
|
10330
11167
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs, projectInfo.coverage);
|
|
10331
11168
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
10332
11169
|
return completion;
|
|
10333
11170
|
}
|
|
10334
11171
|
if (!scoreable) {
|
|
10335
11172
|
if (!machineOutput) {
|
|
10336
|
-
process.stdout.write(renderCoverageNotice(projectInfo, showHeader));
|
|
11173
|
+
process.stdout.write(renderCoverageNotice(projectInfo, !printedHumanHeader && showHeader));
|
|
10337
11174
|
if (allDiagnostics.length > 0) process.stdout.write(renderDiagnostics(allDiagnostics, options.verbose ?? false));
|
|
10338
11175
|
}
|
|
10339
11176
|
return completion;
|
|
@@ -10345,8 +11182,6 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10345
11182
|
warnings: completion.warningCount,
|
|
10346
11183
|
files: projectInfo.sourceFileCount
|
|
10347
11184
|
});
|
|
10348
|
-
const projectName = projectInfo.projectName ?? "project";
|
|
10349
|
-
const language = projectInfo.languages[0] ?? "unknown";
|
|
10350
11185
|
process.stdout.write(buildScanRender({
|
|
10351
11186
|
projectName,
|
|
10352
11187
|
language,
|
|
@@ -10357,7 +11192,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10357
11192
|
elapsedMs,
|
|
10358
11193
|
thresholds: config.scoring.thresholds,
|
|
10359
11194
|
verbose: options.verbose,
|
|
10360
|
-
includeHeader: showHeader,
|
|
11195
|
+
includeHeader: !printedHumanHeader && showHeader,
|
|
10361
11196
|
printBrand: options.printBrand
|
|
10362
11197
|
}));
|
|
10363
11198
|
return completion;
|
|
@@ -10368,8 +11203,9 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10368
11203
|
const ciCommand = async (directory, config, options = {}) => {
|
|
10369
11204
|
try {
|
|
10370
11205
|
return await scanCommand(directory, config, {
|
|
10371
|
-
changes:
|
|
10372
|
-
staged:
|
|
11206
|
+
changes: Boolean(options.changes),
|
|
11207
|
+
staged: Boolean(options.staged),
|
|
11208
|
+
base: options.base,
|
|
10373
11209
|
verbose: false,
|
|
10374
11210
|
json: !options.human && !options.sarif,
|
|
10375
11211
|
sarif: options.sarif,
|
|
@@ -10467,7 +11303,7 @@ const buildDoctorRender = (input) => {
|
|
|
10467
11303
|
};
|
|
10468
11304
|
const header = renderHeader({
|
|
10469
11305
|
version: APP_VERSION,
|
|
10470
|
-
command: "
|
|
11306
|
+
command: "Doctor report",
|
|
10471
11307
|
context: [input.projectName, input.languageLabel].filter((s) => s.length > 0),
|
|
10472
11308
|
brand: input.printBrand !== false
|
|
10473
11309
|
}, deps);
|
|
@@ -11018,7 +11854,7 @@ const buildAgentPrompt = (rootDirectory, diagnostics, score) => {
|
|
|
11018
11854
|
}
|
|
11019
11855
|
lines.push("---");
|
|
11020
11856
|
lines.push("Fix each issue following the guidance above. Prioritize errors over warnings.");
|
|
11021
|
-
lines.push("After making changes, run `
|
|
11857
|
+
lines.push("After making changes, run `aislop scan` to verify all issues are resolved and the score improves.");
|
|
11022
11858
|
return lines.join("\n");
|
|
11023
11859
|
};
|
|
11024
11860
|
const SUPPORTED_AGENT_NAMES = Object.keys(AGENT_CONFIGS);
|
|
@@ -11958,7 +12794,7 @@ const runExpoDoctor = async (context) => {
|
|
|
11958
12794
|
rule: "expo-doctor/config-error",
|
|
11959
12795
|
severity: "warning",
|
|
11960
12796
|
message: configError,
|
|
11961
|
-
help: "Install project dependencies, then re-run `
|
|
12797
|
+
help: "Install project dependencies, then re-run `aislop scan`.",
|
|
11962
12798
|
line: 0,
|
|
11963
12799
|
column: 0,
|
|
11964
12800
|
category: "Expo",
|
|
@@ -12458,7 +13294,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12458
13294
|
const projectName = projectInfo.projectName ?? "project";
|
|
12459
13295
|
if (showHeader) process.stdout.write(renderHeader({
|
|
12460
13296
|
version: APP_VERSION,
|
|
12461
|
-
command: "
|
|
13297
|
+
command: "Fix run",
|
|
12462
13298
|
context: [projectName],
|
|
12463
13299
|
brand: options.printBrand !== false
|
|
12464
13300
|
}));
|
|
@@ -12516,10 +13352,11 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12516
13352
|
label: "Verification complete"
|
|
12517
13353
|
});
|
|
12518
13354
|
const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
|
|
12519
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
13355
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
12520
13356
|
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
12521
13357
|
const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
|
|
12522
13358
|
const remaining = errors + warnings;
|
|
13359
|
+
const actionableDiagnostics = allDiagnostics.filter((d) => d.severity !== "info");
|
|
12523
13360
|
if (steps.length === 0) rail.complete({
|
|
12524
13361
|
status: "skipped",
|
|
12525
13362
|
label: "No applicable auto-fixers found"
|
|
@@ -12537,7 +13374,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12537
13374
|
language,
|
|
12538
13375
|
fileCount: projectInfo.sourceFileCount,
|
|
12539
13376
|
results: scanResults,
|
|
12540
|
-
diagnostics:
|
|
13377
|
+
diagnostics: actionableDiagnostics,
|
|
12541
13378
|
score: scoreResult,
|
|
12542
13379
|
elapsedMs: performance.now() - startTime,
|
|
12543
13380
|
thresholds: config.scoring.thresholds,
|
|
@@ -12547,7 +13384,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12547
13384
|
}));
|
|
12548
13385
|
}
|
|
12549
13386
|
if (options.agent) {
|
|
12550
|
-
launchAgent(options.agent, resolvedDir,
|
|
13387
|
+
launchAgent(options.agent, resolvedDir, actionableDiagnostics, scoreResult.score);
|
|
12551
13388
|
return {
|
|
12552
13389
|
exitCode: 0,
|
|
12553
13390
|
score: scoreResult.score,
|
|
@@ -12556,7 +13393,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12556
13393
|
};
|
|
12557
13394
|
}
|
|
12558
13395
|
if (options.prompt) {
|
|
12559
|
-
printPrompt(resolvedDir,
|
|
13396
|
+
printPrompt(resolvedDir, actionableDiagnostics, scoreResult.score);
|
|
12560
13397
|
return {
|
|
12561
13398
|
exitCode: 0,
|
|
12562
13399
|
score: scoreResult.score,
|
|
@@ -12584,7 +13421,7 @@ const buildInitSuccessRender = (input) => {
|
|
|
12584
13421
|
};
|
|
12585
13422
|
const header = input.includeHeader === false ? "" : renderHeader({
|
|
12586
13423
|
version: APP_VERSION,
|
|
12587
|
-
command: "
|
|
13424
|
+
command: "Setup",
|
|
12588
13425
|
context: [],
|
|
12589
13426
|
brand: input.printBrand !== false
|
|
12590
13427
|
}, deps);
|
|
@@ -12719,7 +13556,8 @@ const writeAislopConfig = (configDir, configPath, choices) => {
|
|
|
12719
13556
|
scoring: {
|
|
12720
13557
|
weights: { ...DEFAULT_CONFIG.scoring.weights },
|
|
12721
13558
|
thresholds: { ...DEFAULT_CONFIG.scoring.thresholds },
|
|
12722
|
-
smoothing: DEFAULT_CONFIG.scoring.smoothing
|
|
13559
|
+
smoothing: DEFAULT_CONFIG.scoring.smoothing,
|
|
13560
|
+
maxPerRule: DEFAULT_CONFIG.scoring.maxPerRule
|
|
12723
13561
|
},
|
|
12724
13562
|
ci: {
|
|
12725
13563
|
failBelow: choices.failBelow,
|
|
@@ -12735,7 +13573,7 @@ const initCommand = async (directory, options = {}) => {
|
|
|
12735
13573
|
const printBrand = options.printBrand !== false;
|
|
12736
13574
|
process.stdout.write(renderHeader({
|
|
12737
13575
|
version: APP_VERSION,
|
|
12738
|
-
command: "
|
|
13576
|
+
command: "Setup",
|
|
12739
13577
|
context: [],
|
|
12740
13578
|
brand: printBrand
|
|
12741
13579
|
}));
|
|
@@ -12800,13 +13638,437 @@ const initCommand = async (directory, options = {}) => {
|
|
|
12800
13638
|
}));
|
|
12801
13639
|
};
|
|
12802
13640
|
|
|
13641
|
+
//#endregion
|
|
13642
|
+
//#region src/ui/action-frame.ts
|
|
13643
|
+
const renderActionStart = (input) => {
|
|
13644
|
+
const hint = input.hint ? ` ${style(theme, "muted", `· ${input.hint}`)}` : "";
|
|
13645
|
+
return `\n ${style(theme, "muted", "┌")} ${style(theme, "accent", input.label)}${hint}\n\n`;
|
|
13646
|
+
};
|
|
13647
|
+
const renderActionEnd = (input) => {
|
|
13648
|
+
const status = input.status ?? "complete";
|
|
13649
|
+
const token = status === "complete" ? "success" : "muted";
|
|
13650
|
+
const text = status === "complete" ? `${input.label} complete` : `${input.label} skipped`;
|
|
13651
|
+
return `\n ${style(theme, "muted", "└")} ${style(theme, token, text)}\n`;
|
|
13652
|
+
};
|
|
13653
|
+
|
|
13654
|
+
//#endregion
|
|
13655
|
+
//#region src/ui/home.ts
|
|
13656
|
+
const HOME_COMMANDS = [
|
|
13657
|
+
{
|
|
13658
|
+
command: "aislop scan",
|
|
13659
|
+
summary: "Score this project and show findings",
|
|
13660
|
+
group: "Run"
|
|
13661
|
+
},
|
|
13662
|
+
{
|
|
13663
|
+
command: "aislop fix",
|
|
13664
|
+
summary: "Auto-fix safe issues or hand off to an agent",
|
|
13665
|
+
group: "Run"
|
|
13666
|
+
},
|
|
13667
|
+
{
|
|
13668
|
+
command: "aislop ci",
|
|
13669
|
+
summary: "Run the quality gate for CI",
|
|
13670
|
+
group: "Run"
|
|
13671
|
+
},
|
|
13672
|
+
{
|
|
13673
|
+
command: "aislop doctor",
|
|
13674
|
+
summary: "Check which engines can run here",
|
|
13675
|
+
group: "Run"
|
|
13676
|
+
},
|
|
13677
|
+
{
|
|
13678
|
+
command: "aislop init",
|
|
13679
|
+
summary: "Create config and optional CI workflow",
|
|
13680
|
+
group: "Setup"
|
|
13681
|
+
},
|
|
13682
|
+
{
|
|
13683
|
+
command: "aislop hook install",
|
|
13684
|
+
summary: "Run aislop after coding-agent edits",
|
|
13685
|
+
group: "Setup"
|
|
13686
|
+
},
|
|
13687
|
+
{
|
|
13688
|
+
command: "aislop rules",
|
|
13689
|
+
summary: "Explain every rule and fix mode",
|
|
13690
|
+
group: "Learn"
|
|
13691
|
+
},
|
|
13692
|
+
{
|
|
13693
|
+
command: "aislop trend",
|
|
13694
|
+
summary: "Show local score history",
|
|
13695
|
+
group: "Learn"
|
|
13696
|
+
},
|
|
13697
|
+
{
|
|
13698
|
+
command: "aislop badge",
|
|
13699
|
+
summary: "Print a score badge URL and README markdown",
|
|
13700
|
+
group: "Learn"
|
|
13701
|
+
},
|
|
13702
|
+
{
|
|
13703
|
+
command: "aislop commands",
|
|
13704
|
+
summary: "List all commands and major flags",
|
|
13705
|
+
group: "Utility"
|
|
13706
|
+
},
|
|
13707
|
+
{
|
|
13708
|
+
command: "aislop update",
|
|
13709
|
+
summary: "Check the latest npm version",
|
|
13710
|
+
group: "Learn"
|
|
13711
|
+
},
|
|
13712
|
+
{
|
|
13713
|
+
command: "aislop version",
|
|
13714
|
+
summary: "Print the installed version",
|
|
13715
|
+
group: "Utility"
|
|
13716
|
+
}
|
|
13717
|
+
];
|
|
13718
|
+
const GROUPS = [
|
|
13719
|
+
"Run",
|
|
13720
|
+
"Setup",
|
|
13721
|
+
"Learn",
|
|
13722
|
+
"Utility"
|
|
13723
|
+
];
|
|
13724
|
+
const COMMAND_REFERENCE = [
|
|
13725
|
+
{
|
|
13726
|
+
command: "aislop",
|
|
13727
|
+
summary: "Open the interactive menu, or scan the current directory in non-TTY shells"
|
|
13728
|
+
},
|
|
13729
|
+
{
|
|
13730
|
+
command: "aislop scan [directory]",
|
|
13731
|
+
summary: "Score code quality and show findings",
|
|
13732
|
+
flags: [
|
|
13733
|
+
"--changes",
|
|
13734
|
+
"--staged",
|
|
13735
|
+
"-d, --verbose",
|
|
13736
|
+
"--json",
|
|
13737
|
+
"--sarif",
|
|
13738
|
+
"--format <format>",
|
|
13739
|
+
"--include <patterns>",
|
|
13740
|
+
"--exclude <patterns>"
|
|
13741
|
+
]
|
|
13742
|
+
},
|
|
13743
|
+
{
|
|
13744
|
+
command: "aislop fix [directory]",
|
|
13745
|
+
summary: "Apply safe auto-fixes or hand remaining findings to an agent",
|
|
13746
|
+
flags: [
|
|
13747
|
+
"-d, --verbose",
|
|
13748
|
+
"-f, --force",
|
|
13749
|
+
"--safe",
|
|
13750
|
+
"-p, --prompt",
|
|
13751
|
+
"--claude",
|
|
13752
|
+
"--codex",
|
|
13753
|
+
"--cursor",
|
|
13754
|
+
"--windsurf",
|
|
13755
|
+
"--vscode",
|
|
13756
|
+
"--amp",
|
|
13757
|
+
"--antigravity",
|
|
13758
|
+
"--deep-agents",
|
|
13759
|
+
"--gemini",
|
|
13760
|
+
"--kimi",
|
|
13761
|
+
"--opencode",
|
|
13762
|
+
"--warp",
|
|
13763
|
+
"--aider",
|
|
13764
|
+
"--goose",
|
|
13765
|
+
"--pi",
|
|
13766
|
+
"--crush"
|
|
13767
|
+
]
|
|
13768
|
+
},
|
|
13769
|
+
{
|
|
13770
|
+
command: "aislop ci [directory]",
|
|
13771
|
+
summary: "Run the CI quality gate with thresholded exit codes",
|
|
13772
|
+
flags: [
|
|
13773
|
+
"--human",
|
|
13774
|
+
"--sarif",
|
|
13775
|
+
"--format <format>"
|
|
13776
|
+
]
|
|
13777
|
+
},
|
|
13778
|
+
{
|
|
13779
|
+
command: "aislop init [directory]",
|
|
13780
|
+
summary: "Create .aislop/config.yml, .aislop/rules.yml, and optional GitHub Actions workflow",
|
|
13781
|
+
flags: ["--strict"]
|
|
13782
|
+
},
|
|
13783
|
+
{
|
|
13784
|
+
command: "aislop doctor [directory]",
|
|
13785
|
+
summary: "Check installed engines and project coverage"
|
|
13786
|
+
},
|
|
13787
|
+
{
|
|
13788
|
+
command: "aislop rules [directory]",
|
|
13789
|
+
summary: "Explain rule IDs, severity, fixability, and meaning",
|
|
13790
|
+
flags: ["--search"]
|
|
13791
|
+
},
|
|
13792
|
+
{
|
|
13793
|
+
command: "aislop hook install [agents...]",
|
|
13794
|
+
summary: "Install coding-agent hooks",
|
|
13795
|
+
flags: [
|
|
13796
|
+
"--agent <names>",
|
|
13797
|
+
"-g, --global",
|
|
13798
|
+
"--project",
|
|
13799
|
+
"--dry-run",
|
|
13800
|
+
"--yes",
|
|
13801
|
+
"--quality-gate",
|
|
13802
|
+
"--claude",
|
|
13803
|
+
"--cursor",
|
|
13804
|
+
"--gemini",
|
|
13805
|
+
"--pi",
|
|
13806
|
+
"--codex",
|
|
13807
|
+
"--windsurf",
|
|
13808
|
+
"--cline",
|
|
13809
|
+
"--kilocode",
|
|
13810
|
+
"--antigravity",
|
|
13811
|
+
"--copilot"
|
|
13812
|
+
]
|
|
13813
|
+
},
|
|
13814
|
+
{
|
|
13815
|
+
command: "aislop hook uninstall [agents...]",
|
|
13816
|
+
summary: "Remove installed coding-agent hooks",
|
|
13817
|
+
flags: [
|
|
13818
|
+
"--agent <names>",
|
|
13819
|
+
"-g, --global",
|
|
13820
|
+
"--project",
|
|
13821
|
+
"--dry-run",
|
|
13822
|
+
"--claude",
|
|
13823
|
+
"--cursor",
|
|
13824
|
+
"--gemini",
|
|
13825
|
+
"--pi",
|
|
13826
|
+
"--codex",
|
|
13827
|
+
"--windsurf",
|
|
13828
|
+
"--cline",
|
|
13829
|
+
"--kilocode",
|
|
13830
|
+
"--antigravity",
|
|
13831
|
+
"--copilot"
|
|
13832
|
+
]
|
|
13833
|
+
},
|
|
13834
|
+
{
|
|
13835
|
+
command: "aislop hooks",
|
|
13836
|
+
summary: "Alias for hook"
|
|
13837
|
+
},
|
|
13838
|
+
{
|
|
13839
|
+
command: "aislop hook status",
|
|
13840
|
+
summary: "Show installed hook status"
|
|
13841
|
+
},
|
|
13842
|
+
{
|
|
13843
|
+
command: "aislop hook baseline",
|
|
13844
|
+
summary: "Capture the current score as the hook baseline"
|
|
13845
|
+
},
|
|
13846
|
+
{
|
|
13847
|
+
command: "aislop install [agents...]",
|
|
13848
|
+
summary: "Alias for hook install",
|
|
13849
|
+
flags: [
|
|
13850
|
+
"--agent <names>",
|
|
13851
|
+
"-g, --global",
|
|
13852
|
+
"--project",
|
|
13853
|
+
"--dry-run",
|
|
13854
|
+
"--yes",
|
|
13855
|
+
"--quality-gate",
|
|
13856
|
+
"--claude",
|
|
13857
|
+
"--cursor",
|
|
13858
|
+
"--gemini",
|
|
13859
|
+
"--pi",
|
|
13860
|
+
"--codex",
|
|
13861
|
+
"--windsurf",
|
|
13862
|
+
"--cline",
|
|
13863
|
+
"--kilocode",
|
|
13864
|
+
"--antigravity",
|
|
13865
|
+
"--copilot"
|
|
13866
|
+
]
|
|
13867
|
+
},
|
|
13868
|
+
{
|
|
13869
|
+
command: "aislop install hooks [agents...]",
|
|
13870
|
+
summary: "Natural alias for install; same flags"
|
|
13871
|
+
},
|
|
13872
|
+
{
|
|
13873
|
+
command: "aislop uninstall [agents...]",
|
|
13874
|
+
summary: "Alias for hook uninstall",
|
|
13875
|
+
flags: [
|
|
13876
|
+
"--agent <names>",
|
|
13877
|
+
"-g, --global",
|
|
13878
|
+
"--project",
|
|
13879
|
+
"--dry-run",
|
|
13880
|
+
"--claude",
|
|
13881
|
+
"--cursor",
|
|
13882
|
+
"--gemini",
|
|
13883
|
+
"--pi",
|
|
13884
|
+
"--codex",
|
|
13885
|
+
"--windsurf",
|
|
13886
|
+
"--cline",
|
|
13887
|
+
"--kilocode",
|
|
13888
|
+
"--antigravity",
|
|
13889
|
+
"--copilot"
|
|
13890
|
+
]
|
|
13891
|
+
},
|
|
13892
|
+
{
|
|
13893
|
+
command: "aislop uninstall hooks [agents...]",
|
|
13894
|
+
summary: "Natural alias for uninstall; same flags"
|
|
13895
|
+
},
|
|
13896
|
+
{
|
|
13897
|
+
command: "aislop badge [directory]",
|
|
13898
|
+
summary: "Print score badge URL and README markdown",
|
|
13899
|
+
flags: [
|
|
13900
|
+
"--owner <owner>",
|
|
13901
|
+
"--repo <repo>",
|
|
13902
|
+
"--json"
|
|
13903
|
+
]
|
|
13904
|
+
},
|
|
13905
|
+
{
|
|
13906
|
+
command: "aislop trend [directory]",
|
|
13907
|
+
summary: "Show recent local scores from .aislop/history.jsonl",
|
|
13908
|
+
flags: ["--limit <n>"]
|
|
13909
|
+
},
|
|
13910
|
+
{
|
|
13911
|
+
command: "aislop update",
|
|
13912
|
+
summary: "Show current and latest npm versions"
|
|
13913
|
+
},
|
|
13914
|
+
{
|
|
13915
|
+
command: "aislop upgrade",
|
|
13916
|
+
summary: "Alias for update"
|
|
13917
|
+
},
|
|
13918
|
+
{
|
|
13919
|
+
command: "aislop version",
|
|
13920
|
+
summary: "Print the installed version"
|
|
13921
|
+
},
|
|
13922
|
+
{
|
|
13923
|
+
command: "aislop commands",
|
|
13924
|
+
summary: "Show this command reference"
|
|
13925
|
+
}
|
|
13926
|
+
];
|
|
13927
|
+
const renderCommandGroups = () => {
|
|
13928
|
+
const commandWidth = Math.max(...HOME_COMMANDS.map((c) => c.command.length));
|
|
13929
|
+
const lines = [];
|
|
13930
|
+
for (const group of GROUPS) {
|
|
13931
|
+
lines.push(` ${style(theme, "dim", group)}`);
|
|
13932
|
+
for (const item of HOME_COMMANDS.filter((c) => c.group === group)) lines.push(` ${style(theme, "muted", "$")} ${style(theme, "fg", padEnd(item.command, commandWidth))} ${style(theme, "muted", item.summary)}`);
|
|
13933
|
+
lines.push("");
|
|
13934
|
+
}
|
|
13935
|
+
return lines.join("\n");
|
|
13936
|
+
};
|
|
13937
|
+
const renderHelpDetails = () => [
|
|
13938
|
+
` ${style(theme, "dim", "Usage")}`,
|
|
13939
|
+
" aislop Open interactive menu",
|
|
13940
|
+
" aislop scan [options] [directory]",
|
|
13941
|
+
" aislop fix [options] [directory]",
|
|
13942
|
+
" aislop ci [options] [directory]",
|
|
13943
|
+
" aislop init [options] [directory]",
|
|
13944
|
+
" aislop doctor [directory]",
|
|
13945
|
+
" aislop rules [directory]",
|
|
13946
|
+
" aislop badge [options] [directory]",
|
|
13947
|
+
" aislop trend [options] [directory]",
|
|
13948
|
+
" aislop hook install [agents...]",
|
|
13949
|
+
" aislop install hooks [agents...]",
|
|
13950
|
+
" aislop update",
|
|
13951
|
+
" aislop version",
|
|
13952
|
+
"",
|
|
13953
|
+
` ${style(theme, "dim", "Scan flags")}`,
|
|
13954
|
+
" --changes scan changed files from HEAD",
|
|
13955
|
+
" --staged scan staged files",
|
|
13956
|
+
" --json emit machine-readable JSON",
|
|
13957
|
+
" --sarif emit SARIF 2.1.0",
|
|
13958
|
+
" --format choose json or sarif",
|
|
13959
|
+
" --exclude exclude comma-separated or repeated paths",
|
|
13960
|
+
" --include include comma-separated or repeated paths",
|
|
13961
|
+
"",
|
|
13962
|
+
` ${style(theme, "dim", "Fix flags")}`,
|
|
13963
|
+
" --safe only reversible fixes",
|
|
13964
|
+
" --force aggressive dependency and framework fixes",
|
|
13965
|
+
" --prompt print an agent handoff prompt",
|
|
13966
|
+
" --codex open Codex to fix remaining findings",
|
|
13967
|
+
" --claude open Claude Code to fix remaining findings",
|
|
13968
|
+
"",
|
|
13969
|
+
` ${style(theme, "dim", "Ignore and scope")}`,
|
|
13970
|
+
" .aislopignore skip generated, vendored, or noisy paths",
|
|
13971
|
+
" .gitignore respected for untracked files",
|
|
13972
|
+
" --exclude skip extra paths for this run",
|
|
13973
|
+
" --include scan only matching paths for this run",
|
|
13974
|
+
"",
|
|
13975
|
+
` ${style(theme, "dim", "More")}`,
|
|
13976
|
+
" aislop commands show every command and major flag",
|
|
13977
|
+
" aislop <cmd> --help show detailed help for one command",
|
|
13978
|
+
" -h, --help show help",
|
|
13979
|
+
" -v, -V, --version show version",
|
|
13980
|
+
"",
|
|
13981
|
+
` ${style(theme, "dim", "One-off latest run")}`,
|
|
13982
|
+
" npx aislop@latest scan",
|
|
13983
|
+
"",
|
|
13984
|
+
` ${style(theme, "dim", "Examples")}`,
|
|
13985
|
+
" aislop scan --changes",
|
|
13986
|
+
" aislop fix --codex",
|
|
13987
|
+
" aislop hook install --claude",
|
|
13988
|
+
" aislop install hooks",
|
|
13989
|
+
" aislop rules --search",
|
|
13990
|
+
""
|
|
13991
|
+
].join("\n");
|
|
13992
|
+
const renderHome = (input = {}) => {
|
|
13993
|
+
let out = renderHeader({
|
|
13994
|
+
version: input.version ?? APP_VERSION,
|
|
13995
|
+
command: "--bare",
|
|
13996
|
+
context: []
|
|
13997
|
+
});
|
|
13998
|
+
out += `${renderCommandGroups().trimEnd()}\n`;
|
|
13999
|
+
if (input.includeHelpDetails) {
|
|
14000
|
+
out += `\n${renderHelpDetails().trimEnd()}\n`;
|
|
14001
|
+
out += renderHintLine("Run aislop scan to scan your project");
|
|
14002
|
+
}
|
|
14003
|
+
return out;
|
|
14004
|
+
};
|
|
14005
|
+
const renderRootHelp = (input = {}) => `${renderHome({
|
|
14006
|
+
version: input.version,
|
|
14007
|
+
includeHelpDetails: true
|
|
14008
|
+
})}\n`;
|
|
14009
|
+
const renderCommandReference = (input = {}) => {
|
|
14010
|
+
const version = input.version ?? APP_VERSION;
|
|
14011
|
+
const commandWidth = Math.max(...COMMAND_REFERENCE.map((c) => c.command.length));
|
|
14012
|
+
const lines = [renderHeader({
|
|
14013
|
+
version,
|
|
14014
|
+
command: "Commands",
|
|
14015
|
+
context: ["full list"]
|
|
14016
|
+
}).trimEnd(), ""];
|
|
14017
|
+
for (const item of COMMAND_REFERENCE) {
|
|
14018
|
+
lines.push(` ${style(theme, "fg", padEnd(item.command, commandWidth))} ${style(theme, "muted", item.summary)}`);
|
|
14019
|
+
if (item.flags?.length) lines.push(` ${style(theme, "dim", item.flags.join(" "))}`);
|
|
14020
|
+
}
|
|
14021
|
+
lines.push("", ` ${style(theme, "dim", "Scope files")}`, " .aislopignore Skip generated, vendored, or noisy paths", " .gitignore Respected for untracked files");
|
|
14022
|
+
lines.push("", renderHintLine("Run aislop <command> --help for complete command-specific options").trimEnd());
|
|
14023
|
+
return `${lines.join("\n")}\n`;
|
|
14024
|
+
};
|
|
14025
|
+
|
|
12803
14026
|
//#endregion
|
|
12804
14027
|
//#region src/commands/rules.ts
|
|
14028
|
+
const ENGINE_PRESENTATION = {
|
|
14029
|
+
"ai-slop": {
|
|
14030
|
+
label: "AI Slop",
|
|
14031
|
+
summary: "Generated-code leftovers: vague comments, unsafe casts, stubs, swallowed errors.",
|
|
14032
|
+
order: 10
|
|
14033
|
+
},
|
|
14034
|
+
security: {
|
|
14035
|
+
label: "Security",
|
|
14036
|
+
summary: "Secrets, injection, XSS, shell execution, and vulnerable dependencies.",
|
|
14037
|
+
order: 20
|
|
14038
|
+
},
|
|
14039
|
+
"code-quality": {
|
|
14040
|
+
label: "Code Quality",
|
|
14041
|
+
summary: "Dead code, duplicate code, complexity, and dependency hygiene.",
|
|
14042
|
+
order: 30
|
|
14043
|
+
},
|
|
14044
|
+
format: {
|
|
14045
|
+
label: "Format",
|
|
14046
|
+
summary: "Formatter and import-order checks that aislop can usually fix.",
|
|
14047
|
+
order: 40
|
|
14048
|
+
},
|
|
14049
|
+
lint: {
|
|
14050
|
+
label: "Lint",
|
|
14051
|
+
summary: "Language linter and compiler findings from bundled or system tools.",
|
|
14052
|
+
order: 50
|
|
14053
|
+
},
|
|
14054
|
+
architecture: {
|
|
14055
|
+
label: "Architecture",
|
|
14056
|
+
summary: "Project-specific import and layering rules from .aislop/rules.yml.",
|
|
14057
|
+
order: 60
|
|
14058
|
+
}
|
|
14059
|
+
};
|
|
14060
|
+
const presentationFor = (engine) => ENGINE_PRESENTATION[engine] ?? {
|
|
14061
|
+
label: engine,
|
|
14062
|
+
summary: "Project-specific rules.",
|
|
14063
|
+
order: 100
|
|
14064
|
+
};
|
|
14065
|
+
const severityLabel = (severity) => severity === "warning" ? "warn" : severity;
|
|
14066
|
+
const fixModeLabel = (fixable) => fixable ? "auto" : "review";
|
|
12805
14067
|
const buildRulesRender = (input) => {
|
|
12806
|
-
const header = renderHeader({
|
|
14068
|
+
const header = input.includeHeader === false ? "" : renderHeader({
|
|
12807
14069
|
version: APP_VERSION,
|
|
12808
|
-
command: "
|
|
12809
|
-
context: [],
|
|
14070
|
+
command: "Rules catalog",
|
|
14071
|
+
context: [`${input.rules.length} checks`],
|
|
12810
14072
|
brand: input.printBrand !== false
|
|
12811
14073
|
});
|
|
12812
14074
|
const byEngine = /* @__PURE__ */ new Map();
|
|
@@ -12815,23 +14077,50 @@ const buildRulesRender = (input) => {
|
|
|
12815
14077
|
list.push(r);
|
|
12816
14078
|
byEngine.set(r.engine, list);
|
|
12817
14079
|
}
|
|
12818
|
-
const engines = [...byEngine.keys()].sort()
|
|
14080
|
+
const engines = [...byEngine.keys()].sort((a, b) => {
|
|
14081
|
+
const pa = presentationFor(a);
|
|
14082
|
+
const pb = presentationFor(b);
|
|
14083
|
+
if (pa.order !== pb.order) return pa.order - pb.order;
|
|
14084
|
+
return pa.label.localeCompare(pb.label);
|
|
14085
|
+
});
|
|
12819
14086
|
const idWidth = Math.max(20, ...input.rules.map((r) => r.id.length));
|
|
12820
|
-
const lines = [];
|
|
14087
|
+
const lines = [` ${style(theme, "muted", "auto = aislop fix can change it; review = inspect and fix with a developer or agent.")}`, ""];
|
|
12821
14088
|
for (const engine of engines) {
|
|
12822
|
-
|
|
14089
|
+
const presentation = presentationFor(engine);
|
|
14090
|
+
lines.push(` ${style(theme, "accent", presentation.label)}`);
|
|
14091
|
+
lines.push(` ${style(theme, "muted", presentation.summary)}`);
|
|
14092
|
+
lines.push(` ${style(theme, "dim", padEnd("Rule ID", idWidth))} ${style(theme, "dim", "Sev")} ${style(theme, "dim", "Fix")} ${style(theme, "dim", "Meaning")}`);
|
|
12823
14093
|
const rules = (byEngine.get(engine) ?? []).sort((a, b) => a.id.localeCompare(b.id));
|
|
12824
14094
|
for (const r of rules) {
|
|
12825
|
-
const
|
|
12826
|
-
const
|
|
12827
|
-
|
|
14095
|
+
const severityText = severityLabel(r.severity);
|
|
14096
|
+
const severity = style(theme, r.severity === "error" ? "danger" : "warn", padEnd(severityText, 5));
|
|
14097
|
+
const fixable = r.fixable ? style(theme, "accent", padEnd("auto", 6)) : style(theme, "muted", padEnd("review", 6));
|
|
14098
|
+
lines.push(` ${padEnd(r.id, idWidth)} ${severity} ${fixable} ${descriptionForRule(r.id)}`);
|
|
12828
14099
|
}
|
|
12829
14100
|
lines.push("");
|
|
12830
14101
|
}
|
|
12831
14102
|
const invocation = input.invocation ?? detectInvocation();
|
|
12832
|
-
const tail = renderHintLine(`Run ${invocation} scan to
|
|
14103
|
+
const tail = renderHintLine(`Run ${invocation} scan to check your project against these rules`) + renderHintLine(`Run ${invocation} init to choose engines and CI settings`);
|
|
12833
14104
|
return `${header}${lines.join("\n")}\n${tail}`;
|
|
12834
14105
|
};
|
|
14106
|
+
const buildRuleDetailRender = (rule, input = {}) => {
|
|
14107
|
+
const presentation = presentationFor(rule.engine);
|
|
14108
|
+
const header = input.includeHeader === false ? "" : renderHeader({
|
|
14109
|
+
version: APP_VERSION,
|
|
14110
|
+
command: "Rule detail",
|
|
14111
|
+
context: [presentation.label],
|
|
14112
|
+
brand: input.printBrand !== false
|
|
14113
|
+
});
|
|
14114
|
+
const rows = [
|
|
14115
|
+
["Rule", rule.id],
|
|
14116
|
+
["Engine", `${presentation.label} — ${presentation.summary}`],
|
|
14117
|
+
["Severity", severityLabel(rule.severity)],
|
|
14118
|
+
["Fix", `${fixModeLabel(rule.fixable)}${rule.fixable ? " (aislop fix can change it)" : " (review and fix intentionally)"}`],
|
|
14119
|
+
["Meaning", descriptionForRule(rule.id)]
|
|
14120
|
+
];
|
|
14121
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
14122
|
+
return `${header}${rows.map(([label, value]) => ` ${style(theme, "muted", padEnd(label, labelWidth))} ${style(theme, label === "Severity" && rule.severity === "error" ? "danger" : "fg", value)}`).join("\n")}\n\n${renderHintLine(rule.fixable ? "Run aislop fix to apply the automatic fix" : "Use the meaning above to fix or review the finding")}`;
|
|
14123
|
+
};
|
|
12835
14124
|
const AI_SLOP_FIXABLE = new Set([
|
|
12836
14125
|
"ai-slop/trivial-comment",
|
|
12837
14126
|
"ai-slop/unused-import",
|
|
@@ -12964,7 +14253,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
12964
14253
|
fixable: false
|
|
12965
14254
|
};
|
|
12966
14255
|
};
|
|
12967
|
-
const
|
|
14256
|
+
const collectRuleEntries = (directory) => {
|
|
12968
14257
|
const resolvedDir = path.resolve(directory);
|
|
12969
14258
|
const entries = [];
|
|
12970
14259
|
for (const { engine, rules } of BUILTIN_RULES) for (const rule of rules) entries.push(toRuleEntry(engine, rule));
|
|
@@ -12978,6 +14267,41 @@ const rulesCommand = async (directory, options = {}) => {
|
|
|
12978
14267
|
fixable: false
|
|
12979
14268
|
});
|
|
12980
14269
|
}
|
|
14270
|
+
return entries;
|
|
14271
|
+
};
|
|
14272
|
+
const runRulesExplorer = async (entries, options) => {
|
|
14273
|
+
const selected = await searchSelect({
|
|
14274
|
+
message: "Search rules",
|
|
14275
|
+
items: entries.map((rule) => {
|
|
14276
|
+
const presentation = presentationFor(rule.engine);
|
|
14277
|
+
return {
|
|
14278
|
+
value: rule,
|
|
14279
|
+
label: rule.id,
|
|
14280
|
+
hint: `${presentation.label} · ${severityLabel(rule.severity)} · ${descriptionForRule(rule.id)}`,
|
|
14281
|
+
keywords: [
|
|
14282
|
+
presentation.label,
|
|
14283
|
+
rule.engine,
|
|
14284
|
+
rule.severity,
|
|
14285
|
+
fixModeLabel(rule.fixable),
|
|
14286
|
+
descriptionForRule(rule.id)
|
|
14287
|
+
]
|
|
14288
|
+
};
|
|
14289
|
+
}),
|
|
14290
|
+
maxVisible: 10,
|
|
14291
|
+
required: true
|
|
14292
|
+
});
|
|
14293
|
+
if (selected === null) return;
|
|
14294
|
+
process.stdout.write(`${buildRuleDetailRender(selected, {
|
|
14295
|
+
printBrand: options.printBrand,
|
|
14296
|
+
includeHeader: true
|
|
14297
|
+
})}\n`);
|
|
14298
|
+
};
|
|
14299
|
+
const rulesCommand = async (directory, options = {}) => {
|
|
14300
|
+
const entries = collectRuleEntries(directory);
|
|
14301
|
+
if (options.interactive && process.stdin.isTTY && process.stdout.isTTY) {
|
|
14302
|
+
await runRulesExplorer(entries, options);
|
|
14303
|
+
return;
|
|
14304
|
+
}
|
|
12981
14305
|
process.stdout.write(`${buildRulesRender({
|
|
12982
14306
|
rules: entries,
|
|
12983
14307
|
invocation: detectInvocation(),
|
|
@@ -12991,27 +14315,37 @@ const INTERACTIVE_OPTIONS = [
|
|
|
12991
14315
|
{
|
|
12992
14316
|
value: "scan",
|
|
12993
14317
|
label: "Scan",
|
|
12994
|
-
hint: "
|
|
14318
|
+
hint: "Score project and show findings"
|
|
12995
14319
|
},
|
|
12996
14320
|
{
|
|
12997
14321
|
value: "fix",
|
|
12998
14322
|
label: "Fix",
|
|
12999
|
-
hint: "
|
|
13000
|
-
},
|
|
13001
|
-
{
|
|
13002
|
-
value: "init",
|
|
13003
|
-
label: "Init",
|
|
13004
|
-
hint: "Create aislop config"
|
|
14323
|
+
hint: "Auto-fix or hand off remaining findings"
|
|
13005
14324
|
},
|
|
13006
14325
|
{
|
|
13007
14326
|
value: "doctor",
|
|
13008
14327
|
label: "Doctor",
|
|
13009
|
-
hint: "Check
|
|
14328
|
+
hint: "Check required tools"
|
|
14329
|
+
},
|
|
14330
|
+
{
|
|
14331
|
+
value: "init",
|
|
14332
|
+
label: "Setup",
|
|
14333
|
+
hint: "Create config and CI workflow"
|
|
13010
14334
|
},
|
|
13011
14335
|
{
|
|
13012
14336
|
value: "rules",
|
|
13013
14337
|
label: "Rules",
|
|
13014
|
-
hint: "
|
|
14338
|
+
hint: "Explain every check"
|
|
14339
|
+
},
|
|
14340
|
+
{
|
|
14341
|
+
value: "hook-install",
|
|
14342
|
+
label: "Install hooks",
|
|
14343
|
+
hint: "Run aislop after agent edits"
|
|
14344
|
+
},
|
|
14345
|
+
{
|
|
14346
|
+
value: "hook-status",
|
|
14347
|
+
label: "Hook status",
|
|
14348
|
+
hint: "Show installed hooks"
|
|
13015
14349
|
},
|
|
13016
14350
|
{
|
|
13017
14351
|
value: "quit",
|
|
@@ -13019,6 +14353,7 @@ const INTERACTIVE_OPTIONS = [
|
|
|
13019
14353
|
hint: "Exit"
|
|
13020
14354
|
}
|
|
13021
14355
|
];
|
|
14356
|
+
const optionFor = (action) => INTERACTIVE_OPTIONS.find((option) => option.value === action);
|
|
13022
14357
|
const run = async (action, directory, config) => {
|
|
13023
14358
|
switch (action) {
|
|
13024
14359
|
case "scan":
|
|
@@ -13029,52 +14364,79 @@ const run = async (action, directory, config) => {
|
|
|
13029
14364
|
json: false,
|
|
13030
14365
|
printBrand: false
|
|
13031
14366
|
});
|
|
13032
|
-
return;
|
|
14367
|
+
return "complete";
|
|
13033
14368
|
case "fix":
|
|
13034
14369
|
await fixCommand(directory, config, {
|
|
13035
14370
|
verbose: false,
|
|
13036
14371
|
printBrand: false
|
|
13037
14372
|
});
|
|
13038
|
-
return;
|
|
14373
|
+
return "complete";
|
|
14374
|
+
case "hook-install": {
|
|
14375
|
+
const agents = await promptAgentSelection("install");
|
|
14376
|
+
if (agents === null || agents.length === 0) return "skipped";
|
|
14377
|
+
await hookInstall({
|
|
14378
|
+
agents,
|
|
14379
|
+
scope: "global",
|
|
14380
|
+
dryRun: false,
|
|
14381
|
+
yes: false,
|
|
14382
|
+
qualityGate: false
|
|
14383
|
+
});
|
|
14384
|
+
return "complete";
|
|
14385
|
+
}
|
|
14386
|
+
case "hook-status":
|
|
14387
|
+
await hookStatus();
|
|
14388
|
+
return "complete";
|
|
13039
14389
|
case "init":
|
|
13040
14390
|
await initCommand(directory, { printBrand: false });
|
|
13041
|
-
return;
|
|
14391
|
+
return "complete";
|
|
13042
14392
|
case "doctor":
|
|
13043
14393
|
await doctorCommand(directory, { printBrand: false });
|
|
13044
|
-
return;
|
|
14394
|
+
return "complete";
|
|
13045
14395
|
case "rules":
|
|
13046
|
-
await rulesCommand(directory, {
|
|
13047
|
-
|
|
13048
|
-
|
|
14396
|
+
await rulesCommand(directory, {
|
|
14397
|
+
printBrand: false,
|
|
14398
|
+
interactive: true
|
|
14399
|
+
});
|
|
14400
|
+
return "complete";
|
|
14401
|
+
case "quit": return "skipped";
|
|
13049
14402
|
}
|
|
13050
14403
|
};
|
|
13051
|
-
const
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
14404
|
+
const runFramed = async (action, directory, config) => {
|
|
14405
|
+
const option = optionFor(action);
|
|
14406
|
+
const label = option?.label ?? action;
|
|
14407
|
+
process.stdout.write(renderActionStart({
|
|
14408
|
+
label,
|
|
14409
|
+
hint: option?.hint
|
|
14410
|
+
}));
|
|
14411
|
+
const status = await run(action, directory, config);
|
|
14412
|
+
process.stdout.write(renderActionEnd({
|
|
14413
|
+
label,
|
|
14414
|
+
status
|
|
13056
14415
|
}));
|
|
13057
|
-
|
|
14416
|
+
};
|
|
14417
|
+
const interactiveCommand = async (directory, config) => {
|
|
14418
|
+
process.stdout.write(`${renderHome({ version: APP_VERSION })}\n`);
|
|
14419
|
+
const picked = await searchSelect({
|
|
13058
14420
|
message: "What would you like to do?",
|
|
13059
|
-
|
|
14421
|
+
items: INTERACTIVE_OPTIONS.map((o) => ({
|
|
13060
14422
|
value: o.value,
|
|
13061
14423
|
label: o.label,
|
|
13062
14424
|
hint: o.hint
|
|
13063
14425
|
}))
|
|
13064
14426
|
});
|
|
13065
|
-
if (
|
|
13066
|
-
await
|
|
14427
|
+
if (picked === null || picked === "quit") return;
|
|
14428
|
+
await runFramed(picked, directory, config);
|
|
13067
14429
|
while (true) {
|
|
13068
|
-
const again = await
|
|
13069
|
-
message: "Next?",
|
|
13070
|
-
|
|
14430
|
+
const again = await searchSelect({
|
|
14431
|
+
message: "Next action?",
|
|
14432
|
+
items: INTERACTIVE_OPTIONS.map((o) => ({
|
|
13071
14433
|
value: o.value,
|
|
13072
14434
|
label: o.label,
|
|
13073
14435
|
hint: o.hint
|
|
13074
14436
|
}))
|
|
13075
14437
|
});
|
|
13076
|
-
if (
|
|
13077
|
-
await
|
|
14438
|
+
if (again === null || again === "quit") return;
|
|
14439
|
+
await runFramed(again, directory, config);
|
|
13078
14440
|
}
|
|
13079
14441
|
};
|
|
13080
14442
|
|
|
@@ -13116,7 +14478,7 @@ const delta = (current, previous) => {
|
|
|
13116
14478
|
const buildTrendRender = (input) => {
|
|
13117
14479
|
const header = renderHeader({
|
|
13118
14480
|
version: APP_VERSION,
|
|
13119
|
-
command: "
|
|
14481
|
+
command: "Score history",
|
|
13120
14482
|
context: [],
|
|
13121
14483
|
brand: input.printBrand !== false
|
|
13122
14484
|
});
|
|
@@ -13178,7 +14540,13 @@ const isOutdated = (current, latest) => {
|
|
|
13178
14540
|
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
13179
14541
|
return l.patch > c.patch;
|
|
13180
14542
|
};
|
|
13181
|
-
const formatUpdateNotice = (current, latest) =>
|
|
14543
|
+
const formatUpdateNotice = (current, latest) => [
|
|
14544
|
+
"",
|
|
14545
|
+
`Update available: ${current} -> ${latest}.`,
|
|
14546
|
+
"Upgrade: npm i -g aislop@latest",
|
|
14547
|
+
"One-off: npx aislop@latest",
|
|
14548
|
+
""
|
|
14549
|
+
].join("\n");
|
|
13182
14550
|
const readCache = (cachePath) => {
|
|
13183
14551
|
try {
|
|
13184
14552
|
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
@@ -13225,6 +14593,49 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
|
|
|
13225
14593
|
}
|
|
13226
14594
|
};
|
|
13227
14595
|
|
|
14596
|
+
//#endregion
|
|
14597
|
+
//#region src/commands/update.ts
|
|
14598
|
+
const renderUpgradeHelp = (label = "Upgrade:") => [
|
|
14599
|
+
`${style(theme, "dim", label)}`,
|
|
14600
|
+
" npm i -g aislop@latest",
|
|
14601
|
+
"",
|
|
14602
|
+
`${style(theme, "dim", "One-off latest run:")}`,
|
|
14603
|
+
" npx aislop@latest",
|
|
14604
|
+
""
|
|
14605
|
+
].join("\n");
|
|
14606
|
+
const buildUpdateStatusRender = (input) => {
|
|
14607
|
+
const lines = [
|
|
14608
|
+
`Current: ${input.current}`,
|
|
14609
|
+
`Latest: ${input.latest ?? "unavailable"}`,
|
|
14610
|
+
""
|
|
14611
|
+
];
|
|
14612
|
+
if (!input.latest) {
|
|
14613
|
+
lines.push("Status: could not reach the npm registry right now.", "");
|
|
14614
|
+
lines.push(renderUpgradeHelp("Use latest when npm is reachable:").trimEnd());
|
|
14615
|
+
return `${lines.join("\n")}\n`;
|
|
14616
|
+
}
|
|
14617
|
+
if (isOutdated(input.current, input.latest)) {
|
|
14618
|
+
lines.push(`Status: update available (${input.current} -> ${input.latest}).`, "");
|
|
14619
|
+
lines.push(renderUpgradeHelp("Upgrade:").trimEnd());
|
|
14620
|
+
return `${lines.join("\n")}\n`;
|
|
14621
|
+
}
|
|
14622
|
+
lines.push("Status: aislop is up to date.", "");
|
|
14623
|
+
lines.push(renderUpgradeHelp("Latest commands:").trimEnd());
|
|
14624
|
+
return `${lines.join("\n")}\n`;
|
|
14625
|
+
};
|
|
14626
|
+
const updateCommand = async (options = {}) => {
|
|
14627
|
+
if (options.printBrand !== false) process.stdout.write(renderHeader({
|
|
14628
|
+
version: APP_VERSION,
|
|
14629
|
+
command: "Update check",
|
|
14630
|
+
context: ["npm"]
|
|
14631
|
+
}));
|
|
14632
|
+
const latest = await fetchLatestVersion();
|
|
14633
|
+
process.stdout.write(buildUpdateStatusRender({
|
|
14634
|
+
current: APP_VERSION,
|
|
14635
|
+
latest
|
|
14636
|
+
}));
|
|
14637
|
+
};
|
|
14638
|
+
|
|
13228
14639
|
//#endregion
|
|
13229
14640
|
//#region src/cli.ts
|
|
13230
14641
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -13253,6 +14664,7 @@ const runScan = async (directory, flags) => {
|
|
|
13253
14664
|
const { exitCode } = await scanCommand(directory, finalConfig, {
|
|
13254
14665
|
changes: Boolean(flags.changes),
|
|
13255
14666
|
staged: Boolean(flags.staged),
|
|
14667
|
+
base: flags.base,
|
|
13256
14668
|
verbose: Boolean(flags.verbose),
|
|
13257
14669
|
json: !sarif && wantsJson(flags),
|
|
13258
14670
|
sarif,
|
|
@@ -13265,46 +14677,32 @@ const runScan = async (directory, flags) => {
|
|
|
13265
14677
|
}
|
|
13266
14678
|
};
|
|
13267
14679
|
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);
|
|
13268
|
-
const
|
|
13269
|
-
|
|
14680
|
+
const hasNoUserArgs = () => process.argv.slice(2).length === 0;
|
|
14681
|
+
const shouldRenderRootHelp = () => {
|
|
14682
|
+
const args = process.argv.slice(2);
|
|
14683
|
+
return args.length === 1 && [
|
|
14684
|
+
"--help",
|
|
14685
|
+
"-h",
|
|
14686
|
+
"help"
|
|
14687
|
+
].includes(args[0] ?? "");
|
|
14688
|
+
};
|
|
14689
|
+
const shouldRenderPlainVersion = () => {
|
|
14690
|
+
const args = process.argv.slice(2);
|
|
14691
|
+
return args.length === 1 && [
|
|
14692
|
+
"-V",
|
|
14693
|
+
"-v",
|
|
14694
|
+
"--version",
|
|
14695
|
+
"version"
|
|
14696
|
+
].includes(args[0] ?? "");
|
|
14697
|
+
};
|
|
14698
|
+
const program = new Command().name("aislop").description("The quality gate for agentic coding.").version(APP_VERSION, "-v, --version").argument("[directory]", "directory to scan when no command is passed", ".").option("--changes", "only scan changed files (git diff)").option("--staged", "only scan staged files").option("--base <ref>", "diff base for --changes, e.g. origin/main (default HEAD)").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) => {
|
|
14699
|
+
if (hasNoUserArgs() && noFlagsPassed(flags) && process.stdin.isTTY) try {
|
|
13270
14700
|
await interactiveCommand(directory, loadConfig(directory));
|
|
13271
14701
|
return;
|
|
13272
14702
|
} catch {}
|
|
13273
14703
|
await runScan(directory, flags);
|
|
13274
|
-
})
|
|
13275
|
-
|
|
13276
|
-
command: "--bare",
|
|
13277
|
-
context: []
|
|
13278
|
-
})).addHelpText("after", `
|
|
13279
|
-
${style(theme, "dim", "Commands:")}
|
|
13280
|
-
npx aislop scan [dir] Full code quality scan
|
|
13281
|
-
npx aislop fix [dir] Auto-fix ai slop in codebase
|
|
13282
|
-
npx aislop init [dir] Initialize aislop config
|
|
13283
|
-
npx aislop doctor [dir] Check installed tools
|
|
13284
|
-
npx aislop ci [dir] CI-friendly JSON output
|
|
13285
|
-
npx aislop rules [dir] List all rules
|
|
13286
|
-
npx aislop trend [dir] Show score history trend
|
|
13287
|
-
|
|
13288
|
-
${style(theme, "dim", "Examples:")}
|
|
13289
|
-
npx aislop Interactive menu
|
|
13290
|
-
npx aislop scan Scan entire project
|
|
13291
|
-
npx aislop scan -d Scan with file/line details
|
|
13292
|
-
npx aislop scan --changes Scan only changed files
|
|
13293
|
-
npx aislop scan --staged Scan only staged files (for hooks)
|
|
13294
|
-
npx aislop fix Auto-fix ai slop in codebase
|
|
13295
|
-
npx aislop fix -f Run aggressive fixes (includes audit and dependency alignment)
|
|
13296
|
-
npx aislop fix --claude Open Claude Code to fix remaining issues
|
|
13297
|
-
npx aislop fix --cursor Open Cursor + copy prompt to clipboard
|
|
13298
|
-
npx aislop fix -p Print a prompt to paste into any coding agent
|
|
13299
|
-
npx aislop ci JSON output for CI pipelines
|
|
13300
|
-
npx aislop scan --sarif SARIF 2.1.0 for GitHub code scanning
|
|
13301
|
-
npx aislop trend Show score history over time
|
|
13302
|
-
npx aislop scan --exclude node_modules
|
|
13303
|
-
npx aislop scan --exclude node_modules,dist,file.txt
|
|
13304
|
-
npx aislop scan --exclude node_modules --exclude dist --exclude **/*.ts
|
|
13305
|
-
${renderHintLine("Run npx aislop scan to scan your project").trimEnd()}
|
|
13306
|
-
`);
|
|
13307
|
-
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) => {
|
|
14704
|
+
});
|
|
14705
|
+
program.command("scan [directory]").description("Score a project and print findings").option("--changes", "only scan changed files").option("--staged", "only scan staged files").option("--base <ref>", "diff base for --changes, e.g. origin/main (default HEAD)").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) => {
|
|
13308
14706
|
await runScan(directory, command.optsWithGlobals());
|
|
13309
14707
|
});
|
|
13310
14708
|
const FIX_AGENT_FLAGS = [
|
|
@@ -13392,7 +14790,7 @@ const FIX_AGENT_FLAGS = [
|
|
|
13392
14790
|
const matchFixAgent = (flags) => {
|
|
13393
14791
|
return FIX_AGENT_FLAGS.find((a) => flags[a.name])?.flag;
|
|
13394
14792
|
};
|
|
13395
|
-
const fixProgram = program.command("fix [directory]").description("Auto-fix
|
|
14793
|
+
const fixProgram = program.command("fix [directory]").description("Auto-fix findings or hand off to a coding agent").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").option("--safe", "only apply reversible fixes (imports, comment removal, formatting); skip anything that deletes code or rewrites behaviour").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues");
|
|
13396
14794
|
for (const a of FIX_AGENT_FLAGS) fixProgram.option(`--${a.flag}`, a.help);
|
|
13397
14795
|
fixProgram.action(async (directory = ".", _flags, command) => {
|
|
13398
14796
|
const flags = command.optsWithGlobals();
|
|
@@ -13404,7 +14802,7 @@ fixProgram.action(async (directory = ".", _flags, command) => {
|
|
|
13404
14802
|
agent: matchFixAgent(flags)
|
|
13405
14803
|
});
|
|
13406
14804
|
});
|
|
13407
|
-
program.command("init [directory]").description("
|
|
14805
|
+
program.command("init [directory]").description("Create aislop config and optional CI workflow").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
|
|
13408
14806
|
const flags = command.optsWithGlobals();
|
|
13409
14807
|
await withCommandLifecycle({
|
|
13410
14808
|
command: "init",
|
|
@@ -13414,7 +14812,7 @@ program.command("init [directory]").description("Initialize aislop config in pro
|
|
|
13414
14812
|
return { exitCode: 0 };
|
|
13415
14813
|
});
|
|
13416
14814
|
});
|
|
13417
|
-
program.command("doctor [directory]").description("Check
|
|
14815
|
+
program.command("doctor [directory]").description("Check toolchain coverage for this project").action(async (directory = ".") => {
|
|
13418
14816
|
await withCommandLifecycle({
|
|
13419
14817
|
command: "doctor",
|
|
13420
14818
|
config: loadConfig(directory).telemetry
|
|
@@ -13423,9 +14821,21 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
13423
14821
|
return { exitCode: 0 };
|
|
13424
14822
|
});
|
|
13425
14823
|
});
|
|
13426
|
-
program.command("ci [directory]").description("
|
|
14824
|
+
const ciProgram = program.command("ci [directory]").description("Run the quality gate for CI");
|
|
14825
|
+
for (const [flag, description] of [
|
|
14826
|
+
["--changes", "only gate files changed vs --base (or HEAD)"],
|
|
14827
|
+
["--staged", "only gate staged files"],
|
|
14828
|
+
["--base <ref>", "diff base for --changes, e.g. origin/main (default HEAD)"],
|
|
14829
|
+
["--human", "render the human-friendly scan design instead of JSON"],
|
|
14830
|
+
["--sarif", "output SARIF 2.1.0 (for GitHub code scanning)"],
|
|
14831
|
+
["--format <format>", "output format: json or sarif"]
|
|
14832
|
+
]) ciProgram.option(flag, description);
|
|
14833
|
+
ciProgram.action(async (directory = ".", _flags, command) => {
|
|
13427
14834
|
const flags = command.optsWithGlobals();
|
|
13428
14835
|
const { exitCode } = await ciCommand(directory, loadConfig(directory), {
|
|
14836
|
+
changes: Boolean(flags.changes),
|
|
14837
|
+
staged: Boolean(flags.staged),
|
|
14838
|
+
base: flags.base,
|
|
13429
14839
|
human: Boolean(flags.human),
|
|
13430
14840
|
sarif: Boolean(flags.sarif) || flags.format === "sarif"
|
|
13431
14841
|
});
|
|
@@ -13434,16 +14844,17 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
|
|
|
13434
14844
|
process.exitCode = exitCode;
|
|
13435
14845
|
}
|
|
13436
14846
|
});
|
|
13437
|
-
program.command("rules [directory]").description("
|
|
14847
|
+
program.command("rules [directory]").description("Explain rules, severity, and fix mode").option("-s, --search", "open an interactive searchable rule explorer").action(async (directory = ".", _flags, command) => {
|
|
14848
|
+
const flags = command.optsWithGlobals();
|
|
13438
14849
|
await withCommandLifecycle({
|
|
13439
14850
|
command: "rules",
|
|
13440
14851
|
config: loadConfig(directory).telemetry
|
|
13441
14852
|
}, async () => {
|
|
13442
|
-
await rulesCommand(directory);
|
|
14853
|
+
await rulesCommand(directory, { interactive: Boolean(flags.search) });
|
|
13443
14854
|
return { exitCode: 0 };
|
|
13444
14855
|
});
|
|
13445
14856
|
});
|
|
13446
|
-
program.command("badge [directory]").description("Print
|
|
14857
|
+
program.command("badge [directory]").description("Print score badge URL and README markdown").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
|
|
13447
14858
|
const flags = command.optsWithGlobals();
|
|
13448
14859
|
try {
|
|
13449
14860
|
await withCommandLifecycle({
|
|
@@ -13464,7 +14875,7 @@ program.command("badge [directory]").description("Print the public score badge U
|
|
|
13464
14875
|
process.exit(1);
|
|
13465
14876
|
}
|
|
13466
14877
|
});
|
|
13467
|
-
program.command("trend [directory]").description("Show score history
|
|
14878
|
+
program.command("trend [directory]").description("Show local score history").option("--limit <n>", "number of recent runs to show", (v) => Number.parseInt(v, 10)).action(async (directory = ".", _flags, command) => {
|
|
13468
14879
|
const flags = command.optsWithGlobals();
|
|
13469
14880
|
await withCommandLifecycle({
|
|
13470
14881
|
command: "trend",
|
|
@@ -13474,9 +14885,27 @@ program.command("trend [directory]").description("Show score history trend from
|
|
|
13474
14885
|
return { exitCode: 0 };
|
|
13475
14886
|
});
|
|
13476
14887
|
});
|
|
14888
|
+
program.command("update").alias("upgrade").description("Check npm for the latest aislop version").action(async () => {
|
|
14889
|
+
await updateCommand();
|
|
14890
|
+
});
|
|
14891
|
+
program.command("version").description("Print the installed aislop version").action(() => {
|
|
14892
|
+
process.stdout.write(`${APP_VERSION}\n`);
|
|
14893
|
+
});
|
|
14894
|
+
program.command("commands").description("List all commands and major flags").action(() => {
|
|
14895
|
+
process.stdout.write(renderCommandReference({ version: APP_VERSION }));
|
|
14896
|
+
});
|
|
13477
14897
|
registerHookCommand(program);
|
|
14898
|
+
registerHookAliases(program);
|
|
13478
14899
|
const main = async () => {
|
|
13479
14900
|
fireInstalledOnce();
|
|
14901
|
+
if (shouldRenderPlainVersion()) {
|
|
14902
|
+
process.stdout.write(`${APP_VERSION}\n`);
|
|
14903
|
+
return;
|
|
14904
|
+
}
|
|
14905
|
+
if (shouldRenderRootHelp()) {
|
|
14906
|
+
process.stdout.write(renderRootHelp({ version: APP_VERSION }));
|
|
14907
|
+
return;
|
|
14908
|
+
}
|
|
13480
14909
|
await program.parseAsync();
|
|
13481
14910
|
await flushTelemetry();
|
|
13482
14911
|
await maybeNotifyUpdate();
|
|
@@ -13484,4 +14913,4 @@ const main = async () => {
|
|
|
13484
14913
|
main();
|
|
13485
14914
|
|
|
13486
14915
|
//#endregion
|
|
13487
|
-
export { runSubprocess as n,
|
|
14916
|
+
export { APP_VERSION as a, runSubprocess as i, withFindingAssessments as n, ENGINE_INFO as r, summarizeFindingAssessments as t };
|