@methiu/raigal 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AgentApp-CIiRfOun.js → AgentApp-DJYdD1VH.js} +4 -2
- package/dist/adapters/astro.d.ts +1 -6
- package/dist/adapters/astro.js +1 -5
- package/dist/adapters/expo.d.ts +2 -6
- package/dist/adapters/expo.js +2 -5
- package/dist/adapters/nuxt.d.ts +1 -6
- package/dist/adapters/nuxt.js +1 -5
- package/dist/adapters/sveltekit.d.ts +1 -6
- package/dist/adapters/sveltekit.js +3 -7
- package/dist/adapters/vite.d.ts +1 -7
- package/dist/adapters/vite.js +3 -7
- package/dist/cli.js +1582 -865
- package/dist/{finding-assessment-M5fUTE-C.js → finding-assessment-CGpt5YUR.js} +3 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +1529 -157
- package/dist/{json-C9fBWj3V.js → json-BaycSoJP.js} +2 -1
- package/dist/{json-C2IwKr9o.js → json-BflWt4dX.js} +4 -3
- package/dist/mcp.js +500 -126
- package/dist/{mount-DTc8WaIt.js → mount-CHGc3gB6.js} +1 -1
- package/dist/{sarif--DI_2p2r.js → sarif-CdtsAfJ1.js} +1 -1
- package/dist/{sarif-Do8E1xhZ.js → sarif-CpuYM3aK.js} +2 -2
- package/dist/{version-v4OAaBLZ.js → version-B4eYJn3o.js} +1 -1
- package/package.json +8 -6
- package/scripts/release-guard.mjs +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as APP_VERSION } from "./version-
|
|
2
|
-
import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-
|
|
1
|
+
import { t as APP_VERSION } from "./version-B4eYJn3o.js";
|
|
2
|
+
import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-CGpt5YUR.js";
|
|
3
3
|
import { n as isSecretClassRule, r as maskSecrets, t as FIXED_SECRET_DIAGNOSTIC_MESSAGE } from "./mask-secrets-BXn-ZSEh.js";
|
|
4
4
|
import { createRequire, isBuiltin } from "node:module";
|
|
5
5
|
import path from "node:path";
|
|
@@ -8,14 +8,14 @@ import YAML, { parse } from "yaml";
|
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
9
|
import pc from "picocolors";
|
|
10
10
|
import wcwidth from "wcwidth";
|
|
11
|
-
import
|
|
11
|
+
import picomatch from "picomatch";
|
|
12
12
|
import { spawn, spawnSync } from "node:child_process";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
14
|
import os, { tmpdir } from "node:os";
|
|
15
15
|
import { performance } from "node:perf_hooks";
|
|
16
16
|
import ts from "typescript";
|
|
17
17
|
import { parse as parse$1 } from "smol-toml";
|
|
18
|
-
import { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
|
|
18
|
+
import crypto, { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
|
|
19
19
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
20
20
|
import * as readline from "node:readline";
|
|
21
21
|
import { Writable } from "node:stream";
|
|
@@ -88,7 +88,8 @@ const DEFAULT_CONFIG = {
|
|
|
88
88
|
format: "json"
|
|
89
89
|
},
|
|
90
90
|
telemetry: { enabled: true },
|
|
91
|
-
rules: {}
|
|
91
|
+
rules: {},
|
|
92
|
+
overrides: []
|
|
92
93
|
};
|
|
93
94
|
const GITHUB_WORKFLOW_DIR = ".github/workflows";
|
|
94
95
|
const GITHUB_WORKFLOW_FILE = "raigal.yml";
|
|
@@ -131,7 +132,7 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
|
131
132
|
//#endregion
|
|
132
133
|
//#region src/config/extends.ts
|
|
133
134
|
const MAX_DEPTH$1 = 5;
|
|
134
|
-
const MAX_CONFIG_BYTES =
|
|
135
|
+
const MAX_CONFIG_BYTES = 1048576;
|
|
135
136
|
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
136
137
|
const deepMerge = (...sources) => {
|
|
137
138
|
const result = {};
|
|
@@ -312,12 +313,25 @@ const CiSchema = z.object({
|
|
|
312
313
|
format: z.enum(["json"]).default("json")
|
|
313
314
|
}).strict();
|
|
314
315
|
const TelemetrySchema = z.object({ enabled: z.boolean().default(true) }).strict();
|
|
316
|
+
const BaselineConfigSchema = z.object({ mode: z.enum(["strict", "relaxed"]).default("relaxed") }).strict();
|
|
317
|
+
const LicensingConfigSchema = z.object({ onLapse: z.enum(["fail-open", "fail-closed"]).default("fail-open") }).strict();
|
|
318
|
+
const WorkspaceConfigSchema = z.object({ minimumFloor: z.number().min(0).max(100).default(50) }).strict();
|
|
315
319
|
const RuleSeverityOverride = z.enum([
|
|
316
320
|
"error",
|
|
317
321
|
"warning",
|
|
318
322
|
"off"
|
|
319
323
|
]);
|
|
320
324
|
const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
|
|
325
|
+
const FileOverrideSchema = z.object({
|
|
326
|
+
files: z.array(z.string().min(1)).min(1),
|
|
327
|
+
quality: z.object({
|
|
328
|
+
maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
|
|
329
|
+
maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
|
|
330
|
+
maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
|
|
331
|
+
maxParams: QualitySchema.shape.maxParams.unwrap().optional()
|
|
332
|
+
}).optional(),
|
|
333
|
+
rules: z.record(z.string(), RuleSeverityOverride).optional()
|
|
334
|
+
});
|
|
321
335
|
const AislopConfigSchema = z.object({
|
|
322
336
|
version: z.number().default(1),
|
|
323
337
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
@@ -372,7 +386,11 @@ const AislopConfigSchema = z.object({
|
|
|
372
386
|
format: "json"
|
|
373
387
|
})),
|
|
374
388
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
389
|
+
baseline: BaselineConfigSchema.optional(),
|
|
390
|
+
licensing: LicensingConfigSchema.optional(),
|
|
391
|
+
workspace: WorkspaceConfigSchema.optional(),
|
|
375
392
|
rules: RulesSchema,
|
|
393
|
+
overrides: z.array(FileOverrideSchema).default(() => []),
|
|
376
394
|
exclude: z.array(z.string()).default(() => [
|
|
377
395
|
"node_modules",
|
|
378
396
|
".git",
|
|
@@ -405,6 +423,9 @@ const KNOWN_KEYS_BY_PATH = {
|
|
|
405
423
|
"scoring",
|
|
406
424
|
"ci",
|
|
407
425
|
"telemetry",
|
|
426
|
+
"baseline",
|
|
427
|
+
"licensing",
|
|
428
|
+
"workspace",
|
|
408
429
|
"rules",
|
|
409
430
|
"exclude",
|
|
410
431
|
"include"
|
|
@@ -455,7 +476,10 @@ const KNOWN_KEYS_BY_PATH = {
|
|
|
455
476
|
],
|
|
456
477
|
"scoring.thresholds": ["good", "ok"],
|
|
457
478
|
ci: ["failBelow", "format"],
|
|
458
|
-
telemetry: ["enabled"]
|
|
479
|
+
telemetry: ["enabled"],
|
|
480
|
+
baseline: ["mode"],
|
|
481
|
+
licensing: ["onLapse"],
|
|
482
|
+
workspace: ["minimumFloor"]
|
|
459
483
|
};
|
|
460
484
|
const levenshtein = (a, b) => {
|
|
461
485
|
const m = a.length;
|
|
@@ -587,7 +611,8 @@ const loadConfig = (directory) => {
|
|
|
587
611
|
const configPath = findConfigFile(directory);
|
|
588
612
|
if (!configPath) return DEFAULT_CONFIG;
|
|
589
613
|
try {
|
|
590
|
-
|
|
614
|
+
const merged = loadConfigChain(configPath);
|
|
615
|
+
return validateConfig(merged, configPath);
|
|
591
616
|
} catch (error) {
|
|
592
617
|
if (error instanceof ConfigError) throw error;
|
|
593
618
|
throw new ConfigError(configPath, `Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -674,7 +699,7 @@ const highlightRaigal = (text, theme, baseToken) => {
|
|
|
674
699
|
const tail = text.slice(lastIndex);
|
|
675
700
|
return out + (baseToken ? style(theme, baseToken, tail) : tail);
|
|
676
701
|
};
|
|
677
|
-
const highlightAislop = highlightRaigal;
|
|
702
|
+
const highlightAislop = (...args) => highlightRaigal(...args);
|
|
678
703
|
//#endregion
|
|
679
704
|
//#region src/ui/width.ts
|
|
680
705
|
const ANSI_RE$1 = new RegExp(`\\[[0-9;]*m`, "g");
|
|
@@ -683,7 +708,8 @@ const stringWidth = (s) => {
|
|
|
683
708
|
const bare = stripAnsi$1(s);
|
|
684
709
|
let total = 0;
|
|
685
710
|
for (const ch of bare) {
|
|
686
|
-
const
|
|
711
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
712
|
+
const w = wcwidth(cp);
|
|
687
713
|
total += w > 0 ? w : 1;
|
|
688
714
|
}
|
|
689
715
|
return total;
|
|
@@ -699,7 +725,8 @@ const truncate = (s, max, ellipsis = "…") => {
|
|
|
699
725
|
let out = "";
|
|
700
726
|
let w = 0;
|
|
701
727
|
for (const ch of s) {
|
|
702
|
-
const
|
|
728
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
729
|
+
const cw = wcwidth(cp);
|
|
703
730
|
if (w + cw > limit) break;
|
|
704
731
|
out += ch;
|
|
705
732
|
w += cw;
|
|
@@ -709,7 +736,8 @@ const truncate = (s, max, ellipsis = "…") => {
|
|
|
709
736
|
//#endregion
|
|
710
737
|
//#region src/ui/display.ts
|
|
711
738
|
const renderDisplaySection = (label, deps = {}) => {
|
|
712
|
-
|
|
739
|
+
const t = deps.theme ?? theme;
|
|
740
|
+
return style(t, "section", label);
|
|
713
741
|
};
|
|
714
742
|
const renderDisplayRows = (rows, options = {}) => {
|
|
715
743
|
if (rows.length === 0) return [];
|
|
@@ -856,8 +884,10 @@ const detectSourceLanguages = (sourceFiles) => {
|
|
|
856
884
|
const detectLanguages = (directory, sourceFiles) => {
|
|
857
885
|
const languages = new Set(detectSourceLanguages(sourceFiles));
|
|
858
886
|
for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
|
|
859
|
-
if (readPackageJson(path.join(directory, "package.json")))
|
|
860
|
-
|
|
887
|
+
if (readPackageJson(path.join(directory, "package.json"))) {
|
|
888
|
+
if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
|
|
889
|
+
else languages.add("javascript");
|
|
890
|
+
}
|
|
861
891
|
for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
|
|
862
892
|
languages.add("python");
|
|
863
893
|
break;
|
|
@@ -938,13 +968,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
|
|
|
938
968
|
const normalized = withoutProjectPrefix.slice(0, end);
|
|
939
969
|
if (normalized.length === 0) return [];
|
|
940
970
|
if (normalized.length > 256) return [];
|
|
941
|
-
if (
|
|
971
|
+
if (picomatch.scan(normalized).isGlob) return [normalized];
|
|
942
972
|
if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
|
|
943
973
|
return supportedGlobPatterns([normalized, `${normalized}/**`]);
|
|
944
974
|
});
|
|
945
975
|
const isPathExcluded = (relativePath, normalizedPatterns) => {
|
|
946
976
|
if (normalizedPatterns.length === 0) return false;
|
|
947
|
-
return
|
|
977
|
+
return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
|
|
948
978
|
};
|
|
949
979
|
const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
|
|
950
980
|
if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
|
|
@@ -990,7 +1020,7 @@ const isAutoGenerated = (filePath) => {
|
|
|
990
1020
|
};
|
|
991
1021
|
//#endregion
|
|
992
1022
|
//#region src/utils/git-ignore.ts
|
|
993
|
-
const MAX_BUFFER$2 =
|
|
1023
|
+
const MAX_BUFFER$2 = 52428800;
|
|
994
1024
|
const NOT_IGNORED_ARGUMENTS = [
|
|
995
1025
|
"ls-files",
|
|
996
1026
|
"--cached",
|
|
@@ -1101,7 +1131,7 @@ const dropGitIgnoredPaths = (rootDirectory, absolutePaths) => {
|
|
|
1101
1131
|
};
|
|
1102
1132
|
//#endregion
|
|
1103
1133
|
//#region src/utils/project-file-list.ts
|
|
1104
|
-
const MAX_BUFFER$1 =
|
|
1134
|
+
const MAX_BUFFER$1 = 52428800;
|
|
1105
1135
|
const normalizePruneDirectories = (directories) => new Set([...directories].map((directory) => directory.toLowerCase()));
|
|
1106
1136
|
const isInPrunedDirectory = (filePath, pruneDirectories) => {
|
|
1107
1137
|
const pathSegments = filePath.split("/");
|
|
@@ -1312,12 +1342,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
|
|
|
1312
1342
|
const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
|
|
1313
1343
|
if (normalized === "" || normalized === ".") return ["**"];
|
|
1314
1344
|
if (normalized.length > 256) return [];
|
|
1315
|
-
if (
|
|
1345
|
+
if (picomatch.scan(normalized).isGlob) return [normalized];
|
|
1316
1346
|
return supportedGlobPatterns([normalized, `${normalized}/**`]);
|
|
1317
1347
|
});
|
|
1318
1348
|
const createPathMatcher = (patterns) => {
|
|
1319
|
-
|
|
1320
|
-
|
|
1349
|
+
if (patterns.length === 0) return () => false;
|
|
1350
|
+
const isMatch = picomatch(patterns, { dot: true });
|
|
1351
|
+
return (filePath) => isMatch(filePath);
|
|
1321
1352
|
};
|
|
1322
1353
|
const filterFiles = (rootDirectory, files, options) => {
|
|
1323
1354
|
const extraExtensions = new Set(options.extraExtensions);
|
|
@@ -1651,7 +1682,8 @@ const resolvePackageRoot = (startFile) => {
|
|
|
1651
1682
|
while (true) {
|
|
1652
1683
|
const packageJsonPath = path.join(current, "package.json");
|
|
1653
1684
|
if (fs.existsSync(packageJsonPath)) try {
|
|
1654
|
-
|
|
1685
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1686
|
+
if (packageJson.name === "aislop" || packageJson.name === "@methiu/raigal" || packageJson.name === "raigal") return current;
|
|
1655
1687
|
} catch {}
|
|
1656
1688
|
const parent = path.dirname(current);
|
|
1657
1689
|
if (parent === current) break;
|
|
@@ -1746,7 +1778,7 @@ const checkInstalledTools = async () => {
|
|
|
1746
1778
|
};
|
|
1747
1779
|
//#endregion
|
|
1748
1780
|
//#region src/utils/project-path-safety.ts
|
|
1749
|
-
const MAX_PROJECT_CONFIG_BYTES =
|
|
1781
|
+
const MAX_PROJECT_CONFIG_BYTES = 1048576;
|
|
1750
1782
|
const isWithinDirectory$2 = (rootDirectory, candidate) => {
|
|
1751
1783
|
const relative = path.relative(rootDirectory, candidate);
|
|
1752
1784
|
return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
@@ -1800,7 +1832,7 @@ const isRootBoundedTarget = (candidate, rootDirectory) => {
|
|
|
1800
1832
|
};
|
|
1801
1833
|
//#endregion
|
|
1802
1834
|
//#region src/utils/aislop-ignore.ts
|
|
1803
|
-
const
|
|
1835
|
+
const readAislopIgnorePatterns = (rootDirectory) => {
|
|
1804
1836
|
const patterns = [];
|
|
1805
1837
|
for (const fileName of [".raigalignore", ".aislopignore"]) {
|
|
1806
1838
|
const targetPath = safeProjectFilePath(path.join(rootDirectory, fileName), rootDirectory);
|
|
@@ -1811,7 +1843,6 @@ const readRaigalIgnorePatterns = (rootDirectory) => {
|
|
|
1811
1843
|
}
|
|
1812
1844
|
return patterns;
|
|
1813
1845
|
};
|
|
1814
|
-
const readAislopIgnorePatterns = readRaigalIgnorePatterns;
|
|
1815
1846
|
//#endregion
|
|
1816
1847
|
//#region src/utils/source-file-context.ts
|
|
1817
1848
|
const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
|
|
@@ -2072,7 +2103,8 @@ const resolveTrustedTscPath = () => {
|
|
|
2072
2103
|
};
|
|
2073
2104
|
const isReferenceOnlyConfig = (tsconfigPath) => {
|
|
2074
2105
|
try {
|
|
2075
|
-
const
|
|
2106
|
+
const raw = fs.readFileSync(tsconfigPath, "utf-8");
|
|
2107
|
+
const parsed = parseJsonc(raw);
|
|
2076
2108
|
if (!parsed) return false;
|
|
2077
2109
|
return Array.isArray(parsed.references) && !parsed.files && !parsed.include && !parsed.extends;
|
|
2078
2110
|
} catch {
|
|
@@ -2391,7 +2423,7 @@ const changeContextOrder = (context) => {
|
|
|
2391
2423
|
};
|
|
2392
2424
|
//#endregion
|
|
2393
2425
|
//#region src/utils/git.ts
|
|
2394
|
-
const MAX_BUFFER =
|
|
2426
|
+
const MAX_BUFFER = 52428800;
|
|
2395
2427
|
const baseRefExists = (cwd, ref) => {
|
|
2396
2428
|
const result = spawnSync("git", [
|
|
2397
2429
|
"rev-parse",
|
|
@@ -2639,6 +2671,132 @@ const doctorCommand = async (directory, options = {}) => {
|
|
|
2639
2671
|
}));
|
|
2640
2672
|
};
|
|
2641
2673
|
//#endregion
|
|
2674
|
+
//#region src/utils/globs.ts
|
|
2675
|
+
const MAX_BRACE_EXPANSIONS = 256;
|
|
2676
|
+
const braceListAt = (pattern, start) => {
|
|
2677
|
+
let depth = 0;
|
|
2678
|
+
const commas = [];
|
|
2679
|
+
for (let i = start; i < pattern.length; i += 1) {
|
|
2680
|
+
const char = pattern[i];
|
|
2681
|
+
if (char === "\\") i += 1;
|
|
2682
|
+
else if (char === "{") depth += 1;
|
|
2683
|
+
else if (char === ",") {
|
|
2684
|
+
if (depth === 1) commas.push(i);
|
|
2685
|
+
} else if (char === "}") {
|
|
2686
|
+
depth -= 1;
|
|
2687
|
+
if (depth === 0) return commas.length > 0 ? {
|
|
2688
|
+
start,
|
|
2689
|
+
end: i,
|
|
2690
|
+
commas
|
|
2691
|
+
} : null;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return null;
|
|
2695
|
+
};
|
|
2696
|
+
const findBraceList = (pattern) => {
|
|
2697
|
+
for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
|
|
2698
|
+
if (start > 0 && pattern[start - 1] === "\\") continue;
|
|
2699
|
+
const list = braceListAt(pattern, start);
|
|
2700
|
+
if (list) return list;
|
|
2701
|
+
}
|
|
2702
|
+
return null;
|
|
2703
|
+
};
|
|
2704
|
+
const expandInto = (pattern, results) => {
|
|
2705
|
+
const list = findBraceList(pattern);
|
|
2706
|
+
if (!list) {
|
|
2707
|
+
results.push(pattern);
|
|
2708
|
+
return results.length <= MAX_BRACE_EXPANSIONS;
|
|
2709
|
+
}
|
|
2710
|
+
const bounds = [
|
|
2711
|
+
list.start,
|
|
2712
|
+
...list.commas,
|
|
2713
|
+
list.end
|
|
2714
|
+
];
|
|
2715
|
+
const prefix = pattern.slice(0, list.start);
|
|
2716
|
+
const suffix = pattern.slice(list.end + 1);
|
|
2717
|
+
for (let i = 0; i < bounds.length - 1; i += 1) {
|
|
2718
|
+
const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
|
|
2719
|
+
if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
|
|
2720
|
+
}
|
|
2721
|
+
return true;
|
|
2722
|
+
};
|
|
2723
|
+
const expandBraceLists = (pattern) => {
|
|
2724
|
+
const results = [];
|
|
2725
|
+
return expandInto(pattern, results) ? results : [pattern];
|
|
2726
|
+
};
|
|
2727
|
+
const isNegated = (pattern) => picomatch.scan(pattern).negated;
|
|
2728
|
+
const matchesGlobList = (value, patterns, options = {}) => {
|
|
2729
|
+
const expanded = patterns.flatMap(expandBraceLists);
|
|
2730
|
+
const positives = expanded.filter((pattern) => !isNegated(pattern));
|
|
2731
|
+
const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
|
|
2732
|
+
const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
|
|
2733
|
+
const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
|
|
2734
|
+
return included && !excluded;
|
|
2735
|
+
};
|
|
2736
|
+
//#endregion
|
|
2737
|
+
//#region src/scoring/rule-severity.ts
|
|
2738
|
+
/**
|
|
2739
|
+
* Apply per-rule severity overrides from config: "off" drops the diagnostic,
|
|
2740
|
+
* "error"/"warning" rewrite its severity before scoring and rendering.
|
|
2741
|
+
*/
|
|
2742
|
+
const applyRuleSeverities = (diagnostics, overrides) => {
|
|
2743
|
+
if (Object.keys(overrides).length === 0) return diagnostics;
|
|
2744
|
+
const result = [];
|
|
2745
|
+
for (const diagnostic of diagnostics) {
|
|
2746
|
+
const override = overrides[diagnostic.rule];
|
|
2747
|
+
if (!override) {
|
|
2748
|
+
result.push(diagnostic);
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
if (override === "off") continue;
|
|
2752
|
+
result.push(override === diagnostic.severity ? diagnostic : {
|
|
2753
|
+
...diagnostic,
|
|
2754
|
+
severity: override
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
return result;
|
|
2758
|
+
};
|
|
2759
|
+
//#endregion
|
|
2760
|
+
//#region src/config/overrides.ts
|
|
2761
|
+
const overrideRelativePath = (rootDirectory, filePath) => {
|
|
2762
|
+
const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
|
|
2763
|
+
const root = rootDirectory.replaceAll("\\", "/");
|
|
2764
|
+
const file = filePath.replaceAll("\\", "/");
|
|
2765
|
+
return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
|
|
2766
|
+
};
|
|
2767
|
+
const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
|
|
2768
|
+
const resolveFilePolicy = (config, relativePath) => {
|
|
2769
|
+
let quality = config.quality;
|
|
2770
|
+
let rules = config.rules ?? {};
|
|
2771
|
+
if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
|
|
2772
|
+
quality,
|
|
2773
|
+
rules
|
|
2774
|
+
};
|
|
2775
|
+
for (const entry of config.overrides ?? []) {
|
|
2776
|
+
if (!matchesEntry(relativePath, entry.files)) continue;
|
|
2777
|
+
if (entry.quality) quality = {
|
|
2778
|
+
...quality,
|
|
2779
|
+
...entry.quality
|
|
2780
|
+
};
|
|
2781
|
+
if (entry.rules) rules = {
|
|
2782
|
+
...rules,
|
|
2783
|
+
...entry.rules
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
return {
|
|
2787
|
+
quality,
|
|
2788
|
+
rules
|
|
2789
|
+
};
|
|
2790
|
+
};
|
|
2791
|
+
const applyFileOverrides = (diagnostics, context) => {
|
|
2792
|
+
if (!context.config.overrides?.length) return diagnostics;
|
|
2793
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
2794
|
+
const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
|
|
2795
|
+
const { rules } = resolveFilePolicy(context.config, relativePath);
|
|
2796
|
+
return applyRuleSeverities([diagnostic], rules);
|
|
2797
|
+
});
|
|
2798
|
+
};
|
|
2799
|
+
//#endregion
|
|
2642
2800
|
//#region src/utils/string-literals.ts
|
|
2643
2801
|
const consumeQuotedString = (content, start, quote) => {
|
|
2644
2802
|
const len = content.length;
|
|
@@ -3247,7 +3405,7 @@ const tokenizeJs = (content, ext = ".ts") => {
|
|
|
3247
3405
|
};
|
|
3248
3406
|
//#endregion
|
|
3249
3407
|
//#region src/utils/source-masker.ts
|
|
3250
|
-
const JS_EXTS$
|
|
3408
|
+
const JS_EXTS$3 = /* @__PURE__ */ new Set([
|
|
3251
3409
|
".ts",
|
|
3252
3410
|
".tsx",
|
|
3253
3411
|
".js",
|
|
@@ -3255,7 +3413,7 @@ const JS_EXTS$2 = /* @__PURE__ */ new Set([
|
|
|
3255
3413
|
".mjs",
|
|
3256
3414
|
".cjs"
|
|
3257
3415
|
]);
|
|
3258
|
-
const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
|
|
3416
|
+
const PY_EXTS$1 = /* @__PURE__ */ new Set([".py"]);
|
|
3259
3417
|
const RB_EXTS = /* @__PURE__ */ new Set([".rb"]);
|
|
3260
3418
|
const PHP_EXTS = /* @__PURE__ */ new Set([".php"]);
|
|
3261
3419
|
const CSHARP_EXTS = /* @__PURE__ */ new Set([".cs"]);
|
|
@@ -3279,8 +3437,8 @@ const C_STYLE_COMMENT_EXTS = /* @__PURE__ */ new Set([
|
|
|
3279
3437
|
".zig"
|
|
3280
3438
|
]);
|
|
3281
3439
|
const familyForExt = (ext) => {
|
|
3282
|
-
if (JS_EXTS$
|
|
3283
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
3440
|
+
if (JS_EXTS$3.has(ext)) return "js";
|
|
3441
|
+
if (PY_EXTS$1.has(ext)) return "py";
|
|
3284
3442
|
if (RB_EXTS.has(ext)) return "rb";
|
|
3285
3443
|
if (PHP_EXTS.has(ext)) return "php";
|
|
3286
3444
|
if (CSHARP_EXTS.has(ext)) return "csharp";
|
|
@@ -3550,7 +3708,7 @@ const areLiteralMultisetsEqual = (a, b) => {
|
|
|
3550
3708
|
};
|
|
3551
3709
|
//#endregion
|
|
3552
3710
|
//#region src/engines/ai-slop/abstractions.ts
|
|
3553
|
-
const JS_EXTS$
|
|
3711
|
+
const JS_EXTS$2 = /* @__PURE__ */ new Set([
|
|
3554
3712
|
".ts",
|
|
3555
3713
|
".tsx",
|
|
3556
3714
|
".js",
|
|
@@ -3561,11 +3719,11 @@ const JS_EXTS$1 = /* @__PURE__ */ new Set([
|
|
|
3561
3719
|
const THIN_WRAPPER_PATTERNS = [
|
|
3562
3720
|
{
|
|
3563
3721
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
3564
|
-
extensions: JS_EXTS$
|
|
3722
|
+
extensions: JS_EXTS$2
|
|
3565
3723
|
},
|
|
3566
3724
|
{
|
|
3567
3725
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
3568
|
-
extensions: JS_EXTS$
|
|
3726
|
+
extensions: JS_EXTS$2
|
|
3569
3727
|
},
|
|
3570
3728
|
{
|
|
3571
3729
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -4813,7 +4971,7 @@ const scriptKindFor$1 = (ext) => {
|
|
|
4813
4971
|
}
|
|
4814
4972
|
};
|
|
4815
4973
|
const lineFor$1 = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
4816
|
-
const makeDiagnostic = (filePath, line, rule, message, help) => ({
|
|
4974
|
+
const makeDiagnostic$1 = (filePath, line, rule, message, help) => ({
|
|
4817
4975
|
filePath,
|
|
4818
4976
|
engine: "ai-slop",
|
|
4819
4977
|
rule,
|
|
@@ -4863,7 +5021,7 @@ const detectRedundantTryCatch = (sourceFile, relativePath) => {
|
|
|
4863
5021
|
if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
|
|
4864
5022
|
const catchNameNode = node.catchClause.variableDeclaration?.name;
|
|
4865
5023
|
const [onlyStatement] = node.catchClause.block.statements;
|
|
4866
|
-
if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
|
|
5024
|
+
if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
|
|
4867
5025
|
}
|
|
4868
5026
|
ts.forEachChild(node, visit);
|
|
4869
5027
|
};
|
|
@@ -4881,7 +5039,7 @@ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
|
|
|
4881
5039
|
const [arg] = child.arguments;
|
|
4882
5040
|
if (arg && ts.isIdentifier(arg)) {
|
|
4883
5041
|
const primitive = params.get(arg.text);
|
|
4884
|
-
if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
|
|
5042
|
+
if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
|
|
4885
5043
|
}
|
|
4886
5044
|
}
|
|
4887
5045
|
ts.forEachChild(child, visitBody);
|
|
@@ -4924,7 +5082,7 @@ const detectDuplicateExportedTypes = (parsedSources) => {
|
|
|
4924
5082
|
continue;
|
|
4925
5083
|
}
|
|
4926
5084
|
if (previous.filePath === declaration.filePath) continue;
|
|
4927
|
-
diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
|
|
5085
|
+
diagnostics.push(makeDiagnostic$1(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
|
|
4928
5086
|
}
|
|
4929
5087
|
}
|
|
4930
5088
|
return diagnostics;
|
|
@@ -5026,7 +5184,7 @@ const detectDuplicateImports = async (context) => {
|
|
|
5026
5184
|
rule: "ai-slop/duplicate-import",
|
|
5027
5185
|
severity: "warning",
|
|
5028
5186
|
message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
|
|
5029
|
-
help: "Two imports from the same module split readers' attention and grow the import block. Run
|
|
5187
|
+
help: "Two imports from the same module split readers' attention and grow the import block. Run raigal fix to merge them automatically.",
|
|
5030
5188
|
line: dup.line,
|
|
5031
5189
|
column: 1,
|
|
5032
5190
|
category: "AI Slop",
|
|
@@ -6648,7 +6806,8 @@ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith
|
|
|
6648
6806
|
const RUNTIME_BUILTINS = /* @__PURE__ */ new Set(["bun"]);
|
|
6649
6807
|
const isJsBuiltin = (spec) => {
|
|
6650
6808
|
if (RUNTIME_BUILTINS.has(spec)) return true;
|
|
6651
|
-
|
|
6809
|
+
const stripped = spec.startsWith("node:") ? spec.slice(5) : spec;
|
|
6810
|
+
return isBuiltin(stripped) || isBuiltin(spec);
|
|
6652
6811
|
};
|
|
6653
6812
|
const VIRTUAL_MODULE_PREFIXES = [
|
|
6654
6813
|
"astro:",
|
|
@@ -6860,7 +7019,8 @@ const isLocalAliasReplacement = (rawValue, configDirectory, rootDirectory) => {
|
|
|
6860
7019
|
if (!target) return false;
|
|
6861
7020
|
if (!target.startsWith(".") && !target.startsWith("/") && !target.startsWith("~/") && !target.startsWith("@/")) return false;
|
|
6862
7021
|
const normalized = target.startsWith("~/") || target.startsWith("@/") ? target.slice(2) : target;
|
|
6863
|
-
|
|
7022
|
+
const candidate = path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized);
|
|
7023
|
+
return isRootBoundedTarget(candidate, rootDirectory);
|
|
6864
7024
|
};
|
|
6865
7025
|
const hasValidPackageImportTarget = (value, packageDirectory, rootDirectory) => {
|
|
6866
7026
|
if (!isRootBoundedTarget(packageDirectory, rootDirectory)) return false;
|
|
@@ -8054,7 +8214,9 @@ const isRustDocCommentLine = (line) => {
|
|
|
8054
8214
|
};
|
|
8055
8215
|
const collectBlocks = (sourceLines, syntax, content, ext) => {
|
|
8056
8216
|
const blocks = [];
|
|
8057
|
-
const
|
|
8217
|
+
const actualContent = content ?? sourceLines.join("\n");
|
|
8218
|
+
const actualExt = ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py");
|
|
8219
|
+
const commentLines = getCommentLines(actualContent, actualExt);
|
|
8058
8220
|
let i = 0;
|
|
8059
8221
|
while (i < sourceLines.length) {
|
|
8060
8222
|
const line = sourceLines[i];
|
|
@@ -8382,7 +8544,8 @@ const detectNarrativeComments = async (context) => {
|
|
|
8382
8544
|
} catch {
|
|
8383
8545
|
continue;
|
|
8384
8546
|
}
|
|
8385
|
-
const
|
|
8547
|
+
const lines = content.split("\n");
|
|
8548
|
+
const blocks = collectBlocks(lines, syntax, content, ext);
|
|
8386
8549
|
for (const block of blocks) {
|
|
8387
8550
|
const { matched, reason } = detectNarrativeInBlock(block, ext);
|
|
8388
8551
|
if (!matched) continue;
|
|
@@ -8854,7 +9017,7 @@ const detectRustPatterns = async (context) => {
|
|
|
8854
9017
|
};
|
|
8855
9018
|
//#endregion
|
|
8856
9019
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
8857
|
-
const JS_EXTS = /* @__PURE__ */ new Set([
|
|
9020
|
+
const JS_EXTS$1 = /* @__PURE__ */ new Set([
|
|
8858
9021
|
".ts",
|
|
8859
9022
|
".tsx",
|
|
8860
9023
|
".js",
|
|
@@ -9049,7 +9212,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
9049
9212
|
for (const filePath of files) {
|
|
9050
9213
|
if (isAutoGenerated(filePath)) continue;
|
|
9051
9214
|
const ext = path.extname(filePath);
|
|
9052
|
-
const isJs = JS_EXTS.has(ext);
|
|
9215
|
+
const isJs = JS_EXTS$1.has(ext);
|
|
9053
9216
|
if (!isJs && !(ext === ".py")) continue;
|
|
9054
9217
|
const relPath = relativePosix(context.rootDirectory, filePath);
|
|
9055
9218
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -9334,7 +9497,7 @@ const detectUnusedCss = async (context) => {
|
|
|
9334
9497
|
rule: "ai-slop/unused-css",
|
|
9335
9498
|
severity: "warning",
|
|
9336
9499
|
message: `CSS class '.${def.name}' is defined but never referenced in the codebase`,
|
|
9337
|
-
help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add
|
|
9500
|
+
help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add a raigal-ignore comment if so)",
|
|
9338
9501
|
line: def.line,
|
|
9339
9502
|
column: 0,
|
|
9340
9503
|
category: "AI Slop",
|
|
@@ -10324,7 +10487,8 @@ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
|
|
|
10324
10487
|
const results = [];
|
|
10325
10488
|
const multiplier = functionLocMultiplier(fn, ext);
|
|
10326
10489
|
const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
|
|
10327
|
-
|
|
10490
|
+
const effectiveLineCount = fn.lineCount - fn.templateLines;
|
|
10491
|
+
if (exceedsLimit(effectiveLineCount, limits.maxFunctionLoc, multiplier)) results.push({
|
|
10328
10492
|
filePath: relativePath,
|
|
10329
10493
|
engine: "code-quality",
|
|
10330
10494
|
rule: "complexity/function-too-long",
|
|
@@ -10381,10 +10545,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
|
10381
10545
|
};
|
|
10382
10546
|
const checkComplexity = async (context) => {
|
|
10383
10547
|
const files = getSourceFiles(context);
|
|
10384
|
-
const limits = context.config.quality;
|
|
10385
10548
|
const diagnostics = [];
|
|
10386
10549
|
for (const filePath of files) {
|
|
10387
10550
|
if (isAutoGenerated(filePath)) continue;
|
|
10551
|
+
const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
|
|
10388
10552
|
diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
|
|
10389
10553
|
}
|
|
10390
10554
|
return diagnostics;
|
|
@@ -10414,14 +10578,14 @@ const isTrivialLine = (line) => {
|
|
|
10414
10578
|
};
|
|
10415
10579
|
const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
|
|
10416
10580
|
const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
|
|
10417
|
-
const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
10418
|
-
const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
10581
|
+
const SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
10582
|
+
const FILE_SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
10419
10583
|
const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
|
|
10420
10584
|
const isLowSignalMarkupWindow = (lines) => {
|
|
10421
10585
|
return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
|
|
10422
10586
|
};
|
|
10423
10587
|
const isLowSignalDataWindow = (lines) => {
|
|
10424
|
-
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >=
|
|
10588
|
+
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= 9;
|
|
10425
10589
|
};
|
|
10426
10590
|
const findSuppressedLines = (lines) => {
|
|
10427
10591
|
const suppressed = /* @__PURE__ */ new Set();
|
|
@@ -10453,7 +10617,7 @@ const collectMeaningfulLines = (content) => {
|
|
|
10453
10617
|
if (isLowSignalDataWindow(window)) continue;
|
|
10454
10618
|
if (window.every(isTrivialLine)) continue;
|
|
10455
10619
|
const normalised = window.map(normaliseLine);
|
|
10456
|
-
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length <
|
|
10620
|
+
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < 9) continue;
|
|
10457
10621
|
if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
|
|
10458
10622
|
hits.push({
|
|
10459
10623
|
startLine: i + 1,
|
|
@@ -10557,8 +10721,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
|
|
|
10557
10721
|
return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
|
|
10558
10722
|
};
|
|
10559
10723
|
const DEPENDENCY_HELP = {
|
|
10560
|
-
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
10561
|
-
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
10724
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
|
|
10725
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
|
|
10562
10726
|
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
10563
10727
|
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
10564
10728
|
binaries: "This binary is used but its package is not in package.json."
|
|
@@ -11276,13 +11440,23 @@ const withReportPath = async (run) => {
|
|
|
11276
11440
|
};
|
|
11277
11441
|
const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
|
|
11278
11442
|
const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
|
|
11443
|
+
const expandBraces = (pattern, depth = 0) => {
|
|
11444
|
+
if (depth > 10) return [pattern];
|
|
11445
|
+
const match = /\{([^{}]+)\}/.exec(pattern);
|
|
11446
|
+
if (!match) return [pattern];
|
|
11447
|
+
const [fullMatch, options] = match;
|
|
11448
|
+
const parts = options.split(",");
|
|
11449
|
+
const prefix = pattern.slice(0, match.index);
|
|
11450
|
+
const suffix = pattern.slice(match.index + fullMatch.length);
|
|
11451
|
+
return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
|
|
11452
|
+
};
|
|
11279
11453
|
const buildDotnetFormatExcludeScope = (excludePatterns) => {
|
|
11280
11454
|
const scope = {
|
|
11281
11455
|
excludeArguments: [],
|
|
11282
11456
|
unsupportedPatterns: []
|
|
11283
11457
|
};
|
|
11284
11458
|
for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
|
|
11285
|
-
const expanded =
|
|
11459
|
+
const expanded = expandBraces(pattern);
|
|
11286
11460
|
if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
|
|
11287
11461
|
else scope.unsupportedPatterns.push(pattern);
|
|
11288
11462
|
}
|
|
@@ -11507,7 +11681,7 @@ const runGofmt = async (context) => {
|
|
|
11507
11681
|
rule: "go-formatting",
|
|
11508
11682
|
severity: "warning",
|
|
11509
11683
|
message: "Go file is not formatted correctly",
|
|
11510
|
-
help: "Run `
|
|
11684
|
+
help: "Run `raigal fix` to auto-format with gofmt",
|
|
11511
11685
|
line: 0,
|
|
11512
11686
|
column: 0,
|
|
11513
11687
|
category: "Format",
|
|
@@ -11577,7 +11751,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
11577
11751
|
rule: "python-formatting",
|
|
11578
11752
|
severity: "warning",
|
|
11579
11753
|
message: "Python file is not formatted correctly",
|
|
11580
|
-
help: "Run `
|
|
11754
|
+
help: "Run `raigal fix` to auto-format with ruff",
|
|
11581
11755
|
line: 0,
|
|
11582
11756
|
column: 0,
|
|
11583
11757
|
category: "Format",
|
|
@@ -11589,7 +11763,8 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
11589
11763
|
const fixRuffFormat = async (context) => {
|
|
11590
11764
|
const targets = context.files ? getPythonTargets(context) : [context.rootDirectory];
|
|
11591
11765
|
if (context.files && targets.length === 0) return;
|
|
11592
|
-
const
|
|
11766
|
+
const ruffBinary = resolveToolBinary("ruff");
|
|
11767
|
+
const result = await runSubprocess(ruffBinary, ["format", ...targets], {
|
|
11593
11768
|
cwd: context.rootDirectory,
|
|
11594
11769
|
timeout: 6e4
|
|
11595
11770
|
});
|
|
@@ -11659,7 +11834,8 @@ const parseClangTidyOutput = (output, rootDirectory) => {
|
|
|
11659
11834
|
const runClangTidy = async (context) => {
|
|
11660
11835
|
const compileCommandsDir = findCompileCommandsDir(context);
|
|
11661
11836
|
if (!compileCommandsDir) return [];
|
|
11662
|
-
const
|
|
11837
|
+
const implSources = findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase()));
|
|
11838
|
+
const sources = filterSourcesInDatabase(implSources, readCompileCommandsFiles(compileCommandsDir));
|
|
11663
11839
|
if (sources.length === 0) return [];
|
|
11664
11840
|
const diagnostics = [];
|
|
11665
11841
|
for (const chunk of chunkFilePaths(sources, CLANG_TIDY_CHUNK_MAX_FILES)) try {
|
|
@@ -12029,7 +12205,7 @@ const runExpoDoctor = async (context) => {
|
|
|
12029
12205
|
rule: "expo-doctor/config-error",
|
|
12030
12206
|
severity: "warning",
|
|
12031
12207
|
message: configError,
|
|
12032
|
-
help: "Install project dependencies, then re-run `
|
|
12208
|
+
help: "Install project dependencies, then re-run `raigal scan`.",
|
|
12033
12209
|
line: 0,
|
|
12034
12210
|
column: 0,
|
|
12035
12211
|
category: "Expo",
|
|
@@ -12495,7 +12671,8 @@ const processFile = (filePath, fileTargets, result) => {
|
|
|
12495
12671
|
});
|
|
12496
12672
|
continue;
|
|
12497
12673
|
}
|
|
12498
|
-
const
|
|
12674
|
+
const shape = classifyIdentifier(best);
|
|
12675
|
+
const { edit, skipReason } = computeEdit(sourceFile, shape);
|
|
12499
12676
|
if (!edit) {
|
|
12500
12677
|
pendingSkips.push({
|
|
12501
12678
|
target,
|
|
@@ -13076,7 +13253,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
|
|
|
13076
13253
|
if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
|
|
13077
13254
|
return [pattern];
|
|
13078
13255
|
});
|
|
13079
|
-
const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 &&
|
|
13256
|
+
const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
|
|
13080
13257
|
const getOxlintTargets = (context) => {
|
|
13081
13258
|
const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
|
|
13082
13259
|
return getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/")).filter((filePath) => !isEslintIgnored(filePath, eslintIgnorePatterns));
|
|
@@ -13310,7 +13487,8 @@ const ruffLintFixArgs = (context, unsafe) => {
|
|
|
13310
13487
|
};
|
|
13311
13488
|
const fixRuffLint = async (context) => {
|
|
13312
13489
|
if (context.files && getPythonTargets(context).length === 0) return;
|
|
13313
|
-
const
|
|
13490
|
+
const ruffBinary = resolveToolBinary("ruff");
|
|
13491
|
+
const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, false), {
|
|
13314
13492
|
cwd: context.rootDirectory,
|
|
13315
13493
|
timeout: 6e4
|
|
13316
13494
|
});
|
|
@@ -13318,7 +13496,8 @@ const fixRuffLint = async (context) => {
|
|
|
13318
13496
|
};
|
|
13319
13497
|
const fixRuffLintForce = async (context) => {
|
|
13320
13498
|
if (context.files && getPythonTargets(context).length === 0) return;
|
|
13321
|
-
const
|
|
13499
|
+
const ruffBinary = resolveToolBinary("ruff");
|
|
13500
|
+
const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, true), {
|
|
13322
13501
|
cwd: context.rootDirectory,
|
|
13323
13502
|
timeout: 6e4
|
|
13324
13503
|
});
|
|
@@ -13407,6 +13586,174 @@ const lintEngine = {
|
|
|
13407
13586
|
}
|
|
13408
13587
|
};
|
|
13409
13588
|
//#endregion
|
|
13589
|
+
//#region src/engines/security/ai-security.ts
|
|
13590
|
+
const JS_EXTS = /* @__PURE__ */ new Set([
|
|
13591
|
+
".ts",
|
|
13592
|
+
".tsx",
|
|
13593
|
+
".js",
|
|
13594
|
+
".jsx",
|
|
13595
|
+
".mjs",
|
|
13596
|
+
".cjs"
|
|
13597
|
+
]);
|
|
13598
|
+
const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
|
|
13599
|
+
const UNICODE_TAG_RE = /[\u{E0000}-\u{E007F}]/gu;
|
|
13600
|
+
const hasRuleIgnore = (lines, lineIndex, ruleSuffix) => {
|
|
13601
|
+
const start = Math.max(0, lineIndex - 2);
|
|
13602
|
+
const re = new RegExp(`(?:raigal|aislop)[- ]ignore(?:-file)?\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
|
|
13603
|
+
return lines.slice(start, lineIndex + 1).some((line) => re.test(line));
|
|
13604
|
+
};
|
|
13605
|
+
const hasFileIgnore = (lines, ruleSuffix) => {
|
|
13606
|
+
const re = new RegExp(`(?:raigal|aislop)[- ]ignore-file\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
|
|
13607
|
+
return lines.slice(0, 10).some((line) => re.test(line));
|
|
13608
|
+
};
|
|
13609
|
+
const makeDiagnostic = (filePath, rule, message, help, line, column) => ({
|
|
13610
|
+
filePath,
|
|
13611
|
+
engine: "security",
|
|
13612
|
+
rule,
|
|
13613
|
+
severity: "error",
|
|
13614
|
+
message,
|
|
13615
|
+
help,
|
|
13616
|
+
line,
|
|
13617
|
+
column,
|
|
13618
|
+
category: "Security",
|
|
13619
|
+
fixable: false
|
|
13620
|
+
});
|
|
13621
|
+
const P_BYPASS = "bypassPermissions";
|
|
13622
|
+
const P_EXEC_TYPE = "executor_type";
|
|
13623
|
+
const P_LOCAL = "local";
|
|
13624
|
+
const P_USE_DOCKER = "use_docker";
|
|
13625
|
+
const P_AUTO_RUN = "auto_run";
|
|
13626
|
+
const P_UNSAFE_MODE = "unsafe_mode";
|
|
13627
|
+
const P_PYTHON_REPL = "PythonREPLTool";
|
|
13628
|
+
const UNSANDBOXED_TOOL_PATTERNS = [
|
|
13629
|
+
{
|
|
13630
|
+
pattern: new RegExp(`\\bpermission_?mode["']?\\s*[:=]\\s*["']${P_BYPASS}["']`, "i"),
|
|
13631
|
+
message: `permission_mode '${P_BYPASS}' bypasses agent sandboxing and human confirmation`,
|
|
13632
|
+
help: `Do not use ${P_BYPASS} mode in production; use standard interactive approval`
|
|
13633
|
+
},
|
|
13634
|
+
{
|
|
13635
|
+
pattern: new RegExp(`\\b${P_EXEC_TYPE}["']?\\s*[:=]\\s*["']${P_LOCAL}["']`, "i"),
|
|
13636
|
+
message: `${P_EXEC_TYPE}='${P_LOCAL}' executes LLM-generated code directly on host machine without container isolation`,
|
|
13637
|
+
help: "Configure agent to use DockerCommandLineCodeExecutor or a containerized sandbox environment"
|
|
13638
|
+
},
|
|
13639
|
+
{
|
|
13640
|
+
pattern: new RegExp(`\\b(?:LocalCommandLineCodeExecutor|LocalCodeExecutor)\\s*\\(`),
|
|
13641
|
+
message: "Local code executor executes LLM-generated code on host machine without container isolation",
|
|
13642
|
+
help: "Use DockerCommandLineCodeExecutor or an isolated execution sandbox"
|
|
13643
|
+
},
|
|
13644
|
+
{
|
|
13645
|
+
pattern: new RegExp(`\\b${P_USE_DOCKER}\\s*[:=]\\s*(?:False|false)\\b|["']${P_USE_DOCKER}["']\\s*:\\s*(?:False|false)\\b`),
|
|
13646
|
+
message: `${P_USE_DOCKER}=False disables container sandboxing for agent code execution`,
|
|
13647
|
+
help: `Set ${P_USE_DOCKER}=True to isolate agent execution inside Docker containers`
|
|
13648
|
+
},
|
|
13649
|
+
{
|
|
13650
|
+
pattern: new RegExp(`\\b(?:interpreter\\.)?${P_AUTO_RUN}\\s*[:=]\\s*(?:True|true)\\b`),
|
|
13651
|
+
message: `${P_AUTO_RUN}=True allows autonomous shell execution without human confirmation`,
|
|
13652
|
+
help: `Keep ${P_AUTO_RUN}=False so dangerous commands require explicit human review`
|
|
13653
|
+
},
|
|
13654
|
+
{
|
|
13655
|
+
pattern: new RegExp(`\\bCodeInterpreterTool\\s*\\([^)]*${P_UNSAFE_MODE}\\s*=\\s*(?:True|true)\\b|\\b${P_UNSAFE_MODE}\\s*[:=]\\s*(?:True|true)\\b`),
|
|
13656
|
+
message: `${P_UNSAFE_MODE}=True allows code interpreter tool to execute without Docker isolation`,
|
|
13657
|
+
help: `Disable ${P_UNSAFE_MODE} and isolate code execution in containerized environments`
|
|
13658
|
+
},
|
|
13659
|
+
{
|
|
13660
|
+
pattern: new RegExp(`\\b(?:${P_PYTHON_REPL}|PythonAstREPLTool)\\s*\\(`),
|
|
13661
|
+
message: `${P_PYTHON_REPL} executes arbitrary Python code in host process without container isolation`,
|
|
13662
|
+
help: `Use containerized sandbox execution rather than unconfined ${P_PYTHON_REPL} in production`
|
|
13663
|
+
}
|
|
13664
|
+
];
|
|
13665
|
+
const P_SYS = "system";
|
|
13666
|
+
const P_DEV = "developer";
|
|
13667
|
+
const P_SM = "SystemMessage";
|
|
13668
|
+
const PROMPT_INJECTION_JS_PATTERNS = [
|
|
13669
|
+
new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
|
|
13670
|
+
new RegExp(`(?:content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
|
|
13671
|
+
new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']))`, "g"),
|
|
13672
|
+
new RegExp(`(?:content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
|
|
13673
|
+
new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\``, "g"),
|
|
13674
|
+
new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])`, "g"),
|
|
13675
|
+
new RegExp(`\\b(?:new\\s+)?${P_SM}\\s*\\(\\s*(?:\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`|["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']|\\{\\s*content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
|
|
13676
|
+
new RegExp(`\\[\\s*["']${P_SYS}["']\\s*,\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`\\s*\\]`, "g")
|
|
13677
|
+
];
|
|
13678
|
+
const PROMPT_INJECTION_PY_PATTERNS = [
|
|
13679
|
+
new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["']))`, "g"),
|
|
13680
|
+
new RegExp(`(?:["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
|
|
13681
|
+
new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\())`, "g"),
|
|
13682
|
+
new RegExp(`(?:["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
|
|
13683
|
+
new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
|
|
13684
|
+
new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
|
|
13685
|
+
new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
|
|
13686
|
+
new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
|
|
13687
|
+
new RegExp(`\\(\\s*["']${P_SYS}["']\\s*,\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])\\s*\\)`, "g")
|
|
13688
|
+
];
|
|
13689
|
+
const checkInvisibleUnicode = (relPath, content, lines) => {
|
|
13690
|
+
if (hasFileIgnore(lines, "invisible-unicode-tags")) return [];
|
|
13691
|
+
const results = [];
|
|
13692
|
+
const tagRegex = new RegExp(UNICODE_TAG_RE.source, UNICODE_TAG_RE.flags);
|
|
13693
|
+
for (const match of content.matchAll(tagRegex)) {
|
|
13694
|
+
const matchIndex = match.index ?? 0;
|
|
13695
|
+
const line = content.slice(0, matchIndex).split("\n").length;
|
|
13696
|
+
const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
|
|
13697
|
+
if (hasRuleIgnore(lines, line - 1, "invisible-unicode-tags")) continue;
|
|
13698
|
+
results.push(makeDiagnostic(relPath, "security/invisible-unicode-tags", "Invisible Unicode Tag character (U+E0000-U+E007F) detected: potential stealth prompt injection or ASCII smuggling", "Remove invisible Unicode tag characters. Unicode Tag block (U+E0000-U+E007F) has no legitimate printable presentation in source code", line, col));
|
|
13699
|
+
}
|
|
13700
|
+
return results;
|
|
13701
|
+
};
|
|
13702
|
+
const checkUnsandboxedTools = (relPath, content, lines) => {
|
|
13703
|
+
if (hasFileIgnore(lines, "unsandboxed-agent-tool")) return [];
|
|
13704
|
+
const results = [];
|
|
13705
|
+
for (const { pattern, message, help } of UNSANDBOXED_TOOL_PATTERNS) {
|
|
13706
|
+
const regex = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
13707
|
+
for (const match of content.matchAll(regex)) {
|
|
13708
|
+
const matchIndex = match.index ?? 0;
|
|
13709
|
+
const line = content.slice(0, matchIndex).split("\n").length;
|
|
13710
|
+
const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
|
|
13711
|
+
if (hasRuleIgnore(lines, line - 1, "unsandboxed-agent-tool")) continue;
|
|
13712
|
+
results.push(makeDiagnostic(relPath, "security/unsandboxed-agent-tool", message, help, line, col));
|
|
13713
|
+
}
|
|
13714
|
+
}
|
|
13715
|
+
return results;
|
|
13716
|
+
};
|
|
13717
|
+
const checkPromptInjection = (relPath, content, lines, isJs) => {
|
|
13718
|
+
if (hasFileIgnore(lines, "prompt-injection")) return [];
|
|
13719
|
+
const results = [];
|
|
13720
|
+
const patterns = isJs ? PROMPT_INJECTION_JS_PATTERNS : PROMPT_INJECTION_PY_PATTERNS;
|
|
13721
|
+
for (const pattern of patterns) {
|
|
13722
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
13723
|
+
for (const match of content.matchAll(regex)) {
|
|
13724
|
+
const matchIndex = match.index ?? 0;
|
|
13725
|
+
const line = content.slice(0, matchIndex).split("\n").length;
|
|
13726
|
+
const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
|
|
13727
|
+
if (hasRuleIgnore(lines, line - 1, "prompt-injection")) continue;
|
|
13728
|
+
results.push(makeDiagnostic(relPath, "security/prompt-injection", "Untrusted variable interpolation into system prompt creates prompt injection vulnerability", "Keep system prompts static. Pass dynamic user inputs via user messages or input parameters with strict delimiters", line, col));
|
|
13729
|
+
}
|
|
13730
|
+
}
|
|
13731
|
+
return results;
|
|
13732
|
+
};
|
|
13733
|
+
const scanAiSecurity = async (context) => {
|
|
13734
|
+
const files = getSourceFiles(context);
|
|
13735
|
+
const diagnostics = [];
|
|
13736
|
+
for (const filePath of files) {
|
|
13737
|
+
const ext = path.extname(filePath);
|
|
13738
|
+
let content;
|
|
13739
|
+
try {
|
|
13740
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
13741
|
+
} catch {
|
|
13742
|
+
continue;
|
|
13743
|
+
}
|
|
13744
|
+
const relativePath = relativePosix(context.rootDirectory, filePath);
|
|
13745
|
+
const lines = content.split("\n");
|
|
13746
|
+
diagnostics.push(...checkInvisibleUnicode(relativePath, content, lines));
|
|
13747
|
+
const isJs = JS_EXTS.has(ext);
|
|
13748
|
+
const isPy = PY_EXTS.has(ext);
|
|
13749
|
+
if (isJs || isPy) {
|
|
13750
|
+
diagnostics.push(...checkUnsandboxedTools(relativePath, content, lines));
|
|
13751
|
+
diagnostics.push(...checkPromptInjection(relativePath, content, lines, isJs));
|
|
13752
|
+
}
|
|
13753
|
+
}
|
|
13754
|
+
return diagnostics;
|
|
13755
|
+
};
|
|
13756
|
+
//#endregion
|
|
13410
13757
|
//#region src/engines/security/audit-value.ts
|
|
13411
13758
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13412
13759
|
const readString = (record, key) => {
|
|
@@ -13786,7 +14133,7 @@ const parseJsAudit = (output, source, manifest) => {
|
|
|
13786
14133
|
rule: "security/dependency-audit-skipped",
|
|
13787
14134
|
severity: "info",
|
|
13788
14135
|
message: `Dependency audit skipped (${source}): lockfile is missing`,
|
|
13789
|
-
help: errorDetail ?? "Generate a lockfile, then re-run `
|
|
14136
|
+
help: errorDetail ?? "Generate a lockfile, then re-run `raigal scan` for dependency vulnerability checks.",
|
|
13790
14137
|
line: 0,
|
|
13791
14138
|
column: 0,
|
|
13792
14139
|
category: "Security",
|
|
@@ -13897,7 +14244,8 @@ const readRuntimeDependencies = (rootDir) => {
|
|
|
13897
14244
|
collectRuntimeNames(rootManifest, names);
|
|
13898
14245
|
const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
|
|
13899
14246
|
if (globs.length === 0) return { runtimeDependencies: names };
|
|
13900
|
-
|
|
14247
|
+
const isMatch = picomatch(globs, { dot: true });
|
|
14248
|
+
for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
|
|
13901
14249
|
const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
|
|
13902
14250
|
if (memberManifest) collectRuntimeNames(memberManifest, names);
|
|
13903
14251
|
}
|
|
@@ -13959,10 +14307,11 @@ const auditSkippedDiagnostic = (source, help) => ({
|
|
|
13959
14307
|
});
|
|
13960
14308
|
const runNpmAudit = async (rootDir, timeout) => {
|
|
13961
14309
|
try {
|
|
13962
|
-
|
|
14310
|
+
const result = await runSubprocess("npm", ["audit", "--json"], {
|
|
13963
14311
|
cwd: rootDir,
|
|
13964
14312
|
timeout
|
|
13965
|
-
})
|
|
14313
|
+
});
|
|
14314
|
+
return parseJsAudit(result.stdout, "npm audit", readRuntimeDependencies(rootDir));
|
|
13966
14315
|
} catch (error) {
|
|
13967
14316
|
return [auditSkippedDiagnostic("npm audit", `Failed to run npm audit: ${errorMessageOf(error)}`)];
|
|
13968
14317
|
}
|
|
@@ -13983,10 +14332,11 @@ const runBunAudit = async (rootDir, timeout) => {
|
|
|
13983
14332
|
const runPnpmAuditWithFallback = async (rootDir, timeout) => {
|
|
13984
14333
|
const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
|
|
13985
14334
|
try {
|
|
13986
|
-
const
|
|
14335
|
+
const result = await runSubprocess("pnpm", ["audit", "--json"], {
|
|
13987
14336
|
cwd: rootDir,
|
|
13988
14337
|
timeout
|
|
13989
|
-
})
|
|
14338
|
+
});
|
|
14339
|
+
const diagnostics = parseJsAudit(result.stdout, "pnpm audit", readRuntimeDependencies(rootDir));
|
|
13990
14340
|
if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
|
|
13991
14341
|
if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
|
|
13992
14342
|
return diagnostics;
|
|
@@ -14343,7 +14693,7 @@ const RISKY_PATTERNS = [
|
|
|
14343
14693
|
//#region src/engines/security/risky.ts
|
|
14344
14694
|
const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
|
|
14345
14695
|
const start = Math.max(0, lineIndex - 2);
|
|
14346
|
-
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
14696
|
+
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|raigal-ignore|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
14347
14697
|
};
|
|
14348
14698
|
const isStructuredDataScript = (content, matchIndex) => {
|
|
14349
14699
|
const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
|
|
@@ -14671,7 +15021,11 @@ const ALL_ENGINES = [
|
|
|
14671
15021
|
name: "security",
|
|
14672
15022
|
async run(context) {
|
|
14673
15023
|
const diagnostics = [];
|
|
14674
|
-
const promises = [
|
|
15024
|
+
const promises = [
|
|
15025
|
+
scanSecrets(context),
|
|
15026
|
+
detectRiskyConstructs(context),
|
|
15027
|
+
scanAiSecurity(context)
|
|
15028
|
+
];
|
|
14675
15029
|
if (context.config.security.audit) promises.push(runDependencyAudit(context));
|
|
14676
15030
|
const results = await Promise.allSettled(promises);
|
|
14677
15031
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -14693,6 +15047,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14693
15047
|
const result = await engine.run(context);
|
|
14694
15048
|
result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
|
|
14695
15049
|
result.elapsed = performance.now() - start;
|
|
15050
|
+
result.diagnostics = applyFileOverrides(result.diagnostics, context);
|
|
14696
15051
|
onComplete?.(result);
|
|
14697
15052
|
return result;
|
|
14698
15053
|
} catch (error) {
|
|
@@ -14701,6 +15056,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14701
15056
|
diagnostics: [],
|
|
14702
15057
|
elapsed: performance.now() - start,
|
|
14703
15058
|
skipped: true,
|
|
15059
|
+
failed: true,
|
|
14704
15060
|
skipReason: error instanceof Error ? error.message : String(error)
|
|
14705
15061
|
};
|
|
14706
15062
|
onComplete?.(result);
|
|
@@ -14711,9 +15067,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14711
15067
|
diagnostics: [],
|
|
14712
15068
|
elapsed: 0,
|
|
14713
15069
|
skipped: true,
|
|
15070
|
+
failed: true,
|
|
14714
15071
|
skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
14715
15072
|
});
|
|
14716
|
-
|
|
15073
|
+
const stripped = stripSecretDiagnostics(finalResults);
|
|
15074
|
+
const csharp = dedupeCSharpAsync(stripped);
|
|
15075
|
+
const comments = dedupeOverlappingComments(csharp);
|
|
15076
|
+
return dedupeEquivalenceDiagnostics(comments);
|
|
14717
15077
|
};
|
|
14718
15078
|
const stripSecretDiagnostics = (results) => {
|
|
14719
15079
|
for (const result of results) for (const d of result.diagnostics) if (d.redactSource || isSecretClassRule(d.rule)) {
|
|
@@ -14762,7 +15122,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
14762
15122
|
const capScale = Math.max(1, fileCount.effectiveSourceFileCount / CAP_REFERENCE_FILES);
|
|
14763
15123
|
const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
|
|
14764
15124
|
const capForRule = (key) => {
|
|
14765
|
-
const
|
|
15125
|
+
const rule = key.slice(key.indexOf(":") + 1);
|
|
15126
|
+
const ruleCap = scoreImpactForRule(rule).cap;
|
|
14766
15127
|
if (typeof ruleCap === "number") return defaultRuleCap ? Math.min(defaultRuleCap, ruleCap) : ruleCap;
|
|
14767
15128
|
return defaultRuleCap;
|
|
14768
15129
|
};
|
|
@@ -14929,6 +15290,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
|
14929
15290
|
"exit_code",
|
|
14930
15291
|
"duration_ms",
|
|
14931
15292
|
"error_kind",
|
|
15293
|
+
"error_name",
|
|
15294
|
+
"error_code",
|
|
15295
|
+
"failed_stage",
|
|
15296
|
+
"engines_failed",
|
|
14932
15297
|
"score",
|
|
14933
15298
|
"score_bucket",
|
|
14934
15299
|
"finding_count",
|
|
@@ -15042,10 +15407,11 @@ const track = (input) => {
|
|
|
15042
15407
|
cachedInstallId = ensured.installId;
|
|
15043
15408
|
installCreated = ensured.created;
|
|
15044
15409
|
}
|
|
15045
|
-
const
|
|
15410
|
+
const merged = {
|
|
15046
15411
|
...baseProperties(cachedInstallId),
|
|
15047
15412
|
...input.properties
|
|
15048
|
-
}
|
|
15413
|
+
};
|
|
15414
|
+
const { clean, dropped } = redactProperties(merged);
|
|
15049
15415
|
if (isDebug()) {
|
|
15050
15416
|
const compact = JSON.stringify({
|
|
15051
15417
|
event: input.event,
|
|
@@ -15157,6 +15523,8 @@ const buildCommandCompletedProps = (input) => {
|
|
|
15157
15523
|
if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
|
|
15158
15524
|
if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
|
|
15159
15525
|
if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
|
|
15526
|
+
if (input.errorName) props.error_name = input.errorName;
|
|
15527
|
+
if (input.errorCode) props.error_code = input.errorCode;
|
|
15160
15528
|
return props;
|
|
15161
15529
|
};
|
|
15162
15530
|
const errorKindFromException = (error) => {
|
|
@@ -15166,6 +15534,26 @@ const errorKindFromException = (error) => {
|
|
|
15166
15534
|
if (message.includes("engine") && message.includes("crash")) return "engine_crash";
|
|
15167
15535
|
return "unknown";
|
|
15168
15536
|
};
|
|
15537
|
+
const SAFE_ERROR_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
|
|
15538
|
+
const SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
|
|
15539
|
+
const errorIdentity = (error) => {
|
|
15540
|
+
let name;
|
|
15541
|
+
if (error instanceof Error && typeof error.name === "string" && error.name.length > 0) name = error.name;
|
|
15542
|
+
else if (error === null) name = "null";
|
|
15543
|
+
else if (typeof error === "object") name = "object";
|
|
15544
|
+
else name = typeof error;
|
|
15545
|
+
const identity = { error_name: SAFE_ERROR_NAME.test(name) ? name : "Error" };
|
|
15546
|
+
const code = error?.code;
|
|
15547
|
+
if (typeof code === "string" && SAFE_ERROR_CODE.test(code)) identity.error_code = code;
|
|
15548
|
+
else if (typeof code === "number" && Number.isFinite(code)) identity.error_code = String(code);
|
|
15549
|
+
return identity;
|
|
15550
|
+
};
|
|
15551
|
+
//#endregion
|
|
15552
|
+
//#region src/telemetry/fatal.ts
|
|
15553
|
+
const reportedErrors = /* @__PURE__ */ new Set();
|
|
15554
|
+
const markErrorReported = (error) => {
|
|
15555
|
+
reportedErrors.add(error);
|
|
15556
|
+
};
|
|
15169
15557
|
//#endregion
|
|
15170
15558
|
//#region src/telemetry/lifecycle.ts
|
|
15171
15559
|
const STARTED_FLUSH_TIMEOUT_MS = 100;
|
|
@@ -15176,6 +15564,7 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15176
15564
|
fileCount: start.fileCount,
|
|
15177
15565
|
properties: start.properties
|
|
15178
15566
|
});
|
|
15567
|
+
start.config;
|
|
15179
15568
|
track({
|
|
15180
15569
|
event: "cli_command_started",
|
|
15181
15570
|
properties: startProps,
|
|
@@ -15192,6 +15581,7 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15192
15581
|
startProps,
|
|
15193
15582
|
exitCode: result.exitCode,
|
|
15194
15583
|
durationMs,
|
|
15584
|
+
errorKind: result.errorKind,
|
|
15195
15585
|
score: result.score ?? void 0,
|
|
15196
15586
|
findingCount: result.findingCount,
|
|
15197
15587
|
errorCount: result.errorCount,
|
|
@@ -15209,16 +15599,21 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15209
15599
|
await flushTelemetry();
|
|
15210
15600
|
return result;
|
|
15211
15601
|
} catch (error) {
|
|
15602
|
+
const durationMs = performance.now() - startedAt;
|
|
15603
|
+
const identity = errorIdentity(error);
|
|
15212
15604
|
track({
|
|
15213
15605
|
event: "cli_command_completed",
|
|
15214
15606
|
properties: buildCommandCompletedProps({
|
|
15215
15607
|
startProps,
|
|
15216
15608
|
exitCode: 1,
|
|
15217
|
-
durationMs
|
|
15218
|
-
errorKind: errorKindFromException(error)
|
|
15609
|
+
durationMs,
|
|
15610
|
+
errorKind: errorKindFromException(error),
|
|
15611
|
+
errorName: identity.error_name,
|
|
15612
|
+
errorCode: identity.error_code
|
|
15219
15613
|
}),
|
|
15220
15614
|
config: start.config
|
|
15221
15615
|
});
|
|
15616
|
+
markErrorReported(error);
|
|
15222
15617
|
await flushTelemetry();
|
|
15223
15618
|
throw error;
|
|
15224
15619
|
}
|
|
@@ -15296,14 +15691,17 @@ const renderRailStep = (step, deps = {}) => {
|
|
|
15296
15691
|
* between the last step and the footer.
|
|
15297
15692
|
*/
|
|
15298
15693
|
const renderRailConnector = (deps = {}) => {
|
|
15299
|
-
|
|
15694
|
+
const t = deps.theme ?? theme;
|
|
15695
|
+
const s = deps.symbols ?? symbols;
|
|
15696
|
+
return ` ${style(t, "accentDim", s.rail)}\n`;
|
|
15300
15697
|
};
|
|
15301
15698
|
/**
|
|
15302
15699
|
* Render the rail-end footer line ("└ <footer text>").
|
|
15303
15700
|
*/
|
|
15304
15701
|
const renderRailFooter = (footer, deps = {}) => {
|
|
15305
15702
|
const t = deps.theme ?? theme;
|
|
15306
|
-
|
|
15703
|
+
const s = deps.symbols ?? symbols;
|
|
15704
|
+
return ` ${style(t, "accentDim", s.railEnd)} ${highlightAislop(footer, t)}\n`;
|
|
15307
15705
|
};
|
|
15308
15706
|
const renderRail = (input, deps = {}) => {
|
|
15309
15707
|
const t = deps.theme ?? theme;
|
|
@@ -15430,7 +15828,8 @@ var LiveRail = class {
|
|
|
15430
15828
|
*/
|
|
15431
15829
|
const renderHintLine = (hint, deps = {}) => {
|
|
15432
15830
|
const t = deps.theme ?? theme;
|
|
15433
|
-
|
|
15831
|
+
const s = deps.symbols ?? symbols;
|
|
15832
|
+
return ` ${style(t, "accent", s.hint)} ${highlightAislop(maskSecrets(hint), t)}\n`;
|
|
15434
15833
|
};
|
|
15435
15834
|
const createLogger = (deps = {}) => {
|
|
15436
15835
|
const t = deps.theme ?? theme;
|
|
@@ -15575,7 +15974,8 @@ const groupByFile = (diagnostics) => {
|
|
|
15575
15974
|
});
|
|
15576
15975
|
};
|
|
15577
15976
|
const isInstalled = (bin) => {
|
|
15578
|
-
|
|
15977
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
15978
|
+
return spawnSync(cmd, [bin], { encoding: "utf-8" }).status === 0;
|
|
15579
15979
|
};
|
|
15580
15980
|
const copyToClipboard = (text) => {
|
|
15581
15981
|
const args = {
|
|
@@ -15714,7 +16114,9 @@ const createEngineContext = (rootDirectory, projectInfo, config, options = {}) =
|
|
|
15714
16114
|
quality: config.quality,
|
|
15715
16115
|
security: config.security,
|
|
15716
16116
|
lint: config.lint,
|
|
15717
|
-
engines: config.engines
|
|
16117
|
+
engines: config.engines,
|
|
16118
|
+
overrides: config.overrides,
|
|
16119
|
+
rules: config.rules
|
|
15718
16120
|
},
|
|
15719
16121
|
...options.scope ? {
|
|
15720
16122
|
files: options.scope.files,
|
|
@@ -16056,7 +16458,9 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
16056
16458
|
if (!header.kid) throw new JwtVerificationError("JWT header missing 'kid'", "unknown_kid");
|
|
16057
16459
|
const publicKey = getPublicKey(header.kid);
|
|
16058
16460
|
if (!publicKey) throw new JwtVerificationError(`Unknown or untrusted signing key id (kid: ${header.kid})`, "unknown_kid");
|
|
16059
|
-
|
|
16461
|
+
const signedData = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
|
|
16462
|
+
const signature = Buffer.from(signatureB64, "base64url");
|
|
16463
|
+
if (!verify(null, signedData, publicKey, signature)) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
|
|
16060
16464
|
let rawPayload;
|
|
16061
16465
|
try {
|
|
16062
16466
|
rawPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf-8"));
|
|
@@ -16068,7 +16472,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
16068
16472
|
const claims = parsed.data;
|
|
16069
16473
|
const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
|
|
16070
16474
|
if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
|
|
16071
|
-
if (!isVersionAtLeast("0.2.
|
|
16475
|
+
if (!isVersionAtLeast("0.2.3", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
|
|
16072
16476
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
16073
16477
|
const grace = options.allowGraceSeconds ?? 0;
|
|
16074
16478
|
if (now > claims.exp + grace) {
|
|
@@ -16102,7 +16506,7 @@ const fetchGitHubOidcToken = async () => {
|
|
|
16102
16506
|
};
|
|
16103
16507
|
//#endregion
|
|
16104
16508
|
//#region src/cloud/entitlement.ts
|
|
16105
|
-
const GRACE_PERIOD_SECONDS =
|
|
16509
|
+
const GRACE_PERIOD_SECONDS = 259200;
|
|
16106
16510
|
var MissingCredentialError = class extends Error {
|
|
16107
16511
|
constructor(message = "No organization credential found") {
|
|
16108
16512
|
super(message);
|
|
@@ -16182,6 +16586,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
|
|
|
16182
16586
|
nowSeconds: now,
|
|
16183
16587
|
allowGraceSeconds: GRACE_PERIOD_SECONDS
|
|
16184
16588
|
});
|
|
16589
|
+
if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
|
|
16590
|
+
clearCachedEntitlement();
|
|
16591
|
+
throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
|
|
16592
|
+
}
|
|
16185
16593
|
return {
|
|
16186
16594
|
jwt: cached.entitlement,
|
|
16187
16595
|
claims: graceClaims,
|
|
@@ -16189,6 +16597,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
|
|
|
16189
16597
|
grace: now > graceClaims.exp
|
|
16190
16598
|
};
|
|
16191
16599
|
} catch (jwtErr) {
|
|
16600
|
+
if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
|
|
16192
16601
|
if (jwtErr instanceof JwtVerificationError && jwtErr.code === "expired_beyond_grace") throw new LicenceUnverifiableError(`Raigal license could not be verified online and the offline grace period expired on ${(/* @__PURE__ */ new Date((cachedClaims?.exp ?? 0) * 1e3)).toISOString()}. Check network connectivity or Raigal Cloud status.`);
|
|
16193
16602
|
}
|
|
16194
16603
|
throw new LicenceUnverifiableError(`Raigal license could not be verified online (${err instanceof Error ? err.message : String(err)}). Check network connectivity or Raigal Cloud status.`);
|
|
@@ -16203,7 +16612,8 @@ const getEntitlement = async () => {
|
|
|
16203
16612
|
} catch {
|
|
16204
16613
|
cachedClaims = null;
|
|
16205
16614
|
}
|
|
16206
|
-
|
|
16615
|
+
const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
|
|
16616
|
+
if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
|
|
16207
16617
|
jwt: cached.entitlement,
|
|
16208
16618
|
claims: cachedClaims,
|
|
16209
16619
|
fromCache: true
|
|
@@ -16212,7 +16622,7 @@ const getEntitlement = async () => {
|
|
|
16212
16622
|
if (!activeToken) {
|
|
16213
16623
|
oidcToken = await fetchGitHubOidcToken();
|
|
16214
16624
|
if (!oidcToken) {
|
|
16215
|
-
if (cached && cachedClaims) return {
|
|
16625
|
+
if (cached && cachedClaims && !isTrialExpired) return {
|
|
16216
16626
|
jwt: cached.entitlement,
|
|
16217
16627
|
claims: cachedClaims,
|
|
16218
16628
|
fromCache: true
|
|
@@ -16221,7 +16631,8 @@ const getEntitlement = async () => {
|
|
|
16221
16631
|
}
|
|
16222
16632
|
} else oidcToken = await fetchGitHubOidcToken();
|
|
16223
16633
|
try {
|
|
16224
|
-
const
|
|
16634
|
+
const tokenToPass = activeToken?.token ?? "";
|
|
16635
|
+
const response = await fetchEntitlementFromApi(tokenToPass, oidcToken);
|
|
16225
16636
|
const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
|
|
16226
16637
|
saveCachedEntitlement(response.entitlement, response.refresh_after);
|
|
16227
16638
|
return {
|
|
@@ -16235,7 +16646,7 @@ const getEntitlement = async () => {
|
|
|
16235
16646
|
};
|
|
16236
16647
|
//#endregion
|
|
16237
16648
|
//#region src/cloud/owner.ts
|
|
16238
|
-
const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([
|
|
16649
|
+
const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
|
|
16239
16650
|
const parseGitRemote = (rawUrl) => {
|
|
16240
16651
|
if (!rawUrl) return {
|
|
16241
16652
|
owner: null,
|
|
@@ -16373,6 +16784,23 @@ const requireEntitlement = async (options = {}) => {
|
|
|
16373
16784
|
try {
|
|
16374
16785
|
const entitlement = await getEntitlement();
|
|
16375
16786
|
const tokenKind = getActiveToken()?.kind ?? "session";
|
|
16787
|
+
if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
|
|
16788
|
+
const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
|
|
16789
|
+
if (Date.now() > trialEnd) {
|
|
16790
|
+
const message = "Raigal license denied: Organization trial has expired";
|
|
16791
|
+
if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
|
|
16792
|
+
if (mode === "fatal") {
|
|
16793
|
+
process.stderr.write(`${message}\n`);
|
|
16794
|
+
process.exit(30);
|
|
16795
|
+
}
|
|
16796
|
+
process.stderr.write(`[raigal] ${message}\n`);
|
|
16797
|
+
return {
|
|
16798
|
+
ok: false,
|
|
16799
|
+
exitCode: 30,
|
|
16800
|
+
message
|
|
16801
|
+
};
|
|
16802
|
+
}
|
|
16803
|
+
}
|
|
16376
16804
|
const detectedOwner = detectLocalRepoOwner(directory);
|
|
16377
16805
|
if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
|
|
16378
16806
|
const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
|
|
@@ -16747,7 +17175,8 @@ const fixNarrativeComments = async (context) => {
|
|
|
16747
17175
|
for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
|
|
16748
17176
|
const newContent = kept.join("\n");
|
|
16749
17177
|
if (newContent !== content) {
|
|
16750
|
-
|
|
17178
|
+
const afterLiterals = extractStringAndTemplateLiterals(newContent, ext);
|
|
17179
|
+
if (!areLiteralMultisetsEqual(beforeLiterals, afterLiterals)) continue;
|
|
16751
17180
|
fs.writeFileSync(filePath, newContent);
|
|
16752
17181
|
}
|
|
16753
17182
|
}
|
|
@@ -16929,6 +17358,56 @@ const computeRemovalRange = (sourceFile, node, content) => {
|
|
|
16929
17358
|
end: finalEnd
|
|
16930
17359
|
};
|
|
16931
17360
|
};
|
|
17361
|
+
const isReferencedOutside = (sourceFile, statement, name) => {
|
|
17362
|
+
const start = statement.getStart(sourceFile);
|
|
17363
|
+
const end = statement.getEnd();
|
|
17364
|
+
let found = false;
|
|
17365
|
+
const visit = (node) => {
|
|
17366
|
+
if (found) return;
|
|
17367
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
17368
|
+
const pos = node.getStart(sourceFile);
|
|
17369
|
+
if (pos < start || pos >= end) found = true;
|
|
17370
|
+
return;
|
|
17371
|
+
}
|
|
17372
|
+
ts.forEachChild(node, visit);
|
|
17373
|
+
};
|
|
17374
|
+
visit(sourceFile);
|
|
17375
|
+
return found;
|
|
17376
|
+
};
|
|
17377
|
+
const exportKeywordRange = (sourceFile, statement, content) => {
|
|
17378
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
|
|
17379
|
+
if (modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) return null;
|
|
17380
|
+
const exportKeyword = modifiers.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
17381
|
+
if (!exportKeyword) return null;
|
|
17382
|
+
let end = exportKeyword.getEnd();
|
|
17383
|
+
while (end < content.length && (content[end] === " " || content[end] === " ")) end++;
|
|
17384
|
+
return {
|
|
17385
|
+
start: exportKeyword.getStart(sourceFile),
|
|
17386
|
+
end
|
|
17387
|
+
};
|
|
17388
|
+
};
|
|
17389
|
+
const removalFor = (sourceFile, statement, content, decl) => {
|
|
17390
|
+
if (!isReferencedOutside(sourceFile, statement, decl.name)) return null;
|
|
17391
|
+
const range = exportKeywordRange(sourceFile, statement, content);
|
|
17392
|
+
if (!range) return {
|
|
17393
|
+
type: "skip",
|
|
17394
|
+
reason: "still referenced in its own file",
|
|
17395
|
+
declaration: decl
|
|
17396
|
+
};
|
|
17397
|
+
return {
|
|
17398
|
+
type: "match",
|
|
17399
|
+
removal: {
|
|
17400
|
+
...range,
|
|
17401
|
+
declaration: decl
|
|
17402
|
+
}
|
|
17403
|
+
};
|
|
17404
|
+
};
|
|
17405
|
+
const declaredNames = (statement) => {
|
|
17406
|
+
if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.map((d) => ts.isIdentifier(d.name) ? d.name.text : null).filter((name) => name !== null);
|
|
17407
|
+
if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) return statement.name && ts.isIdentifier(statement.name) ? [statement.name.text] : [];
|
|
17408
|
+
return [];
|
|
17409
|
+
};
|
|
17410
|
+
const isDeclaredMoreThanOnce = (sourceFile, name) => sourceFile.statements.filter((s) => declaredNames(s).includes(name)).length > 1;
|
|
16932
17411
|
const kindOfStatement = (node) => {
|
|
16933
17412
|
if (ts.isVariableStatement(node)) return "variable";
|
|
16934
17413
|
if (ts.isFunctionDeclaration(node)) return "function";
|
|
@@ -16940,6 +17419,11 @@ const kindOfStatement = (node) => {
|
|
|
16940
17419
|
};
|
|
16941
17420
|
const matchStatement = (sourceFile, statement, content, decl) => {
|
|
16942
17421
|
if (!kindOfStatement(statement)) return { type: "none" };
|
|
17422
|
+
if (declaredNames(statement).includes(decl.name) && isDeclaredMoreThanOnce(sourceFile, decl.name)) return {
|
|
17423
|
+
type: "skip",
|
|
17424
|
+
reason: "merged declaration",
|
|
17425
|
+
declaration: decl
|
|
17426
|
+
};
|
|
16943
17427
|
if (ts.isVariableStatement(statement)) {
|
|
16944
17428
|
const varDecls = statement.declarationList.declarations;
|
|
16945
17429
|
if (varDecls.length === 0) return { type: "none" };
|
|
@@ -16955,6 +17439,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16955
17439
|
reason: "multi-declaration variable statement",
|
|
16956
17440
|
declaration: decl
|
|
16957
17441
|
};
|
|
17442
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17443
|
+
if (referenced) return referenced;
|
|
16958
17444
|
if (initializerHasSideEffects(match.initializer)) return {
|
|
16959
17445
|
type: "skip",
|
|
16960
17446
|
reason: "initializer may have side effects",
|
|
@@ -16972,6 +17458,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16972
17458
|
if (!statement.name) return { type: "none" };
|
|
16973
17459
|
if (statement.name.text !== decl.name) return { type: "none" };
|
|
16974
17460
|
if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
|
|
17461
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17462
|
+
if (referenced) return referenced;
|
|
16975
17463
|
return {
|
|
16976
17464
|
type: "match",
|
|
16977
17465
|
removal: {
|
|
@@ -16983,6 +17471,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16983
17471
|
if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
16984
17472
|
if (statement.name.text !== decl.name) return { type: "none" };
|
|
16985
17473
|
if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
|
|
17474
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17475
|
+
if (referenced) return referenced;
|
|
16986
17476
|
return {
|
|
16987
17477
|
type: "match",
|
|
16988
17478
|
removal: {
|
|
@@ -17056,7 +17546,7 @@ const detectUnusedDeclarations = async (context) => {
|
|
|
17056
17546
|
rule: "code-quality/unused-declaration",
|
|
17057
17547
|
severity: "warning",
|
|
17058
17548
|
message: `Unused ${extracted.kind}: ${extracted.name}`,
|
|
17059
|
-
help: "This top-level declaration is never used;
|
|
17549
|
+
help: "This top-level declaration is never used; raigal will remove it.",
|
|
17060
17550
|
line: d.line,
|
|
17061
17551
|
column: d.column,
|
|
17062
17552
|
category: "Dead Code",
|
|
@@ -17074,7 +17564,7 @@ const detectUnusedDeclarations = async (context) => {
|
|
|
17074
17564
|
rule: "code-quality/unused-declaration",
|
|
17075
17565
|
severity: "warning",
|
|
17076
17566
|
message: `Unused ${kind}: ${name}`,
|
|
17077
|
-
help: "This top-level declaration is never imported;
|
|
17567
|
+
help: "This top-level declaration is never imported; raigal will remove it.",
|
|
17078
17568
|
line: d.line,
|
|
17079
17569
|
column: d.column,
|
|
17080
17570
|
category: "Dead Code",
|
|
@@ -17180,7 +17670,7 @@ const removeUnusedDeclarations = (rootDirectory, declarations) => {
|
|
|
17180
17670
|
};
|
|
17181
17671
|
//#endregion
|
|
17182
17672
|
//#region src/commands/fix-expo.ts
|
|
17183
|
-
const INSTALL_TIMEOUT$1 =
|
|
17673
|
+
const INSTALL_TIMEOUT$1 = 18e5;
|
|
17184
17674
|
const fixExpoDependencies = async (context, onProgress) => {
|
|
17185
17675
|
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
17186
17676
|
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
@@ -17234,8 +17724,8 @@ const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
|
17234
17724
|
};
|
|
17235
17725
|
//#endregion
|
|
17236
17726
|
//#region src/commands/fix-force.ts
|
|
17237
|
-
const INSTALL_TIMEOUT =
|
|
17238
|
-
const AUDIT_TIMEOUT =
|
|
17727
|
+
const INSTALL_TIMEOUT = 18e5;
|
|
17728
|
+
const AUDIT_TIMEOUT = 6e4;
|
|
17239
17729
|
const detectPackageManager = (rootDirectory) => {
|
|
17240
17730
|
if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
|
|
17241
17731
|
if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
|
|
@@ -17554,13 +18044,14 @@ const CANNOT_SCOPE_REASON = "cannot honour an explicit file list under --changes
|
|
|
17554
18044
|
const MISSING_MANIFEST_REASON = "no manifest or lockfile in the selected scope";
|
|
17555
18045
|
const fixScopeError = (resolvedDir, flags) => {
|
|
17556
18046
|
if (flags.changes && flags.staged) return "--changes and --staged cannot be used together.";
|
|
17557
|
-
|
|
18047
|
+
const options = {
|
|
17558
18048
|
changes: Boolean(flags.changes),
|
|
17559
18049
|
staged: Boolean(flags.staged),
|
|
17560
18050
|
base: flags.base,
|
|
17561
18051
|
verbose: false,
|
|
17562
18052
|
json: false
|
|
17563
|
-
}
|
|
18053
|
+
};
|
|
18054
|
+
return scanTargetError(resolvedDir, options);
|
|
17564
18055
|
};
|
|
17565
18056
|
const collectFixFileScope = (rootDirectory, excludePatterns, includePatterns, flags) => {
|
|
17566
18057
|
const mode = resolveScanScopeMode({
|
|
@@ -17619,9 +18110,10 @@ const runFormattingStep = async (deps) => {
|
|
|
17619
18110
|
else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python formatting fixes.");
|
|
17620
18111
|
if (deps.projectInfo.languages.includes("go") && deps.projectInfo.installedTools.gofmt) await deps.runStep("Formatting (go)", () => runGofmt(deps.context), () => fixGofmt(deps.context));
|
|
17621
18112
|
else if (deps.projectInfo.languages.includes("go")) log.warn("Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
17622
|
-
if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt)
|
|
17623
|
-
|
|
17624
|
-
|
|
18113
|
+
if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt) {
|
|
18114
|
+
if (isScopedFix(deps.context)) deps.skipStep?.("Formatting (rust)", CANNOT_SCOPE_REASON);
|
|
18115
|
+
else await deps.runStep("Formatting (rust)", () => runGenericFormatter(deps.context, "rust"), () => fixGenericFormatter(deps.context, "rust"));
|
|
18116
|
+
} else if (deps.projectInfo.languages.includes("rust")) log.warn("Rust detected but rustfmt is not installed; skipping Rust formatting fixes.");
|
|
17625
18117
|
if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) {
|
|
17626
18118
|
if (!skipUnsafeSafeFormatter(deps, "ruby")) await deps.runStep("Formatting (ruby)", () => runGenericFormatter(deps.context, "ruby"), () => fixGenericFormatter(deps.context, "ruby"));
|
|
17627
18119
|
} else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby formatting fixes.");
|
|
@@ -17670,7 +18162,8 @@ const runDeclarationStep = async (deps) => {
|
|
|
17670
18162
|
if (!deps.config.engines["code-quality"]) return;
|
|
17671
18163
|
if (!hasJsOrTs(deps.projectInfo)) return;
|
|
17672
18164
|
await deps.runStep("Unused declarations", () => detectUnusedDeclarations(deps.context), async () => {
|
|
17673
|
-
const
|
|
18165
|
+
const diagnostics = await detectUnusedDeclarations(deps.context);
|
|
18166
|
+
const declarations = diagnosticsToDeclarations(diagnostics);
|
|
17674
18167
|
removeUnusedDeclarations(deps.resolvedDir, declarations);
|
|
17675
18168
|
});
|
|
17676
18169
|
};
|
|
@@ -17701,10 +18194,14 @@ const runForceSteps = async (deps) => {
|
|
|
17701
18194
|
return (await runKnipUnusedFiles(deps.resolvedDir)).filter((diagnostic) => isPathInFixScope(deps.context, diagnostic.filePath));
|
|
17702
18195
|
}, () => fixUnusedFiles(deps.resolvedDir, (filePath) => isPathInFixScope(deps.context, filePath)));
|
|
17703
18196
|
const railUpdate = (label) => deps.rail.setActiveLabel(label);
|
|
17704
|
-
if (deps.config.engines.security)
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
18197
|
+
if (deps.config.engines.security) {
|
|
18198
|
+
if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Dependency audit fixes", MISSING_MANIFEST_REASON);
|
|
18199
|
+
else await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
|
|
18200
|
+
}
|
|
18201
|
+
if (deps.projectInfo.frameworks.includes("expo") && deps.config.lint.expoDoctor) {
|
|
18202
|
+
if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Expo dependency alignment", MISSING_MANIFEST_REASON);
|
|
18203
|
+
else await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
|
|
18204
|
+
}
|
|
17708
18205
|
};
|
|
17709
18206
|
//#endregion
|
|
17710
18207
|
//#region src/commands/fix-plan.ts
|
|
@@ -18026,7 +18523,8 @@ const saveCachedPolicy = (repoSlug, policy) => {
|
|
|
18026
18523
|
};
|
|
18027
18524
|
const fetchRemotePolicy = async (repoSlug, token) => {
|
|
18028
18525
|
try {
|
|
18029
|
-
const
|
|
18526
|
+
const endpoint = `/v1/policy?repo=${encodeURIComponent(repoSlug.toLowerCase())}`;
|
|
18527
|
+
const data = await requestApi(endpoint, {
|
|
18030
18528
|
method: "GET",
|
|
18031
18529
|
token,
|
|
18032
18530
|
timeoutMs: 4e3
|
|
@@ -18086,6 +18584,8 @@ const resolveHistoryDir = (directory) => {
|
|
|
18086
18584
|
const resolved = path.resolve(directory);
|
|
18087
18585
|
const primary = path.join(resolved, CONFIG_DIR);
|
|
18088
18586
|
if (fs.existsSync(primary)) return primary;
|
|
18587
|
+
const legacy = path.join(resolved, ".aislop");
|
|
18588
|
+
if (fs.existsSync(legacy)) return legacy;
|
|
18089
18589
|
return null;
|
|
18090
18590
|
};
|
|
18091
18591
|
const historyPath = (directory) => {
|
|
@@ -18198,6 +18698,842 @@ const recordFullScanActivity = (history, allowPilotInvitation, telemetryConfig)
|
|
|
18198
18698
|
appendHistory(history);
|
|
18199
18699
|
return allowPilotInvitation && !isHistoryDisabled() && !isTelemetryDisabled(telemetryConfig) && recordCompletedFullScan();
|
|
18200
18700
|
};
|
|
18701
|
+
const AISLOP_MD_BODY = `# Raigal - agent instructions
|
|
18702
|
+
|
|
18703
|
+
[Raigal](https://github.com/meethiu/raigal) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, hidden fallbacks, oversized functions, risky security constructs - and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
|
|
18704
|
+
|
|
18705
|
+
## On every edit
|
|
18706
|
+
|
|
18707
|
+
A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`RaigalFeedback\` (score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
|
|
18708
|
+
|
|
18709
|
+
## Severity ladder
|
|
18710
|
+
|
|
18711
|
+
- \`error\` - MUST fix this turn.
|
|
18712
|
+
- \`warning\` + \`fixable: true\` - MUST fix this turn.
|
|
18713
|
+
- \`warning\` + \`fixable: false\` - fix if trivially mechanical, otherwise surface in your reply.
|
|
18714
|
+
|
|
18715
|
+
## Rules
|
|
18716
|
+
|
|
18717
|
+
- \`.raigal/config.yaml\` (or \`.aislop/config.yaml\`) - thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
|
|
18718
|
+
- \`.raigal/rules.yaml\` (or \`.aislop/rules.yaml\`) - project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
|
|
18719
|
+
- Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
|
|
18720
|
+
|
|
18721
|
+
## Principles
|
|
18722
|
+
|
|
18723
|
+
- Do not disable rules to pass the scan. Fix the underlying issue.
|
|
18724
|
+
- If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
|
|
18725
|
+
- The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
|
|
18726
|
+
`;
|
|
18727
|
+
//#endregion
|
|
18728
|
+
//#region src/hooks/io/atomic-write.ts
|
|
18729
|
+
const atomicWrite = (targetPath, content) => {
|
|
18730
|
+
const dir = path.dirname(targetPath);
|
|
18731
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18732
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
18733
|
+
const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
|
|
18734
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
18735
|
+
try {
|
|
18736
|
+
fs.renameSync(tmp, targetPath);
|
|
18737
|
+
} catch (err) {
|
|
18738
|
+
const code = err?.code;
|
|
18739
|
+
if (code === "EPERM" || code === "EBUSY") {
|
|
18740
|
+
fs.copyFileSync(tmp, targetPath);
|
|
18741
|
+
try {
|
|
18742
|
+
fs.unlinkSync(tmp);
|
|
18743
|
+
} catch {}
|
|
18744
|
+
} else throw err;
|
|
18745
|
+
}
|
|
18746
|
+
};
|
|
18747
|
+
const readIfExists = (targetPath) => {
|
|
18748
|
+
try {
|
|
18749
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
18750
|
+
} catch {
|
|
18751
|
+
return null;
|
|
18752
|
+
}
|
|
18753
|
+
};
|
|
18754
|
+
//#endregion
|
|
18755
|
+
//#region src/hooks/io/sentinel.ts
|
|
18756
|
+
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
18757
|
+
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
18758
|
+
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
18759
|
+
const renderFence = (body, hash) => [
|
|
18760
|
+
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
18761
|
+
body.trimEnd(),
|
|
18762
|
+
"<!-- aislop:end v1 -->"
|
|
18763
|
+
].join("\n");
|
|
18764
|
+
const upsertMarkdownFence = (existing, body, hash) => {
|
|
18765
|
+
const fenced = renderFence(body, hash);
|
|
18766
|
+
if (existing == null || existing.length === 0) return {
|
|
18767
|
+
nextContent: `${fenced}\n`,
|
|
18768
|
+
replaced: false
|
|
18769
|
+
};
|
|
18770
|
+
const begin = existing.match(BEGIN_RE);
|
|
18771
|
+
const end = existing.match(END_RE);
|
|
18772
|
+
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
18773
|
+
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
18774
|
+
replaced: true
|
|
18775
|
+
};
|
|
18776
|
+
return {
|
|
18777
|
+
nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
|
|
18778
|
+
replaced: false
|
|
18779
|
+
};
|
|
18780
|
+
};
|
|
18781
|
+
//#endregion
|
|
18782
|
+
//#region src/hooks/install/types.ts
|
|
18783
|
+
const emptyResult = () => ({
|
|
18784
|
+
wrote: [],
|
|
18785
|
+
skipped: [],
|
|
18786
|
+
planned: []
|
|
18787
|
+
});
|
|
18788
|
+
const applyContent = (result, opts, target, nextContent, summary) => {
|
|
18789
|
+
if (readIfExists(target) === nextContent) {
|
|
18790
|
+
result.skipped.push(target);
|
|
18791
|
+
return;
|
|
18792
|
+
}
|
|
18793
|
+
if (opts.dryRun) {
|
|
18794
|
+
result.planned.push({
|
|
18795
|
+
path: target,
|
|
18796
|
+
summary
|
|
18797
|
+
});
|
|
18798
|
+
return;
|
|
18799
|
+
}
|
|
18800
|
+
atomicWrite(target, nextContent);
|
|
18801
|
+
result.wrote.push(target);
|
|
18802
|
+
};
|
|
18803
|
+
const applyRemoval = (result, opts, target, nextContent) => {
|
|
18804
|
+
const existing = readIfExists(target);
|
|
18805
|
+
if (existing == null) {
|
|
18806
|
+
result.skipped.push(target);
|
|
18807
|
+
return;
|
|
18808
|
+
}
|
|
18809
|
+
if (existing === (nextContent ?? "")) {
|
|
18810
|
+
result.skipped.push(target);
|
|
18811
|
+
return;
|
|
18812
|
+
}
|
|
18813
|
+
if (opts.dryRun) {
|
|
18814
|
+
result.removed.push(target);
|
|
18815
|
+
return;
|
|
18816
|
+
}
|
|
18817
|
+
if (nextContent == null) try {
|
|
18818
|
+
fs.unlinkSync(target);
|
|
18819
|
+
} catch {}
|
|
18820
|
+
else atomicWrite(target, nextContent);
|
|
18821
|
+
result.removed.push(target);
|
|
18822
|
+
};
|
|
18823
|
+
//#endregion
|
|
18824
|
+
//#region src/hooks/install/rules-only.ts
|
|
18825
|
+
const installRulesOnly = (opts, paths, summary) => {
|
|
18826
|
+
const result = emptyResult();
|
|
18827
|
+
const existing = readIfExists(paths.rules);
|
|
18828
|
+
const hash = sentinelHash(AISLOP_MD_BODY);
|
|
18829
|
+
const next = upsertMarkdownFence(existing, AISLOP_MD_BODY, hash).nextContent;
|
|
18830
|
+
applyContent(result, opts, paths.rules, next, summary);
|
|
18831
|
+
if (paths.host && paths.marker) {
|
|
18832
|
+
const host = readIfExists(paths.host) ?? "";
|
|
18833
|
+
if (!host.includes(paths.marker)) {
|
|
18834
|
+
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
18835
|
+
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
18836
|
+
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
18837
|
+
} else result.skipped.push(paths.host);
|
|
18838
|
+
}
|
|
18839
|
+
return result;
|
|
18840
|
+
};
|
|
18841
|
+
const uninstallRulesOnly = (opts, paths) => {
|
|
18842
|
+
const result = {
|
|
18843
|
+
removed: [],
|
|
18844
|
+
skipped: []
|
|
18845
|
+
};
|
|
18846
|
+
if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
|
|
18847
|
+
else result.skipped.push(paths.rules);
|
|
18848
|
+
if (paths.host && paths.marker) {
|
|
18849
|
+
const host = readIfExists(paths.host);
|
|
18850
|
+
if (host?.includes(paths.marker)) {
|
|
18851
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
18852
|
+
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
18853
|
+
} else result.skipped.push(paths.host);
|
|
18854
|
+
}
|
|
18855
|
+
return result;
|
|
18856
|
+
};
|
|
18857
|
+
//#endregion
|
|
18858
|
+
//#region src/hooks/install/antigravity.ts
|
|
18859
|
+
const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
|
|
18860
|
+
const installAntigravity = (opts) => {
|
|
18861
|
+
if (opts.scope !== "project") return {
|
|
18862
|
+
wrote: [],
|
|
18863
|
+
skipped: [],
|
|
18864
|
+
planned: [{
|
|
18865
|
+
path: ".agents/rules/antigravity-aislop-rules.md",
|
|
18866
|
+
summary: "Antigravity is project-scope only; pass --project"
|
|
18867
|
+
}]
|
|
18868
|
+
};
|
|
18869
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
|
|
18870
|
+
};
|
|
18871
|
+
const uninstallAntigravity = (opts) => {
|
|
18872
|
+
if (opts.scope !== "project") return {
|
|
18873
|
+
removed: [],
|
|
18874
|
+
skipped: []
|
|
18875
|
+
};
|
|
18876
|
+
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
18877
|
+
};
|
|
18878
|
+
//#endregion
|
|
18879
|
+
//#region src/hooks/io/json-patch.ts
|
|
18880
|
+
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
18881
|
+
const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
|
|
18882
|
+
const groupIsAislop = (group) => {
|
|
18883
|
+
if (typeof group !== "object" || group === null) return false;
|
|
18884
|
+
const hooks = group.hooks;
|
|
18885
|
+
if (!Array.isArray(hooks)) return false;
|
|
18886
|
+
return hooks.some((h) => isAislopManaged(h));
|
|
18887
|
+
};
|
|
18888
|
+
const upsertHookGroup = (config, event, group) => {
|
|
18889
|
+
const next = { ...config };
|
|
18890
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18891
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
|
|
18892
|
+
next.hooks = {
|
|
18893
|
+
...hooks,
|
|
18894
|
+
[event]: [...cleaned, group]
|
|
18895
|
+
};
|
|
18896
|
+
return next;
|
|
18897
|
+
};
|
|
18898
|
+
const upsertFlatHook = (config, event, entry) => {
|
|
18899
|
+
const next = { ...config };
|
|
18900
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18901
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
|
|
18902
|
+
next.hooks = {
|
|
18903
|
+
...hooks,
|
|
18904
|
+
[event]: [...cleaned, entry]
|
|
18905
|
+
};
|
|
18906
|
+
return next;
|
|
18907
|
+
};
|
|
18908
|
+
const removeAislopEntries = (config, event) => {
|
|
18909
|
+
const next = { ...config };
|
|
18910
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18911
|
+
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
18912
|
+
const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
|
|
18913
|
+
const removed = existing.length - cleaned.length;
|
|
18914
|
+
const nextHooks = { ...hooks };
|
|
18915
|
+
if (cleaned.length === 0) delete nextHooks[event];
|
|
18916
|
+
else nextHooks[event] = cleaned;
|
|
18917
|
+
if (Object.keys(nextHooks).length === 0) delete next.hooks;
|
|
18918
|
+
else next.hooks = nextHooks;
|
|
18919
|
+
return {
|
|
18920
|
+
next,
|
|
18921
|
+
removed
|
|
18922
|
+
};
|
|
18923
|
+
};
|
|
18924
|
+
//#endregion
|
|
18925
|
+
//#region src/hooks/install/claude.ts
|
|
18926
|
+
const resolveClaudePaths = (opts) => {
|
|
18927
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
18928
|
+
return {
|
|
18929
|
+
settings: path.join(root, "settings.json"),
|
|
18930
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
18931
|
+
claudeMd: path.join(root, "CLAUDE.md")
|
|
18932
|
+
};
|
|
18933
|
+
};
|
|
18934
|
+
const buildHookGroup$1 = () => {
|
|
18935
|
+
const hashBody = JSON.stringify({
|
|
18936
|
+
command: "raigal hook claude",
|
|
18937
|
+
matcher: "Edit|Write|MultiEdit"
|
|
18938
|
+
});
|
|
18939
|
+
return {
|
|
18940
|
+
matcher: "Edit|Write|MultiEdit",
|
|
18941
|
+
hooks: [{
|
|
18942
|
+
type: "command",
|
|
18943
|
+
command: "raigal hook claude",
|
|
18944
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18945
|
+
v: 1,
|
|
18946
|
+
managed: true,
|
|
18947
|
+
hash: sentinelHash(hashBody)
|
|
18948
|
+
}
|
|
18949
|
+
}]
|
|
18950
|
+
};
|
|
18951
|
+
};
|
|
18952
|
+
const buildStopHookGroup = () => {
|
|
18953
|
+
const hashBody = JSON.stringify({ command: "raigal hook claude --stop" });
|
|
18954
|
+
return {
|
|
18955
|
+
matcher: "",
|
|
18956
|
+
hooks: [{
|
|
18957
|
+
type: "command",
|
|
18958
|
+
command: "raigal hook claude --stop",
|
|
18959
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18960
|
+
v: 1,
|
|
18961
|
+
managed: true,
|
|
18962
|
+
hash: sentinelHash(hashBody)
|
|
18963
|
+
}
|
|
18964
|
+
}]
|
|
18965
|
+
};
|
|
18966
|
+
};
|
|
18967
|
+
const FILE_CHANGED_MATCHER = ".raigal/config.yml|.raigal/rules.yml|.aislop/config.yml|.aislop/rules.yml|package.json";
|
|
18968
|
+
const buildFileChangedHookGroup = () => {
|
|
18969
|
+
const hashBody = JSON.stringify({
|
|
18970
|
+
command: "raigal hook claude --on-file-changed",
|
|
18971
|
+
matcher: FILE_CHANGED_MATCHER
|
|
18972
|
+
});
|
|
18973
|
+
return {
|
|
18974
|
+
matcher: FILE_CHANGED_MATCHER,
|
|
18975
|
+
hooks: [{
|
|
18976
|
+
type: "command",
|
|
18977
|
+
command: "raigal hook claude --on-file-changed",
|
|
18978
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18979
|
+
v: 1,
|
|
18980
|
+
managed: true,
|
|
18981
|
+
hash: sentinelHash(hashBody)
|
|
18982
|
+
}
|
|
18983
|
+
}]
|
|
18984
|
+
};
|
|
18985
|
+
};
|
|
18986
|
+
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
18987
|
+
let obj = {};
|
|
18988
|
+
if (existingRaw) try {
|
|
18989
|
+
obj = JSON.parse(existingRaw);
|
|
18990
|
+
} catch {
|
|
18991
|
+
obj = {};
|
|
18992
|
+
}
|
|
18993
|
+
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
18994
|
+
next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
|
|
18995
|
+
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
18996
|
+
else next = removeAislopEntries(next, "Stop").next;
|
|
18997
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
18998
|
+
};
|
|
18999
|
+
const installClaude = (opts) => {
|
|
19000
|
+
const paths = resolveClaudePaths(opts);
|
|
19001
|
+
const result = emptyResult();
|
|
19002
|
+
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
19003
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
19004
|
+
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
19005
|
+
const existingMd = readIfExists(paths.aislopMd);
|
|
19006
|
+
const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, mdHash);
|
|
19007
|
+
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
19008
|
+
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
19009
|
+
const marker = "@AISLOP.md";
|
|
19010
|
+
if (!existingClaudeMd.includes(marker)) {
|
|
19011
|
+
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
19012
|
+
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
19013
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
19014
|
+
} else result.skipped.push(paths.claudeMd);
|
|
19015
|
+
return result;
|
|
19016
|
+
};
|
|
19017
|
+
const uninstallClaude = (opts) => {
|
|
19018
|
+
const paths = resolveClaudePaths({
|
|
19019
|
+
...opts,
|
|
19020
|
+
qualityGate: false
|
|
19021
|
+
});
|
|
19022
|
+
const result = {
|
|
19023
|
+
removed: [],
|
|
19024
|
+
skipped: []
|
|
19025
|
+
};
|
|
19026
|
+
const settingsRaw = readIfExists(paths.settings);
|
|
19027
|
+
if (settingsRaw) {
|
|
19028
|
+
let obj = {};
|
|
19029
|
+
try {
|
|
19030
|
+
obj = JSON.parse(settingsRaw);
|
|
19031
|
+
} catch {
|
|
19032
|
+
obj = {};
|
|
19033
|
+
}
|
|
19034
|
+
const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
|
|
19035
|
+
const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
|
|
19036
|
+
const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
|
|
19037
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
19038
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
19039
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
19040
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19041
|
+
} else result.skipped.push(paths.settings);
|
|
19042
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
19043
|
+
else result.skipped.push(paths.aislopMd);
|
|
19044
|
+
const claudeMd = readIfExists(paths.claudeMd);
|
|
19045
|
+
if (claudeMd?.includes("@AISLOP.md")) {
|
|
19046
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19047
|
+
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
19048
|
+
} else result.skipped.push(paths.claudeMd);
|
|
19049
|
+
return result;
|
|
19050
|
+
};
|
|
19051
|
+
//#endregion
|
|
19052
|
+
//#region src/hooks/install/cline.ts
|
|
19053
|
+
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
19054
|
+
const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
|
|
19055
|
+
const installCline = (opts) => {
|
|
19056
|
+
if (opts.scope !== "project") return {
|
|
19057
|
+
wrote: [],
|
|
19058
|
+
skipped: [],
|
|
19059
|
+
planned: [{
|
|
19060
|
+
path: ".clinerules",
|
|
19061
|
+
summary: "Cline is project-scope only; pass --project"
|
|
19062
|
+
}]
|
|
19063
|
+
};
|
|
19064
|
+
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
19065
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
|
|
19066
|
+
return {
|
|
19067
|
+
wrote: [...cline.wrote, ...roo.wrote],
|
|
19068
|
+
skipped: [...cline.skipped, ...roo.skipped],
|
|
19069
|
+
planned: [...cline.planned, ...roo.planned]
|
|
19070
|
+
};
|
|
19071
|
+
};
|
|
19072
|
+
const uninstallCline = (opts) => {
|
|
19073
|
+
if (opts.scope !== "project") return {
|
|
19074
|
+
removed: [],
|
|
19075
|
+
skipped: []
|
|
19076
|
+
};
|
|
19077
|
+
const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
|
|
19078
|
+
const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
|
|
19079
|
+
return {
|
|
19080
|
+
removed: [...a.removed, ...b.removed],
|
|
19081
|
+
skipped: [...a.skipped, ...b.skipped]
|
|
19082
|
+
};
|
|
19083
|
+
};
|
|
19084
|
+
//#endregion
|
|
19085
|
+
//#region src/hooks/install/codex.ts
|
|
19086
|
+
const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
|
|
19087
|
+
const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
|
|
19088
|
+
const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
|
|
19089
|
+
//#endregion
|
|
19090
|
+
//#region src/hooks/install/copilot.ts
|
|
19091
|
+
const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
|
|
19092
|
+
const installCopilot = (opts) => {
|
|
19093
|
+
if (opts.scope !== "project") return {
|
|
19094
|
+
wrote: [],
|
|
19095
|
+
skipped: [],
|
|
19096
|
+
planned: [{
|
|
19097
|
+
path: ".github/copilot-instructions.md",
|
|
19098
|
+
summary: "Copilot is project-scope only; pass --project"
|
|
19099
|
+
}]
|
|
19100
|
+
};
|
|
19101
|
+
return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
|
|
19102
|
+
};
|
|
19103
|
+
const uninstallCopilot = (opts) => {
|
|
19104
|
+
if (opts.scope !== "project") return {
|
|
19105
|
+
removed: [],
|
|
19106
|
+
skipped: []
|
|
19107
|
+
};
|
|
19108
|
+
return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
|
|
19109
|
+
};
|
|
19110
|
+
//#endregion
|
|
19111
|
+
//#region src/hooks/install/cursor.ts
|
|
19112
|
+
const resolveCursorPaths = (opts) => {
|
|
19113
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
19114
|
+
return {
|
|
19115
|
+
hooks: path.join(root, "hooks.json"),
|
|
19116
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
19117
|
+
};
|
|
19118
|
+
};
|
|
19119
|
+
const buildHookEntry = () => {
|
|
19120
|
+
const hashBody = JSON.stringify({
|
|
19121
|
+
command: "raigal hook cursor",
|
|
19122
|
+
timeout: 5e3
|
|
19123
|
+
});
|
|
19124
|
+
return {
|
|
19125
|
+
command: "raigal hook cursor",
|
|
19126
|
+
type: "command",
|
|
19127
|
+
timeout: 5e3,
|
|
19128
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
19129
|
+
v: 1,
|
|
19130
|
+
managed: true,
|
|
19131
|
+
hash: sentinelHash(hashBody)
|
|
19132
|
+
}
|
|
19133
|
+
};
|
|
19134
|
+
};
|
|
19135
|
+
const renderHooksJson = (existingRaw) => {
|
|
19136
|
+
let obj = { version: 1 };
|
|
19137
|
+
if (existingRaw) try {
|
|
19138
|
+
obj = JSON.parse(existingRaw);
|
|
19139
|
+
} catch {
|
|
19140
|
+
obj = { version: 1 };
|
|
19141
|
+
}
|
|
19142
|
+
if (typeof obj.version !== "number") obj.version = 1;
|
|
19143
|
+
const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
|
|
19144
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
19145
|
+
};
|
|
19146
|
+
const installCursor = (opts) => {
|
|
19147
|
+
const paths = resolveCursorPaths(opts);
|
|
19148
|
+
const result = emptyResult();
|
|
19149
|
+
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
19150
|
+
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
19151
|
+
if (opts.scope === "project") {
|
|
19152
|
+
const existingRules = readIfExists(paths.rules);
|
|
19153
|
+
const hash = sentinelHash(AISLOP_MD_BODY);
|
|
19154
|
+
const rules = upsertMarkdownFence(existingRules, AISLOP_MD_BODY, hash).nextContent;
|
|
19155
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
|
|
19156
|
+
}
|
|
19157
|
+
return result;
|
|
19158
|
+
};
|
|
19159
|
+
const uninstallCursor = (opts) => {
|
|
19160
|
+
const paths = resolveCursorPaths(opts);
|
|
19161
|
+
const result = {
|
|
19162
|
+
removed: [],
|
|
19163
|
+
skipped: []
|
|
19164
|
+
};
|
|
19165
|
+
const raw = readIfExists(paths.hooks);
|
|
19166
|
+
if (raw) {
|
|
19167
|
+
let obj = {};
|
|
19168
|
+
try {
|
|
19169
|
+
obj = JSON.parse(raw);
|
|
19170
|
+
} catch {
|
|
19171
|
+
obj = {};
|
|
19172
|
+
}
|
|
19173
|
+
const stripped = removeAislopEntries(obj, "afterFileEdit").next;
|
|
19174
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
19175
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
|
|
19176
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
19177
|
+
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19178
|
+
} else result.skipped.push(paths.hooks);
|
|
19179
|
+
if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
|
|
19180
|
+
return result;
|
|
19181
|
+
};
|
|
19182
|
+
//#endregion
|
|
19183
|
+
//#region src/hooks/install/gemini.ts
|
|
19184
|
+
const resolveGeminiPaths = (opts) => {
|
|
19185
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
19186
|
+
return {
|
|
19187
|
+
settings: path.join(root, "settings.json"),
|
|
19188
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
19189
|
+
geminiMd: path.join(root, "GEMINI.md")
|
|
19190
|
+
};
|
|
19191
|
+
};
|
|
19192
|
+
const buildHookGroup = () => {
|
|
19193
|
+
const hashBody = JSON.stringify({
|
|
19194
|
+
command: "raigal hook gemini",
|
|
19195
|
+
matcher: "write_file|replace"
|
|
19196
|
+
});
|
|
19197
|
+
return {
|
|
19198
|
+
matcher: "write_file|replace",
|
|
19199
|
+
hooks: [{
|
|
19200
|
+
name: "raigal",
|
|
19201
|
+
type: "command",
|
|
19202
|
+
command: "raigal hook gemini",
|
|
19203
|
+
timeout: 5e3,
|
|
19204
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
19205
|
+
v: 1,
|
|
19206
|
+
managed: true,
|
|
19207
|
+
hash: sentinelHash(hashBody)
|
|
19208
|
+
}
|
|
19209
|
+
}]
|
|
19210
|
+
};
|
|
19211
|
+
};
|
|
19212
|
+
const renderSettings = (existingRaw) => {
|
|
19213
|
+
let obj = {};
|
|
19214
|
+
if (existingRaw) try {
|
|
19215
|
+
obj = JSON.parse(existingRaw);
|
|
19216
|
+
} catch {
|
|
19217
|
+
obj = {};
|
|
19218
|
+
}
|
|
19219
|
+
const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
|
|
19220
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
19221
|
+
};
|
|
19222
|
+
const installGemini = (opts) => {
|
|
19223
|
+
const paths = resolveGeminiPaths(opts);
|
|
19224
|
+
const result = emptyResult();
|
|
19225
|
+
const next = renderSettings(readIfExists(paths.settings));
|
|
19226
|
+
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
19227
|
+
const existingMd = readIfExists(paths.aislopMd);
|
|
19228
|
+
const hash = sentinelHash(AISLOP_MD_BODY);
|
|
19229
|
+
const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, hash).nextContent;
|
|
19230
|
+
applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
|
|
19231
|
+
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
19232
|
+
const marker = "@AISLOP.md";
|
|
19233
|
+
if (!existingGeminiMd.includes(marker)) {
|
|
19234
|
+
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
19235
|
+
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
19236
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
19237
|
+
} else result.skipped.push(paths.geminiMd);
|
|
19238
|
+
return result;
|
|
19239
|
+
};
|
|
19240
|
+
const uninstallGemini = (opts) => {
|
|
19241
|
+
const paths = resolveGeminiPaths(opts);
|
|
19242
|
+
const result = {
|
|
19243
|
+
removed: [],
|
|
19244
|
+
skipped: []
|
|
19245
|
+
};
|
|
19246
|
+
const raw = readIfExists(paths.settings);
|
|
19247
|
+
if (raw) {
|
|
19248
|
+
let obj = {};
|
|
19249
|
+
try {
|
|
19250
|
+
obj = JSON.parse(raw);
|
|
19251
|
+
} catch {
|
|
19252
|
+
obj = {};
|
|
19253
|
+
}
|
|
19254
|
+
const stripped = removeAislopEntries(obj, "AfterTool").next;
|
|
19255
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
19256
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
19257
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
19258
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19259
|
+
} else result.skipped.push(paths.settings);
|
|
19260
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
19261
|
+
else result.skipped.push(paths.aislopMd);
|
|
19262
|
+
const geminiMd = readIfExists(paths.geminiMd);
|
|
19263
|
+
if (geminiMd?.includes("@AISLOP.md")) {
|
|
19264
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19265
|
+
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
19266
|
+
} else result.skipped.push(paths.geminiMd);
|
|
19267
|
+
return result;
|
|
19268
|
+
};
|
|
19269
|
+
//#endregion
|
|
19270
|
+
//#region src/hooks/install/kilocode.ts
|
|
19271
|
+
const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
|
|
19272
|
+
const installKilocode = (opts) => {
|
|
19273
|
+
if (opts.scope !== "project") return {
|
|
19274
|
+
wrote: [],
|
|
19275
|
+
skipped: [],
|
|
19276
|
+
planned: [{
|
|
19277
|
+
path: ".kilocode/rules/aislop-rules.md",
|
|
19278
|
+
summary: "Kilo Code is project-scope only; pass --project"
|
|
19279
|
+
}]
|
|
19280
|
+
};
|
|
19281
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
|
|
19282
|
+
};
|
|
19283
|
+
const uninstallKilocode = (opts) => {
|
|
19284
|
+
if (opts.scope !== "project") return {
|
|
19285
|
+
removed: [],
|
|
19286
|
+
skipped: []
|
|
19287
|
+
};
|
|
19288
|
+
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
19289
|
+
};
|
|
19290
|
+
//#endregion
|
|
19291
|
+
//#region src/hooks/install/pi.ts
|
|
19292
|
+
const resolvePiPaths = (opts) => {
|
|
19293
|
+
return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
|
|
19294
|
+
};
|
|
19295
|
+
const PI_EXTENSION_SOURCE = `// raigal — auto-generated pi extension. Do not edit by hand.
|
|
19296
|
+
// Reinstall with: raigal hook install --pi
|
|
19297
|
+
import { spawn } from "node:child_process";
|
|
19298
|
+
|
|
19299
|
+
// Runs the CLI async so a slow scan never blocks pi's event loop, and forwards
|
|
19300
|
+
// ctx.signal so Esc actually kills the child instead of it running to completion.
|
|
19301
|
+
const runHook = (bin, args, input, signal) =>
|
|
19302
|
+
new Promise((resolve) => {
|
|
19303
|
+
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
19304
|
+
const chunks = [];
|
|
19305
|
+
let settled = false;
|
|
19306
|
+
let timer;
|
|
19307
|
+
const onAbort = () => {
|
|
19308
|
+
child.kill();
|
|
19309
|
+
finish(null);
|
|
19310
|
+
};
|
|
19311
|
+
const cleanup = () => {
|
|
19312
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
19313
|
+
signal?.removeEventListener("abort", onAbort);
|
|
19314
|
+
};
|
|
19315
|
+
const finish = (value) => {
|
|
19316
|
+
if (settled) return;
|
|
19317
|
+
settled = true;
|
|
19318
|
+
cleanup();
|
|
19319
|
+
resolve(value);
|
|
19320
|
+
};
|
|
19321
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19322
|
+
timer = setTimeout(() => {
|
|
19323
|
+
child.kill();
|
|
19324
|
+
finish(null);
|
|
19325
|
+
}, 15000);
|
|
19326
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
19327
|
+
child.on("error", () => finish(null));
|
|
19328
|
+
child.on("close", (code) => {
|
|
19329
|
+
finish(code === 0 && chunks.length > 0 ? Buffer.concat(chunks).toString("utf-8") : null);
|
|
19330
|
+
});
|
|
19331
|
+
child.stdin.end(input);
|
|
19332
|
+
});
|
|
19333
|
+
|
|
19334
|
+
export default function (pi) {
|
|
19335
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
19336
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
19337
|
+
if (event.isError) return;
|
|
19338
|
+
const filePath = event.input && event.input.path;
|
|
19339
|
+
if (typeof filePath !== "string" || filePath.length === 0) return;
|
|
19340
|
+
|
|
19341
|
+
const bin = process.env.RAIGAL_BIN || process.env.AISLOP_BIN || "raigal";
|
|
19342
|
+
const payload = JSON.stringify({
|
|
19343
|
+
cwd: ctx.cwd,
|
|
19344
|
+
file_path: filePath,
|
|
19345
|
+
tool_name: event.toolName,
|
|
19346
|
+
});
|
|
19347
|
+
|
|
19348
|
+
let out;
|
|
19349
|
+
try {
|
|
19350
|
+
const stdout = await runHook(bin, ["hook", "pi"], payload, ctx.signal);
|
|
19351
|
+
if (!stdout) return;
|
|
19352
|
+
out = JSON.parse(stdout);
|
|
19353
|
+
} catch {
|
|
19354
|
+
return;
|
|
19355
|
+
}
|
|
19356
|
+
if (!out || !out.message) return;
|
|
19357
|
+
|
|
19358
|
+
return {
|
|
19359
|
+
content: [...event.content, { type: "text", text: out.message }],
|
|
19360
|
+
isError: event.isError,
|
|
19361
|
+
};
|
|
19362
|
+
});
|
|
19363
|
+
}
|
|
19364
|
+
`;
|
|
19365
|
+
const installPi = (opts) => {
|
|
19366
|
+
const paths = resolvePiPaths(opts);
|
|
19367
|
+
const result = emptyResult();
|
|
19368
|
+
applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi raigal extension");
|
|
19369
|
+
return result;
|
|
19370
|
+
};
|
|
19371
|
+
const uninstallPi = (opts) => {
|
|
19372
|
+
const paths = resolvePiPaths(opts);
|
|
19373
|
+
const result = {
|
|
19374
|
+
removed: [],
|
|
19375
|
+
skipped: []
|
|
19376
|
+
};
|
|
19377
|
+
if (readIfExists(paths.extension) != null) applyRemoval(result, opts, paths.extension, null);
|
|
19378
|
+
else result.skipped.push(paths.extension);
|
|
19379
|
+
return result;
|
|
19380
|
+
};
|
|
19381
|
+
//#endregion
|
|
19382
|
+
//#region src/hooks/install/windsurf.ts
|
|
19383
|
+
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
19384
|
+
const installWindsurf = (opts) => {
|
|
19385
|
+
if (opts.scope !== "project") return {
|
|
19386
|
+
wrote: [],
|
|
19387
|
+
skipped: [],
|
|
19388
|
+
planned: [{
|
|
19389
|
+
path: ".windsurfrules",
|
|
19390
|
+
summary: "Windsurf is project-scope only; pass --project"
|
|
19391
|
+
}]
|
|
19392
|
+
};
|
|
19393
|
+
return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
|
|
19394
|
+
};
|
|
19395
|
+
const uninstallWindsurf = (opts) => {
|
|
19396
|
+
if (opts.scope !== "project") return {
|
|
19397
|
+
removed: [],
|
|
19398
|
+
skipped: []
|
|
19399
|
+
};
|
|
19400
|
+
return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
|
|
19401
|
+
};
|
|
19402
|
+
//#endregion
|
|
19403
|
+
//#region src/hooks/install/registry.ts
|
|
19404
|
+
const ALL_AGENTS = [
|
|
19405
|
+
"claude",
|
|
19406
|
+
"cursor",
|
|
19407
|
+
"gemini",
|
|
19408
|
+
"pi",
|
|
19409
|
+
"codex",
|
|
19410
|
+
"windsurf",
|
|
19411
|
+
"cline",
|
|
19412
|
+
"kilocode",
|
|
19413
|
+
"antigravity",
|
|
19414
|
+
"copilot"
|
|
19415
|
+
];
|
|
19416
|
+
const AGENTS_PROJECT_ONLY = [
|
|
19417
|
+
"windsurf",
|
|
19418
|
+
"cline",
|
|
19419
|
+
"kilocode",
|
|
19420
|
+
"antigravity",
|
|
19421
|
+
"copilot"
|
|
19422
|
+
];
|
|
19423
|
+
const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
19424
|
+
"claude",
|
|
19425
|
+
"cursor",
|
|
19426
|
+
"gemini",
|
|
19427
|
+
"pi",
|
|
19428
|
+
"codex"
|
|
19429
|
+
];
|
|
19430
|
+
const paths = {
|
|
19431
|
+
claude: (opts) => {
|
|
19432
|
+
const p = resolveClaudePaths(opts);
|
|
19433
|
+
return [
|
|
19434
|
+
p.settings,
|
|
19435
|
+
p.aislopMd,
|
|
19436
|
+
p.claudeMd
|
|
19437
|
+
];
|
|
19438
|
+
},
|
|
19439
|
+
cursor: (opts) => {
|
|
19440
|
+
const p = resolveCursorPaths(opts);
|
|
19441
|
+
return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
|
|
19442
|
+
},
|
|
19443
|
+
gemini: (opts) => {
|
|
19444
|
+
const p = resolveGeminiPaths(opts);
|
|
19445
|
+
return [
|
|
19446
|
+
p.settings,
|
|
19447
|
+
p.aislopMd,
|
|
19448
|
+
p.geminiMd
|
|
19449
|
+
];
|
|
19450
|
+
},
|
|
19451
|
+
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
19452
|
+
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
19453
|
+
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
19454
|
+
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
19455
|
+
kilocode: (opts) => [resolveKilocodePaths(opts).rules],
|
|
19456
|
+
antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
|
|
19457
|
+
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
19458
|
+
};
|
|
19459
|
+
const REGISTRY = {
|
|
19460
|
+
claude: {
|
|
19461
|
+
install: installClaude,
|
|
19462
|
+
uninstall: uninstallClaude,
|
|
19463
|
+
paths: paths.claude
|
|
19464
|
+
},
|
|
19465
|
+
cursor: {
|
|
19466
|
+
install: installCursor,
|
|
19467
|
+
uninstall: uninstallCursor,
|
|
19468
|
+
paths: paths.cursor
|
|
19469
|
+
},
|
|
19470
|
+
gemini: {
|
|
19471
|
+
install: installGemini,
|
|
19472
|
+
uninstall: uninstallGemini,
|
|
19473
|
+
paths: paths.gemini
|
|
19474
|
+
},
|
|
19475
|
+
pi: {
|
|
19476
|
+
install: installPi,
|
|
19477
|
+
uninstall: uninstallPi,
|
|
19478
|
+
paths: paths.pi
|
|
19479
|
+
},
|
|
19480
|
+
codex: {
|
|
19481
|
+
install: installCodex,
|
|
19482
|
+
uninstall: uninstallCodex,
|
|
19483
|
+
paths: paths.codex
|
|
19484
|
+
},
|
|
19485
|
+
windsurf: {
|
|
19486
|
+
install: installWindsurf,
|
|
19487
|
+
uninstall: uninstallWindsurf,
|
|
19488
|
+
paths: paths.windsurf
|
|
19489
|
+
},
|
|
19490
|
+
cline: {
|
|
19491
|
+
install: installCline,
|
|
19492
|
+
uninstall: uninstallCline,
|
|
19493
|
+
paths: paths.cline
|
|
19494
|
+
},
|
|
19495
|
+
kilocode: {
|
|
19496
|
+
install: installKilocode,
|
|
19497
|
+
uninstall: uninstallKilocode,
|
|
19498
|
+
paths: paths.kilocode
|
|
19499
|
+
},
|
|
19500
|
+
antigravity: {
|
|
19501
|
+
install: installAntigravity,
|
|
19502
|
+
uninstall: uninstallAntigravity,
|
|
19503
|
+
paths: paths.antigravity
|
|
19504
|
+
},
|
|
19505
|
+
copilot: {
|
|
19506
|
+
install: installCopilot,
|
|
19507
|
+
uninstall: uninstallCopilot,
|
|
19508
|
+
paths: paths.copilot
|
|
19509
|
+
}
|
|
19510
|
+
};
|
|
19511
|
+
const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
|
|
19512
|
+
const HOOK_MARKERS = [
|
|
19513
|
+
"__raigal",
|
|
19514
|
+
"raigal:begin",
|
|
19515
|
+
"@RAIGAL.md",
|
|
19516
|
+
"__aislop",
|
|
19517
|
+
"aislop:begin",
|
|
19518
|
+
"@AISLOP.md"
|
|
19519
|
+
];
|
|
19520
|
+
const hasHook = (target) => {
|
|
19521
|
+
try {
|
|
19522
|
+
if (!fs.statSync(target).isFile()) return false;
|
|
19523
|
+
const base = path.basename(target).toLowerCase();
|
|
19524
|
+
if (base.startsWith("raigal") || base.startsWith("aislop")) return true;
|
|
19525
|
+
const content = fs.readFileSync(target, "utf8");
|
|
19526
|
+
return HOOK_MARKERS.some((marker) => content.includes(marker));
|
|
19527
|
+
} catch {
|
|
19528
|
+
return false;
|
|
19529
|
+
}
|
|
19530
|
+
};
|
|
19531
|
+
const detectionScopes = (agent) => AGENTS_SUPPORTING_BOTH_SCOPES.includes(agent) ? ["global", "project"] : [defaultScopeFor(agent)];
|
|
19532
|
+
const detectRaigalHooks = (opts) => ALL_AGENTS.filter((agent) => detectionScopes(agent).some((scope) => REGISTRY[agent].paths({
|
|
19533
|
+
home: opts.home,
|
|
19534
|
+
cwd: opts.cwd,
|
|
19535
|
+
scope
|
|
19536
|
+
}).some(hasHook)));
|
|
18201
19537
|
//#endregion
|
|
18202
19538
|
//#region src/output/terminal.ts
|
|
18203
19539
|
const groupBy = (items, key) => {
|
|
@@ -18365,27 +19701,16 @@ const printEngineStatus = (result) => {
|
|
|
18365
19701
|
}
|
|
18366
19702
|
};
|
|
18367
19703
|
//#endregion
|
|
18368
|
-
//#region src/
|
|
18369
|
-
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
if (!override) {
|
|
18379
|
-
result.push(diagnostic);
|
|
18380
|
-
continue;
|
|
18381
|
-
}
|
|
18382
|
-
if (override === "off") continue;
|
|
18383
|
-
result.push(override === diagnostic.severity ? diagnostic : {
|
|
18384
|
-
...diagnostic,
|
|
18385
|
-
severity: override
|
|
18386
|
-
});
|
|
18387
|
-
}
|
|
18388
|
-
return result;
|
|
19704
|
+
//#region src/commands/scan-hook-nudge.ts
|
|
19705
|
+
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
19706
|
+
const buildHookNudge = (ctx) => {
|
|
19707
|
+
if (!shouldShowHookNudge(ctx)) return null;
|
|
19708
|
+
return [
|
|
19709
|
+
"",
|
|
19710
|
+
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
19711
|
+
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
19712
|
+
""
|
|
19713
|
+
].join("\n");
|
|
18389
19714
|
};
|
|
18390
19715
|
//#endregion
|
|
18391
19716
|
//#region src/commands/scan-coverage.ts
|
|
@@ -18679,6 +20004,9 @@ const RULE_LABELS = {
|
|
|
18679
20004
|
"security/shell-injection": "Possible shell injection",
|
|
18680
20005
|
"security/unsafe-deserialization": "Unsafe deserialization",
|
|
18681
20006
|
"security/unsafe-c-call": "Memory-unsafe C call",
|
|
20007
|
+
"security/prompt-injection": "Prompt injection (untrusted system prompt)",
|
|
20008
|
+
"security/unsandboxed-agent-tool": "Unsandboxed agent code execution tool",
|
|
20009
|
+
"security/invisible-unicode-tags": "Invisible Unicode tag character (stealth injection)",
|
|
18682
20010
|
"dotnet/projects-skipped": "C# projects skipped by build-backed lint",
|
|
18683
20011
|
"cppcheck/chunks-skipped": "C/C++ files skipped by cppcheck",
|
|
18684
20012
|
"eslint/no-undef": "Undefined identifier",
|
|
@@ -18767,6 +20095,9 @@ const RULE_DESCRIPTIONS = {
|
|
|
18767
20095
|
"security/shell-injection": "Shell command is built from unsanitized input.",
|
|
18768
20096
|
"security/unsafe-deserialization": "Legacy .NET formatter can deserialize arbitrary types (RCE).",
|
|
18769
20097
|
"security/unsafe-c-call": "Unbounded C string/exec function with buffer-overflow or shell risk.",
|
|
20098
|
+
"security/prompt-injection": "Untrusted dynamic variable interpolation into system or developer prompt.",
|
|
20099
|
+
"security/unsandboxed-agent-tool": "Agent executes code or commands on host without container or interactive gate.",
|
|
20100
|
+
"security/invisible-unicode-tags": "Invisible Unicode Tag block (U+E0000-U+E007F) used for prompt injection or ASCII smuggling.",
|
|
18770
20101
|
"dotnet/projects-skipped": "Build-backed C# lint only ran on restored projects; the rest were skipped.",
|
|
18771
20102
|
"cppcheck/chunks-skipped": "A cppcheck invocation timed out or failed on one or more chunks; their files were not analyzed.",
|
|
18772
20103
|
"oxlint/*": "JavaScript/TypeScript lint finding from oxlint.",
|
|
@@ -18841,7 +20172,8 @@ const renderActionRows = (steps, t) => {
|
|
|
18841
20172
|
const renderTextSteps = (steps, t, s) => {
|
|
18842
20173
|
return steps.filter((step) => step.text).map((step) => {
|
|
18843
20174
|
const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
|
|
18844
|
-
|
|
20175
|
+
const tokenFor = step.emphasis === "primary" ? "accent" : "muted";
|
|
20176
|
+
return ` ${style(t, tokenFor, glyph)} ${highlightAislop(step.text ?? "", t)}`;
|
|
18845
20177
|
});
|
|
18846
20178
|
};
|
|
18847
20179
|
const renderSummary = (input, deps = {}) => {
|
|
@@ -18898,10 +20230,14 @@ const renderSummary = (input, deps = {}) => {
|
|
|
18898
20230
|
return lines.join("\n");
|
|
18899
20231
|
};
|
|
18900
20232
|
const renderStarCta = (deps = {}) => {
|
|
18901
|
-
|
|
20233
|
+
const t = deps.theme ?? theme;
|
|
20234
|
+
const repository = terminalLink("https://raigal.dev");
|
|
20235
|
+
return `\n ${style(t, "muted", `★ Found this useful? Consider purchasing this software at ${repository}`)}\n`;
|
|
18902
20236
|
};
|
|
18903
20237
|
const renderTeamCta = (deps = {}) => {
|
|
18904
|
-
|
|
20238
|
+
const t = deps.theme ?? theme;
|
|
20239
|
+
const href = terminalLink("https://raigal.dev/contact?intent=team-baseline");
|
|
20240
|
+
return `\n ${style(t, "muted", `→ Using raigal with a team? Get a 14-day team baseline at ${href}`)}\n`;
|
|
18905
20241
|
};
|
|
18906
20242
|
const renderCleanRun = (input, deps = {}) => {
|
|
18907
20243
|
const t = deps.theme ?? theme;
|
|
@@ -18960,7 +20296,7 @@ const buildScanNextSteps = (input) => {
|
|
|
18960
20296
|
emphasis: "primary",
|
|
18961
20297
|
label: "Provider",
|
|
18962
20298
|
command: `${input.invocation} agent --provider codex`,
|
|
18963
|
-
detail: "or use --provider claude/opencode"
|
|
20299
|
+
detail: "or use --provider claude/opencode/pi"
|
|
18964
20300
|
}, {
|
|
18965
20301
|
emphasis: "muted",
|
|
18966
20302
|
label: "Preview",
|
|
@@ -19119,13 +20455,15 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19119
20455
|
const configDir = findConfigDir(resolvedDir);
|
|
19120
20456
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
19121
20457
|
const engineConfig = {
|
|
20458
|
+
overrides: config.overrides,
|
|
20459
|
+
rules: config.rules,
|
|
19122
20460
|
quality: config.quality,
|
|
19123
20461
|
security: config.security,
|
|
19124
20462
|
lint: config.lint,
|
|
19125
20463
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0,
|
|
19126
20464
|
engines: config.engines
|
|
19127
20465
|
};
|
|
19128
|
-
const
|
|
20466
|
+
const severityAdjusted = (await runEnginesWithProgress({
|
|
19129
20467
|
rootDirectory: resolvedDir,
|
|
19130
20468
|
languages: projectInfo.languages,
|
|
19131
20469
|
frameworks: projectInfo.frameworks,
|
|
@@ -19140,8 +20478,9 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19140
20478
|
config: engineConfig
|
|
19141
20479
|
}, config.engines, machineOutput)).map((result) => ({
|
|
19142
20480
|
...result,
|
|
19143
|
-
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
19144
|
-
}))
|
|
20481
|
+
diagnostics: config.overrides.length ? result.diagnostics : applyRuleSeverities(result.diagnostics, config.rules)
|
|
20482
|
+
}));
|
|
20483
|
+
const { results: unannotated, suppressedCount } = applySuppressions(severityAdjusted, resolvedDir);
|
|
19145
20484
|
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via raigal-ignore directives`);
|
|
19146
20485
|
const changeMap = options.changes && !options.staged ? getChangedLineMap(resolvedDir, options.base) : null;
|
|
19147
20486
|
const results = changeMap ? unannotated.map((result) => ({
|
|
@@ -19178,6 +20517,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19178
20517
|
engineIssues[r.engine] = r.diagnostics.length;
|
|
19179
20518
|
engineTimings[r.engine] = Math.round(r.elapsed);
|
|
19180
20519
|
}
|
|
20520
|
+
const enginesFailed = results.filter((r) => r.failed).map((r) => r.engine);
|
|
19181
20521
|
const completion = {
|
|
19182
20522
|
exitCode,
|
|
19183
20523
|
score: scoreable ? scoreResult.score : null,
|
|
@@ -19187,7 +20527,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19187
20527
|
warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
|
|
19188
20528
|
fixableCount: allDiagnostics.filter((d) => d.fixable).length,
|
|
19189
20529
|
engineIssues,
|
|
19190
|
-
engineTimings
|
|
20530
|
+
engineTimings,
|
|
20531
|
+
...enginesFailed.length > 0 ? { properties: { engines_failed: enginesFailed } } : {}
|
|
19191
20532
|
};
|
|
19192
20533
|
recordScanToCloud({
|
|
19193
20534
|
results,
|
|
@@ -19200,12 +20541,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19200
20541
|
scoreFileCount
|
|
19201
20542
|
});
|
|
19202
20543
|
if (options.sarif) {
|
|
19203
|
-
const { buildSarifLog } = await import("./sarif-
|
|
20544
|
+
const { buildSarifLog } = await import("./sarif-CpuYM3aK.js");
|
|
19204
20545
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
19205
20546
|
return completion;
|
|
19206
20547
|
}
|
|
19207
20548
|
if (options.json) {
|
|
19208
|
-
const { buildJsonOutput } = await import("./json-
|
|
20549
|
+
const { buildJsonOutput } = await import("./json-BflWt4dX.js");
|
|
19209
20550
|
const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
|
|
19210
20551
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
19211
20552
|
return completion;
|
|
@@ -19238,6 +20579,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19238
20579
|
printBrand: options.printBrand,
|
|
19239
20580
|
showPilotInvitation
|
|
19240
20581
|
}));
|
|
20582
|
+
if (options.command !== "ci" && options.printBrand !== false) {
|
|
20583
|
+
const nudge = buildHookNudge({
|
|
20584
|
+
installedAgentCount: detectRaigalHooks({
|
|
20585
|
+
home: os.homedir(),
|
|
20586
|
+
cwd: resolvedDir
|
|
20587
|
+
}).length,
|
|
20588
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
20589
|
+
isCi: isCiEnv(),
|
|
20590
|
+
invocation: detectInvocation()
|
|
20591
|
+
});
|
|
20592
|
+
if (nudge) process.stdout.write(nudge);
|
|
20593
|
+
}
|
|
19241
20594
|
return completion;
|
|
19242
20595
|
};
|
|
19243
20596
|
//#endregion
|
|
@@ -19347,7 +20700,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19347
20700
|
brand: options.printBrand !== false
|
|
19348
20701
|
}));
|
|
19349
20702
|
const safe = Boolean(options.safe);
|
|
19350
|
-
const
|
|
20703
|
+
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
20704
|
+
const scope = collectFixFileScope(resolvedDir, excludePatterns, config.include, options);
|
|
19351
20705
|
const scopedProjectInfo = scope ? {
|
|
19352
20706
|
...projectInfo,
|
|
19353
20707
|
languages: detectSourceLanguages([...scope.files, ...scope.testFiles])
|
|
@@ -19402,6 +20756,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19402
20756
|
const configDir = findConfigDir(resolvedDir);
|
|
19403
20757
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
19404
20758
|
const engineConfig = {
|
|
20759
|
+
overrides: config.overrides,
|
|
20760
|
+
rules: config.rules,
|
|
19405
20761
|
quality: config.quality,
|
|
19406
20762
|
security: config.security,
|
|
19407
20763
|
lint: config.lint,
|
|
@@ -19427,7 +20783,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19427
20783
|
status: "done",
|
|
19428
20784
|
label: "Verification complete"
|
|
19429
20785
|
});
|
|
19430
|
-
const scanResults = appendPostFixLintResult(verificationResults, collectPostFixLintDiagnostics(steps));
|
|
20786
|
+
const scanResults = appendPostFixLintResult(verificationResults, applyFileOverrides(collectPostFixLintDiagnostics(steps), context));
|
|
19431
20787
|
const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
|
|
19432
20788
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, scope ? scope.scoreFileCount : projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
19433
20789
|
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
@@ -19489,6 +20845,13 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19489
20845
|
};
|
|
19490
20846
|
};
|
|
19491
20847
|
//#endregion
|
|
20848
|
+
//#region src/ui/prompts.ts
|
|
20849
|
+
const runCancellable = async (fn) => {
|
|
20850
|
+
const value = await fn();
|
|
20851
|
+
if (isCancel(value)) return void 0;
|
|
20852
|
+
return value;
|
|
20853
|
+
};
|
|
20854
|
+
//#endregion
|
|
19492
20855
|
//#region src/commands/init.ts
|
|
19493
20856
|
const buildInitSuccessRender = (input) => {
|
|
19494
20857
|
const deps = {
|
|
@@ -19558,13 +20921,13 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
|
|
|
19558
20921
|
};
|
|
19559
20922
|
};
|
|
19560
20923
|
const promptForConfigChoices = async () => {
|
|
19561
|
-
const enginesSelection = await multiselect({
|
|
20924
|
+
const enginesSelection = await runCancellable(() => multiselect({
|
|
19562
20925
|
message: "Which engines should run?",
|
|
19563
20926
|
options: ENGINE_CHOICES,
|
|
19564
20927
|
initialValues: DEFAULT_ENGINE_SELECTION,
|
|
19565
20928
|
required: false
|
|
19566
|
-
});
|
|
19567
|
-
if (isCancel(enginesSelection)) return null;
|
|
20929
|
+
}));
|
|
20930
|
+
if (enginesSelection === void 0 || isCancel(enginesSelection)) return null;
|
|
19568
20931
|
const failBelowRaw = await text({
|
|
19569
20932
|
message: "CI quality gate — fail the build when the score drops below (0-100)",
|
|
19570
20933
|
initialValue: "70",
|
|
@@ -19695,16 +21058,18 @@ const initCommand = async (directory, options = {}) => {
|
|
|
19695
21058
|
status: "done",
|
|
19696
21059
|
label: `Wrote ${CONFIG_DIR}/${CONFIG_FILE}`
|
|
19697
21060
|
}];
|
|
19698
|
-
if (choices.engines.includes("architecture"))
|
|
19699
|
-
fs.
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
|
|
21061
|
+
if (choices.engines.includes("architecture")) {
|
|
21062
|
+
if (!fs.existsSync(rulesPath)) {
|
|
21063
|
+
fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
|
|
21064
|
+
steps.push({
|
|
21065
|
+
status: "done",
|
|
21066
|
+
label: `Wrote ${CONFIG_DIR}/${RULES_FILE}`
|
|
21067
|
+
});
|
|
21068
|
+
} else steps.push({
|
|
21069
|
+
status: "skipped",
|
|
21070
|
+
label: `${CONFIG_DIR}/${RULES_FILE} already exists`
|
|
19703
21071
|
});
|
|
19704
|
-
}
|
|
19705
|
-
status: "skipped",
|
|
19706
|
-
label: `${CONFIG_DIR}/${RULES_FILE} already exists`
|
|
19707
|
-
});
|
|
21072
|
+
}
|
|
19708
21073
|
const workflowResult = writeGithubWorkflow(resolvedDir, choices.writeGithubWorkflow);
|
|
19709
21074
|
if (workflowResult.status === "written") steps.push({
|
|
19710
21075
|
status: "done",
|
|
@@ -19898,8 +21263,10 @@ const runSearchPrompt = async (options) => {
|
|
|
19898
21263
|
}
|
|
19899
21264
|
if (key.name === "space" && options.mode === "multi") {
|
|
19900
21265
|
const item = filtered[cursor];
|
|
19901
|
-
if (item)
|
|
19902
|
-
|
|
21266
|
+
if (item) {
|
|
21267
|
+
if (selected.has(item.value)) selected.delete(item.value);
|
|
21268
|
+
else selected.add(item.value);
|
|
21269
|
+
}
|
|
19903
21270
|
render();
|
|
19904
21271
|
return;
|
|
19905
21272
|
}
|
|
@@ -20095,7 +21462,8 @@ const BUILTIN_RULES = [
|
|
|
20095
21462
|
"typescript/*",
|
|
20096
21463
|
"dotnet/*",
|
|
20097
21464
|
"cppcheck/*",
|
|
20098
|
-
"clang-tidy/*"
|
|
21465
|
+
"clang-tidy/*",
|
|
21466
|
+
"expo-doctor/*"
|
|
20099
21467
|
]
|
|
20100
21468
|
},
|
|
20101
21469
|
{
|
|
@@ -20194,7 +21562,10 @@ const BUILTIN_RULES = [
|
|
|
20194
21562
|
"security/shell-injection",
|
|
20195
21563
|
"security/unsafe-deserialization",
|
|
20196
21564
|
"security/unsafe-c-call",
|
|
20197
|
-
"security/dependency-audit-skipped"
|
|
21565
|
+
"security/dependency-audit-skipped",
|
|
21566
|
+
"security/prompt-injection",
|
|
21567
|
+
"security/unsandboxed-agent-tool",
|
|
21568
|
+
"security/invisible-unicode-tags"
|
|
20198
21569
|
]
|
|
20199
21570
|
}
|
|
20200
21571
|
];
|
|
@@ -20204,7 +21575,8 @@ const collectRuleEntries = (directory) => {
|
|
|
20204
21575
|
for (const { engine, rules } of BUILTIN_RULES) for (const rule of rules) entries.push(toRuleEntry(engine, rule));
|
|
20205
21576
|
const configDir = findConfigDir(resolvedDir);
|
|
20206
21577
|
if (configDir) {
|
|
20207
|
-
const
|
|
21578
|
+
const rulesPath = path.join(configDir, RULES_FILE);
|
|
21579
|
+
const archRules = loadArchitectureRules(rulesPath);
|
|
20208
21580
|
for (const rule of archRules) entries.push({
|
|
20209
21581
|
id: `arch/${rule.name}`,
|
|
20210
21582
|
engine: "architecture",
|