@methiu/raigal 0.2.0 → 0.2.2
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/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 +1 -6
- package/dist/cli.js +930 -535
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1166 -52
- package/dist/{json-C9fBWj3V.js → json-BaycSoJP.js} +2 -1
- package/dist/{json-C2IwKr9o.js → json-DBoMe4xB.js} +3 -2
- package/dist/mcp.js +205 -27
- package/dist/{sarif-Do8E1xhZ.js → sarif-Ba5D8TPH.js} +1 -1
- package/dist/{version-v4OAaBLZ.js → version-9Cnt1TiK.js} +1 -1
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as APP_VERSION } from "./version-
|
|
1
|
+
import { t as APP_VERSION } from "./version-9Cnt1TiK.js";
|
|
2
2
|
import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-M5fUTE-C.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";
|
|
@@ -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";
|
|
@@ -318,6 +319,16 @@ const RuleSeverityOverride = z.enum([
|
|
|
318
319
|
"off"
|
|
319
320
|
]);
|
|
320
321
|
const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
|
|
322
|
+
const FileOverrideSchema = z.object({
|
|
323
|
+
files: z.array(z.string().min(1)).min(1),
|
|
324
|
+
quality: z.object({
|
|
325
|
+
maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
|
|
326
|
+
maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
|
|
327
|
+
maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
|
|
328
|
+
maxParams: QualitySchema.shape.maxParams.unwrap().optional()
|
|
329
|
+
}).optional(),
|
|
330
|
+
rules: z.record(z.string(), RuleSeverityOverride).optional()
|
|
331
|
+
});
|
|
321
332
|
const AislopConfigSchema = z.object({
|
|
322
333
|
version: z.number().default(1),
|
|
323
334
|
extends: z.union([z.string(), z.array(z.string())]).optional(),
|
|
@@ -373,6 +384,7 @@ const AislopConfigSchema = z.object({
|
|
|
373
384
|
})),
|
|
374
385
|
telemetry: TelemetrySchema.default(() => ({ enabled: true })),
|
|
375
386
|
rules: RulesSchema,
|
|
387
|
+
overrides: z.array(FileOverrideSchema).default(() => []),
|
|
376
388
|
exclude: z.array(z.string()).default(() => [
|
|
377
389
|
"node_modules",
|
|
378
390
|
".git",
|
|
@@ -674,7 +686,7 @@ const highlightRaigal = (text, theme, baseToken) => {
|
|
|
674
686
|
const tail = text.slice(lastIndex);
|
|
675
687
|
return out + (baseToken ? style(theme, baseToken, tail) : tail);
|
|
676
688
|
};
|
|
677
|
-
const highlightAislop = highlightRaigal;
|
|
689
|
+
const highlightAislop = (...args) => highlightRaigal(...args);
|
|
678
690
|
//#endregion
|
|
679
691
|
//#region src/ui/width.ts
|
|
680
692
|
const ANSI_RE$1 = new RegExp(`\\[[0-9;]*m`, "g");
|
|
@@ -938,13 +950,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
|
|
|
938
950
|
const normalized = withoutProjectPrefix.slice(0, end);
|
|
939
951
|
if (normalized.length === 0) return [];
|
|
940
952
|
if (normalized.length > 256) return [];
|
|
941
|
-
if (
|
|
953
|
+
if (picomatch.scan(normalized).isGlob) return [normalized];
|
|
942
954
|
if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
|
|
943
955
|
return supportedGlobPatterns([normalized, `${normalized}/**`]);
|
|
944
956
|
});
|
|
945
957
|
const isPathExcluded = (relativePath, normalizedPatterns) => {
|
|
946
958
|
if (normalizedPatterns.length === 0) return false;
|
|
947
|
-
return
|
|
959
|
+
return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
|
|
948
960
|
};
|
|
949
961
|
const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
|
|
950
962
|
if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
|
|
@@ -1312,12 +1324,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
|
|
|
1312
1324
|
const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
|
|
1313
1325
|
if (normalized === "" || normalized === ".") return ["**"];
|
|
1314
1326
|
if (normalized.length > 256) return [];
|
|
1315
|
-
if (
|
|
1327
|
+
if (picomatch.scan(normalized).isGlob) return [normalized];
|
|
1316
1328
|
return supportedGlobPatterns([normalized, `${normalized}/**`]);
|
|
1317
1329
|
});
|
|
1318
1330
|
const createPathMatcher = (patterns) => {
|
|
1319
|
-
|
|
1320
|
-
|
|
1331
|
+
if (patterns.length === 0) return () => false;
|
|
1332
|
+
const isMatch = picomatch(patterns, { dot: true });
|
|
1333
|
+
return (filePath) => isMatch(filePath);
|
|
1321
1334
|
};
|
|
1322
1335
|
const filterFiles = (rootDirectory, files, options) => {
|
|
1323
1336
|
const extraExtensions = new Set(options.extraExtensions);
|
|
@@ -1800,7 +1813,7 @@ const isRootBoundedTarget = (candidate, rootDirectory) => {
|
|
|
1800
1813
|
};
|
|
1801
1814
|
//#endregion
|
|
1802
1815
|
//#region src/utils/aislop-ignore.ts
|
|
1803
|
-
const
|
|
1816
|
+
const readAislopIgnorePatterns = (rootDirectory) => {
|
|
1804
1817
|
const patterns = [];
|
|
1805
1818
|
for (const fileName of [".raigalignore", ".aislopignore"]) {
|
|
1806
1819
|
const targetPath = safeProjectFilePath(path.join(rootDirectory, fileName), rootDirectory);
|
|
@@ -1811,7 +1824,6 @@ const readRaigalIgnorePatterns = (rootDirectory) => {
|
|
|
1811
1824
|
}
|
|
1812
1825
|
return patterns;
|
|
1813
1826
|
};
|
|
1814
|
-
const readAislopIgnorePatterns = readRaigalIgnorePatterns;
|
|
1815
1827
|
//#endregion
|
|
1816
1828
|
//#region src/utils/source-file-context.ts
|
|
1817
1829
|
const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
|
|
@@ -2639,6 +2651,132 @@ const doctorCommand = async (directory, options = {}) => {
|
|
|
2639
2651
|
}));
|
|
2640
2652
|
};
|
|
2641
2653
|
//#endregion
|
|
2654
|
+
//#region src/utils/globs.ts
|
|
2655
|
+
const MAX_BRACE_EXPANSIONS = 256;
|
|
2656
|
+
const braceListAt = (pattern, start) => {
|
|
2657
|
+
let depth = 0;
|
|
2658
|
+
const commas = [];
|
|
2659
|
+
for (let i = start; i < pattern.length; i += 1) {
|
|
2660
|
+
const char = pattern[i];
|
|
2661
|
+
if (char === "\\") i += 1;
|
|
2662
|
+
else if (char === "{") depth += 1;
|
|
2663
|
+
else if (char === ",") {
|
|
2664
|
+
if (depth === 1) commas.push(i);
|
|
2665
|
+
} else if (char === "}") {
|
|
2666
|
+
depth -= 1;
|
|
2667
|
+
if (depth === 0) return commas.length > 0 ? {
|
|
2668
|
+
start,
|
|
2669
|
+
end: i,
|
|
2670
|
+
commas
|
|
2671
|
+
} : null;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
};
|
|
2676
|
+
const findBraceList = (pattern) => {
|
|
2677
|
+
for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
|
|
2678
|
+
if (start > 0 && pattern[start - 1] === "\\") continue;
|
|
2679
|
+
const list = braceListAt(pattern, start);
|
|
2680
|
+
if (list) return list;
|
|
2681
|
+
}
|
|
2682
|
+
return null;
|
|
2683
|
+
};
|
|
2684
|
+
const expandInto = (pattern, results) => {
|
|
2685
|
+
const list = findBraceList(pattern);
|
|
2686
|
+
if (!list) {
|
|
2687
|
+
results.push(pattern);
|
|
2688
|
+
return results.length <= MAX_BRACE_EXPANSIONS;
|
|
2689
|
+
}
|
|
2690
|
+
const bounds = [
|
|
2691
|
+
list.start,
|
|
2692
|
+
...list.commas,
|
|
2693
|
+
list.end
|
|
2694
|
+
];
|
|
2695
|
+
const prefix = pattern.slice(0, list.start);
|
|
2696
|
+
const suffix = pattern.slice(list.end + 1);
|
|
2697
|
+
for (let i = 0; i < bounds.length - 1; i += 1) {
|
|
2698
|
+
const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
|
|
2699
|
+
if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
|
|
2700
|
+
}
|
|
2701
|
+
return true;
|
|
2702
|
+
};
|
|
2703
|
+
const expandBraceLists = (pattern) => {
|
|
2704
|
+
const results = [];
|
|
2705
|
+
return expandInto(pattern, results) ? results : [pattern];
|
|
2706
|
+
};
|
|
2707
|
+
const isNegated = (pattern) => picomatch.scan(pattern).negated;
|
|
2708
|
+
const matchesGlobList = (value, patterns, options = {}) => {
|
|
2709
|
+
const expanded = patterns.flatMap(expandBraceLists);
|
|
2710
|
+
const positives = expanded.filter((pattern) => !isNegated(pattern));
|
|
2711
|
+
const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
|
|
2712
|
+
const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
|
|
2713
|
+
const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
|
|
2714
|
+
return included && !excluded;
|
|
2715
|
+
};
|
|
2716
|
+
//#endregion
|
|
2717
|
+
//#region src/scoring/rule-severity.ts
|
|
2718
|
+
/**
|
|
2719
|
+
* Apply per-rule severity overrides from config: "off" drops the diagnostic,
|
|
2720
|
+
* "error"/"warning" rewrite its severity before scoring and rendering.
|
|
2721
|
+
*/
|
|
2722
|
+
const applyRuleSeverities = (diagnostics, overrides) => {
|
|
2723
|
+
if (Object.keys(overrides).length === 0) return diagnostics;
|
|
2724
|
+
const result = [];
|
|
2725
|
+
for (const diagnostic of diagnostics) {
|
|
2726
|
+
const override = overrides[diagnostic.rule];
|
|
2727
|
+
if (!override) {
|
|
2728
|
+
result.push(diagnostic);
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
if (override === "off") continue;
|
|
2732
|
+
result.push(override === diagnostic.severity ? diagnostic : {
|
|
2733
|
+
...diagnostic,
|
|
2734
|
+
severity: override
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
return result;
|
|
2738
|
+
};
|
|
2739
|
+
//#endregion
|
|
2740
|
+
//#region src/config/overrides.ts
|
|
2741
|
+
const overrideRelativePath = (rootDirectory, filePath) => {
|
|
2742
|
+
const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
|
|
2743
|
+
const root = rootDirectory.replaceAll("\\", "/");
|
|
2744
|
+
const file = filePath.replaceAll("\\", "/");
|
|
2745
|
+
return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
|
|
2746
|
+
};
|
|
2747
|
+
const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
|
|
2748
|
+
const resolveFilePolicy = (config, relativePath) => {
|
|
2749
|
+
let quality = config.quality;
|
|
2750
|
+
let rules = config.rules ?? {};
|
|
2751
|
+
if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
|
|
2752
|
+
quality,
|
|
2753
|
+
rules
|
|
2754
|
+
};
|
|
2755
|
+
for (const entry of config.overrides ?? []) {
|
|
2756
|
+
if (!matchesEntry(relativePath, entry.files)) continue;
|
|
2757
|
+
if (entry.quality) quality = {
|
|
2758
|
+
...quality,
|
|
2759
|
+
...entry.quality
|
|
2760
|
+
};
|
|
2761
|
+
if (entry.rules) rules = {
|
|
2762
|
+
...rules,
|
|
2763
|
+
...entry.rules
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
return {
|
|
2767
|
+
quality,
|
|
2768
|
+
rules
|
|
2769
|
+
};
|
|
2770
|
+
};
|
|
2771
|
+
const applyFileOverrides = (diagnostics, context) => {
|
|
2772
|
+
if (!context.config.overrides?.length) return diagnostics;
|
|
2773
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
2774
|
+
const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
|
|
2775
|
+
const { rules } = resolveFilePolicy(context.config, relativePath);
|
|
2776
|
+
return applyRuleSeverities([diagnostic], rules);
|
|
2777
|
+
});
|
|
2778
|
+
};
|
|
2779
|
+
//#endregion
|
|
2642
2780
|
//#region src/utils/string-literals.ts
|
|
2643
2781
|
const consumeQuotedString = (content, start, quote) => {
|
|
2644
2782
|
const len = content.length;
|
|
@@ -10381,10 +10519,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
|
10381
10519
|
};
|
|
10382
10520
|
const checkComplexity = async (context) => {
|
|
10383
10521
|
const files = getSourceFiles(context);
|
|
10384
|
-
const limits = context.config.quality;
|
|
10385
10522
|
const diagnostics = [];
|
|
10386
10523
|
for (const filePath of files) {
|
|
10387
10524
|
if (isAutoGenerated(filePath)) continue;
|
|
10525
|
+
const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
|
|
10388
10526
|
diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
|
|
10389
10527
|
}
|
|
10390
10528
|
return diagnostics;
|
|
@@ -11276,13 +11414,23 @@ const withReportPath = async (run) => {
|
|
|
11276
11414
|
};
|
|
11277
11415
|
const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
|
|
11278
11416
|
const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
|
|
11417
|
+
const expandBraces = (pattern, depth = 0) => {
|
|
11418
|
+
if (depth > 10) return [pattern];
|
|
11419
|
+
const match = /\{([^{}]+)\}/.exec(pattern);
|
|
11420
|
+
if (!match) return [pattern];
|
|
11421
|
+
const [fullMatch, options] = match;
|
|
11422
|
+
const parts = options.split(",");
|
|
11423
|
+
const prefix = pattern.slice(0, match.index);
|
|
11424
|
+
const suffix = pattern.slice(match.index + fullMatch.length);
|
|
11425
|
+
return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
|
|
11426
|
+
};
|
|
11279
11427
|
const buildDotnetFormatExcludeScope = (excludePatterns) => {
|
|
11280
11428
|
const scope = {
|
|
11281
11429
|
excludeArguments: [],
|
|
11282
11430
|
unsupportedPatterns: []
|
|
11283
11431
|
};
|
|
11284
11432
|
for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
|
|
11285
|
-
const expanded =
|
|
11433
|
+
const expanded = expandBraces(pattern);
|
|
11286
11434
|
if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
|
|
11287
11435
|
else scope.unsupportedPatterns.push(pattern);
|
|
11288
11436
|
}
|
|
@@ -13076,7 +13224,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
|
|
|
13076
13224
|
if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
|
|
13077
13225
|
return [pattern];
|
|
13078
13226
|
});
|
|
13079
|
-
const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 &&
|
|
13227
|
+
const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
|
|
13080
13228
|
const getOxlintTargets = (context) => {
|
|
13081
13229
|
const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
|
|
13082
13230
|
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));
|
|
@@ -13897,7 +14045,8 @@ const readRuntimeDependencies = (rootDir) => {
|
|
|
13897
14045
|
collectRuntimeNames(rootManifest, names);
|
|
13898
14046
|
const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
|
|
13899
14047
|
if (globs.length === 0) return { runtimeDependencies: names };
|
|
13900
|
-
|
|
14048
|
+
const isMatch = picomatch(globs, { dot: true });
|
|
14049
|
+
for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
|
|
13901
14050
|
const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
|
|
13902
14051
|
if (memberManifest) collectRuntimeNames(memberManifest, names);
|
|
13903
14052
|
}
|
|
@@ -14693,6 +14842,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14693
14842
|
const result = await engine.run(context);
|
|
14694
14843
|
result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
|
|
14695
14844
|
result.elapsed = performance.now() - start;
|
|
14845
|
+
result.diagnostics = applyFileOverrides(result.diagnostics, context);
|
|
14696
14846
|
onComplete?.(result);
|
|
14697
14847
|
return result;
|
|
14698
14848
|
} catch (error) {
|
|
@@ -14701,6 +14851,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14701
14851
|
diagnostics: [],
|
|
14702
14852
|
elapsed: performance.now() - start,
|
|
14703
14853
|
skipped: true,
|
|
14854
|
+
failed: true,
|
|
14704
14855
|
skipReason: error instanceof Error ? error.message : String(error)
|
|
14705
14856
|
};
|
|
14706
14857
|
onComplete?.(result);
|
|
@@ -14711,6 +14862,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
14711
14862
|
diagnostics: [],
|
|
14712
14863
|
elapsed: 0,
|
|
14713
14864
|
skipped: true,
|
|
14865
|
+
failed: true,
|
|
14714
14866
|
skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
14715
14867
|
});
|
|
14716
14868
|
return dedupeEquivalenceDiagnostics(dedupeOverlappingComments(dedupeCSharpAsync(stripSecretDiagnostics(finalResults))));
|
|
@@ -14929,6 +15081,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
|
14929
15081
|
"exit_code",
|
|
14930
15082
|
"duration_ms",
|
|
14931
15083
|
"error_kind",
|
|
15084
|
+
"error_name",
|
|
15085
|
+
"error_code",
|
|
15086
|
+
"failed_stage",
|
|
15087
|
+
"engines_failed",
|
|
14932
15088
|
"score",
|
|
14933
15089
|
"score_bucket",
|
|
14934
15090
|
"finding_count",
|
|
@@ -15157,6 +15313,8 @@ const buildCommandCompletedProps = (input) => {
|
|
|
15157
15313
|
if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
|
|
15158
15314
|
if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
|
|
15159
15315
|
if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
|
|
15316
|
+
if (input.errorName) props.error_name = input.errorName;
|
|
15317
|
+
if (input.errorCode) props.error_code = input.errorCode;
|
|
15160
15318
|
return props;
|
|
15161
15319
|
};
|
|
15162
15320
|
const errorKindFromException = (error) => {
|
|
@@ -15166,6 +15324,26 @@ const errorKindFromException = (error) => {
|
|
|
15166
15324
|
if (message.includes("engine") && message.includes("crash")) return "engine_crash";
|
|
15167
15325
|
return "unknown";
|
|
15168
15326
|
};
|
|
15327
|
+
const SAFE_ERROR_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
|
|
15328
|
+
const SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
|
|
15329
|
+
const errorIdentity = (error) => {
|
|
15330
|
+
let name;
|
|
15331
|
+
if (error instanceof Error && typeof error.name === "string" && error.name.length > 0) name = error.name;
|
|
15332
|
+
else if (error === null) name = "null";
|
|
15333
|
+
else if (typeof error === "object") name = "object";
|
|
15334
|
+
else name = typeof error;
|
|
15335
|
+
const identity = { error_name: SAFE_ERROR_NAME.test(name) ? name : "Error" };
|
|
15336
|
+
const code = error?.code;
|
|
15337
|
+
if (typeof code === "string" && SAFE_ERROR_CODE.test(code)) identity.error_code = code;
|
|
15338
|
+
else if (typeof code === "number" && Number.isFinite(code)) identity.error_code = String(code);
|
|
15339
|
+
return identity;
|
|
15340
|
+
};
|
|
15341
|
+
//#endregion
|
|
15342
|
+
//#region src/telemetry/fatal.ts
|
|
15343
|
+
const reportedErrors = /* @__PURE__ */ new Set();
|
|
15344
|
+
const markErrorReported = (error) => {
|
|
15345
|
+
reportedErrors.add(error);
|
|
15346
|
+
};
|
|
15169
15347
|
//#endregion
|
|
15170
15348
|
//#region src/telemetry/lifecycle.ts
|
|
15171
15349
|
const STARTED_FLUSH_TIMEOUT_MS = 100;
|
|
@@ -15176,6 +15354,7 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15176
15354
|
fileCount: start.fileCount,
|
|
15177
15355
|
properties: start.properties
|
|
15178
15356
|
});
|
|
15357
|
+
start.config;
|
|
15179
15358
|
track({
|
|
15180
15359
|
event: "cli_command_started",
|
|
15181
15360
|
properties: startProps,
|
|
@@ -15192,6 +15371,7 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15192
15371
|
startProps,
|
|
15193
15372
|
exitCode: result.exitCode,
|
|
15194
15373
|
durationMs,
|
|
15374
|
+
errorKind: result.errorKind,
|
|
15195
15375
|
score: result.score ?? void 0,
|
|
15196
15376
|
findingCount: result.findingCount,
|
|
15197
15377
|
errorCount: result.errorCount,
|
|
@@ -15209,16 +15389,21 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
15209
15389
|
await flushTelemetry();
|
|
15210
15390
|
return result;
|
|
15211
15391
|
} catch (error) {
|
|
15392
|
+
const durationMs = performance.now() - startedAt;
|
|
15393
|
+
const identity = errorIdentity(error);
|
|
15212
15394
|
track({
|
|
15213
15395
|
event: "cli_command_completed",
|
|
15214
15396
|
properties: buildCommandCompletedProps({
|
|
15215
15397
|
startProps,
|
|
15216
15398
|
exitCode: 1,
|
|
15217
|
-
durationMs
|
|
15218
|
-
errorKind: errorKindFromException(error)
|
|
15399
|
+
durationMs,
|
|
15400
|
+
errorKind: errorKindFromException(error),
|
|
15401
|
+
errorName: identity.error_name,
|
|
15402
|
+
errorCode: identity.error_code
|
|
15219
15403
|
}),
|
|
15220
15404
|
config: start.config
|
|
15221
15405
|
});
|
|
15406
|
+
markErrorReported(error);
|
|
15222
15407
|
await flushTelemetry();
|
|
15223
15408
|
throw error;
|
|
15224
15409
|
}
|
|
@@ -15714,7 +15899,9 @@ const createEngineContext = (rootDirectory, projectInfo, config, options = {}) =
|
|
|
15714
15899
|
quality: config.quality,
|
|
15715
15900
|
security: config.security,
|
|
15716
15901
|
lint: config.lint,
|
|
15717
|
-
engines: config.engines
|
|
15902
|
+
engines: config.engines,
|
|
15903
|
+
overrides: config.overrides,
|
|
15904
|
+
rules: config.rules
|
|
15718
15905
|
},
|
|
15719
15906
|
...options.scope ? {
|
|
15720
15907
|
files: options.scope.files,
|
|
@@ -16068,7 +16255,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
16068
16255
|
const claims = parsed.data;
|
|
16069
16256
|
const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
|
|
16070
16257
|
if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
|
|
16071
|
-
if (!isVersionAtLeast("0.2.
|
|
16258
|
+
if (!isVersionAtLeast("0.2.2", 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
16259
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
16073
16260
|
const grace = options.allowGraceSeconds ?? 0;
|
|
16074
16261
|
if (now > claims.exp + grace) {
|
|
@@ -16182,6 +16369,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
|
|
|
16182
16369
|
nowSeconds: now,
|
|
16183
16370
|
allowGraceSeconds: GRACE_PERIOD_SECONDS
|
|
16184
16371
|
});
|
|
16372
|
+
if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
|
|
16373
|
+
clearCachedEntitlement();
|
|
16374
|
+
throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
|
|
16375
|
+
}
|
|
16185
16376
|
return {
|
|
16186
16377
|
jwt: cached.entitlement,
|
|
16187
16378
|
claims: graceClaims,
|
|
@@ -16189,6 +16380,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
|
|
|
16189
16380
|
grace: now > graceClaims.exp
|
|
16190
16381
|
};
|
|
16191
16382
|
} catch (jwtErr) {
|
|
16383
|
+
if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
|
|
16192
16384
|
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
16385
|
}
|
|
16194
16386
|
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 +16395,8 @@ const getEntitlement = async () => {
|
|
|
16203
16395
|
} catch {
|
|
16204
16396
|
cachedClaims = null;
|
|
16205
16397
|
}
|
|
16206
|
-
|
|
16398
|
+
const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
|
|
16399
|
+
if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
|
|
16207
16400
|
jwt: cached.entitlement,
|
|
16208
16401
|
claims: cachedClaims,
|
|
16209
16402
|
fromCache: true
|
|
@@ -16212,7 +16405,7 @@ const getEntitlement = async () => {
|
|
|
16212
16405
|
if (!activeToken) {
|
|
16213
16406
|
oidcToken = await fetchGitHubOidcToken();
|
|
16214
16407
|
if (!oidcToken) {
|
|
16215
|
-
if (cached && cachedClaims) return {
|
|
16408
|
+
if (cached && cachedClaims && !isTrialExpired) return {
|
|
16216
16409
|
jwt: cached.entitlement,
|
|
16217
16410
|
claims: cachedClaims,
|
|
16218
16411
|
fromCache: true
|
|
@@ -16373,6 +16566,23 @@ const requireEntitlement = async (options = {}) => {
|
|
|
16373
16566
|
try {
|
|
16374
16567
|
const entitlement = await getEntitlement();
|
|
16375
16568
|
const tokenKind = getActiveToken()?.kind ?? "session";
|
|
16569
|
+
if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
|
|
16570
|
+
const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
|
|
16571
|
+
if (Date.now() > trialEnd) {
|
|
16572
|
+
const message = "Raigal license denied: Organization trial has expired";
|
|
16573
|
+
if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
|
|
16574
|
+
if (mode === "fatal") {
|
|
16575
|
+
process.stderr.write(`${message}\n`);
|
|
16576
|
+
process.exit(30);
|
|
16577
|
+
}
|
|
16578
|
+
process.stderr.write(`[raigal] ${message}\n`);
|
|
16579
|
+
return {
|
|
16580
|
+
ok: false,
|
|
16581
|
+
exitCode: 30,
|
|
16582
|
+
message
|
|
16583
|
+
};
|
|
16584
|
+
}
|
|
16585
|
+
}
|
|
16376
16586
|
const detectedOwner = detectLocalRepoOwner(directory);
|
|
16377
16587
|
if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
|
|
16378
16588
|
const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
|
|
@@ -16929,6 +17139,56 @@ const computeRemovalRange = (sourceFile, node, content) => {
|
|
|
16929
17139
|
end: finalEnd
|
|
16930
17140
|
};
|
|
16931
17141
|
};
|
|
17142
|
+
const isReferencedOutside = (sourceFile, statement, name) => {
|
|
17143
|
+
const start = statement.getStart(sourceFile);
|
|
17144
|
+
const end = statement.getEnd();
|
|
17145
|
+
let found = false;
|
|
17146
|
+
const visit = (node) => {
|
|
17147
|
+
if (found) return;
|
|
17148
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
17149
|
+
const pos = node.getStart(sourceFile);
|
|
17150
|
+
if (pos < start || pos >= end) found = true;
|
|
17151
|
+
return;
|
|
17152
|
+
}
|
|
17153
|
+
ts.forEachChild(node, visit);
|
|
17154
|
+
};
|
|
17155
|
+
visit(sourceFile);
|
|
17156
|
+
return found;
|
|
17157
|
+
};
|
|
17158
|
+
const exportKeywordRange = (sourceFile, statement, content) => {
|
|
17159
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
|
|
17160
|
+
if (modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) return null;
|
|
17161
|
+
const exportKeyword = modifiers.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
17162
|
+
if (!exportKeyword) return null;
|
|
17163
|
+
let end = exportKeyword.getEnd();
|
|
17164
|
+
while (end < content.length && (content[end] === " " || content[end] === " ")) end++;
|
|
17165
|
+
return {
|
|
17166
|
+
start: exportKeyword.getStart(sourceFile),
|
|
17167
|
+
end
|
|
17168
|
+
};
|
|
17169
|
+
};
|
|
17170
|
+
const removalFor = (sourceFile, statement, content, decl) => {
|
|
17171
|
+
if (!isReferencedOutside(sourceFile, statement, decl.name)) return null;
|
|
17172
|
+
const range = exportKeywordRange(sourceFile, statement, content);
|
|
17173
|
+
if (!range) return {
|
|
17174
|
+
type: "skip",
|
|
17175
|
+
reason: "still referenced in its own file",
|
|
17176
|
+
declaration: decl
|
|
17177
|
+
};
|
|
17178
|
+
return {
|
|
17179
|
+
type: "match",
|
|
17180
|
+
removal: {
|
|
17181
|
+
...range,
|
|
17182
|
+
declaration: decl
|
|
17183
|
+
}
|
|
17184
|
+
};
|
|
17185
|
+
};
|
|
17186
|
+
const declaredNames = (statement) => {
|
|
17187
|
+
if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.map((d) => ts.isIdentifier(d.name) ? d.name.text : null).filter((name) => name !== null);
|
|
17188
|
+
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] : [];
|
|
17189
|
+
return [];
|
|
17190
|
+
};
|
|
17191
|
+
const isDeclaredMoreThanOnce = (sourceFile, name) => sourceFile.statements.filter((s) => declaredNames(s).includes(name)).length > 1;
|
|
16932
17192
|
const kindOfStatement = (node) => {
|
|
16933
17193
|
if (ts.isVariableStatement(node)) return "variable";
|
|
16934
17194
|
if (ts.isFunctionDeclaration(node)) return "function";
|
|
@@ -16940,6 +17200,11 @@ const kindOfStatement = (node) => {
|
|
|
16940
17200
|
};
|
|
16941
17201
|
const matchStatement = (sourceFile, statement, content, decl) => {
|
|
16942
17202
|
if (!kindOfStatement(statement)) return { type: "none" };
|
|
17203
|
+
if (declaredNames(statement).includes(decl.name) && isDeclaredMoreThanOnce(sourceFile, decl.name)) return {
|
|
17204
|
+
type: "skip",
|
|
17205
|
+
reason: "merged declaration",
|
|
17206
|
+
declaration: decl
|
|
17207
|
+
};
|
|
16943
17208
|
if (ts.isVariableStatement(statement)) {
|
|
16944
17209
|
const varDecls = statement.declarationList.declarations;
|
|
16945
17210
|
if (varDecls.length === 0) return { type: "none" };
|
|
@@ -16955,6 +17220,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16955
17220
|
reason: "multi-declaration variable statement",
|
|
16956
17221
|
declaration: decl
|
|
16957
17222
|
};
|
|
17223
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17224
|
+
if (referenced) return referenced;
|
|
16958
17225
|
if (initializerHasSideEffects(match.initializer)) return {
|
|
16959
17226
|
type: "skip",
|
|
16960
17227
|
reason: "initializer may have side effects",
|
|
@@ -16972,6 +17239,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16972
17239
|
if (!statement.name) return { type: "none" };
|
|
16973
17240
|
if (statement.name.text !== decl.name) return { type: "none" };
|
|
16974
17241
|
if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
|
|
17242
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17243
|
+
if (referenced) return referenced;
|
|
16975
17244
|
return {
|
|
16976
17245
|
type: "match",
|
|
16977
17246
|
removal: {
|
|
@@ -16983,6 +17252,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
|
|
|
16983
17252
|
if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
16984
17253
|
if (statement.name.text !== decl.name) return { type: "none" };
|
|
16985
17254
|
if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
|
|
17255
|
+
const referenced = removalFor(sourceFile, statement, content, decl);
|
|
17256
|
+
if (referenced) return referenced;
|
|
16986
17257
|
return {
|
|
16987
17258
|
type: "match",
|
|
16988
17259
|
removal: {
|
|
@@ -18198,6 +18469,835 @@ const recordFullScanActivity = (history, allowPilotInvitation, telemetryConfig)
|
|
|
18198
18469
|
appendHistory(history);
|
|
18199
18470
|
return allowPilotInvitation && !isHistoryDisabled() && !isTelemetryDisabled(telemetryConfig) && recordCompletedFullScan();
|
|
18200
18471
|
};
|
|
18472
|
+
const AISLOP_MD_BODY = `# Raigal - agent instructions
|
|
18473
|
+
|
|
18474
|
+
[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.
|
|
18475
|
+
|
|
18476
|
+
## On every edit
|
|
18477
|
+
|
|
18478
|
+
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.
|
|
18479
|
+
|
|
18480
|
+
## Severity ladder
|
|
18481
|
+
|
|
18482
|
+
- \`error\` - MUST fix this turn.
|
|
18483
|
+
- \`warning\` + \`fixable: true\` - MUST fix this turn.
|
|
18484
|
+
- \`warning\` + \`fixable: false\` - fix if trivially mechanical, otherwise surface in your reply.
|
|
18485
|
+
|
|
18486
|
+
## Rules
|
|
18487
|
+
|
|
18488
|
+
- \`.raigal/config.yaml\` (or \`.aislop/config.yaml\`) - thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
|
|
18489
|
+
- \`.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.
|
|
18490
|
+
- Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
|
|
18491
|
+
|
|
18492
|
+
## Principles
|
|
18493
|
+
|
|
18494
|
+
- Do not disable rules to pass the scan. Fix the underlying issue.
|
|
18495
|
+
- If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
|
|
18496
|
+
- The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
|
|
18497
|
+
`;
|
|
18498
|
+
//#endregion
|
|
18499
|
+
//#region src/hooks/io/atomic-write.ts
|
|
18500
|
+
const atomicWrite = (targetPath, content) => {
|
|
18501
|
+
const dir = path.dirname(targetPath);
|
|
18502
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18503
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
18504
|
+
const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
|
|
18505
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
18506
|
+
try {
|
|
18507
|
+
fs.renameSync(tmp, targetPath);
|
|
18508
|
+
} catch (err) {
|
|
18509
|
+
const code = err?.code;
|
|
18510
|
+
if (code === "EPERM" || code === "EBUSY") {
|
|
18511
|
+
fs.copyFileSync(tmp, targetPath);
|
|
18512
|
+
try {
|
|
18513
|
+
fs.unlinkSync(tmp);
|
|
18514
|
+
} catch {}
|
|
18515
|
+
} else throw err;
|
|
18516
|
+
}
|
|
18517
|
+
};
|
|
18518
|
+
const readIfExists = (targetPath) => {
|
|
18519
|
+
try {
|
|
18520
|
+
return fs.readFileSync(targetPath, "utf-8");
|
|
18521
|
+
} catch {
|
|
18522
|
+
return null;
|
|
18523
|
+
}
|
|
18524
|
+
};
|
|
18525
|
+
//#endregion
|
|
18526
|
+
//#region src/hooks/io/sentinel.ts
|
|
18527
|
+
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
18528
|
+
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
18529
|
+
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
18530
|
+
const renderFence = (body, hash) => [
|
|
18531
|
+
`<!-- aislop:begin v1 hash=${hash} -->`,
|
|
18532
|
+
body.trimEnd(),
|
|
18533
|
+
"<!-- aislop:end v1 -->"
|
|
18534
|
+
].join("\n");
|
|
18535
|
+
const upsertMarkdownFence = (existing, body, hash) => {
|
|
18536
|
+
const fenced = renderFence(body, hash);
|
|
18537
|
+
if (existing == null || existing.length === 0) return {
|
|
18538
|
+
nextContent: `${fenced}\n`,
|
|
18539
|
+
replaced: false
|
|
18540
|
+
};
|
|
18541
|
+
const begin = existing.match(BEGIN_RE);
|
|
18542
|
+
const end = existing.match(END_RE);
|
|
18543
|
+
if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
|
|
18544
|
+
nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
|
|
18545
|
+
replaced: true
|
|
18546
|
+
};
|
|
18547
|
+
return {
|
|
18548
|
+
nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
|
|
18549
|
+
replaced: false
|
|
18550
|
+
};
|
|
18551
|
+
};
|
|
18552
|
+
//#endregion
|
|
18553
|
+
//#region src/hooks/install/types.ts
|
|
18554
|
+
const emptyResult = () => ({
|
|
18555
|
+
wrote: [],
|
|
18556
|
+
skipped: [],
|
|
18557
|
+
planned: []
|
|
18558
|
+
});
|
|
18559
|
+
const applyContent = (result, opts, target, nextContent, summary) => {
|
|
18560
|
+
if (readIfExists(target) === nextContent) {
|
|
18561
|
+
result.skipped.push(target);
|
|
18562
|
+
return;
|
|
18563
|
+
}
|
|
18564
|
+
if (opts.dryRun) {
|
|
18565
|
+
result.planned.push({
|
|
18566
|
+
path: target,
|
|
18567
|
+
summary
|
|
18568
|
+
});
|
|
18569
|
+
return;
|
|
18570
|
+
}
|
|
18571
|
+
atomicWrite(target, nextContent);
|
|
18572
|
+
result.wrote.push(target);
|
|
18573
|
+
};
|
|
18574
|
+
const applyRemoval = (result, opts, target, nextContent) => {
|
|
18575
|
+
const existing = readIfExists(target);
|
|
18576
|
+
if (existing == null) {
|
|
18577
|
+
result.skipped.push(target);
|
|
18578
|
+
return;
|
|
18579
|
+
}
|
|
18580
|
+
if (existing === (nextContent ?? "")) {
|
|
18581
|
+
result.skipped.push(target);
|
|
18582
|
+
return;
|
|
18583
|
+
}
|
|
18584
|
+
if (opts.dryRun) {
|
|
18585
|
+
result.removed.push(target);
|
|
18586
|
+
return;
|
|
18587
|
+
}
|
|
18588
|
+
if (nextContent == null) try {
|
|
18589
|
+
fs.unlinkSync(target);
|
|
18590
|
+
} catch {}
|
|
18591
|
+
else atomicWrite(target, nextContent);
|
|
18592
|
+
result.removed.push(target);
|
|
18593
|
+
};
|
|
18594
|
+
//#endregion
|
|
18595
|
+
//#region src/hooks/install/rules-only.ts
|
|
18596
|
+
const installRulesOnly = (opts, paths, summary) => {
|
|
18597
|
+
const result = emptyResult();
|
|
18598
|
+
const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
18599
|
+
applyContent(result, opts, paths.rules, next, summary);
|
|
18600
|
+
if (paths.host && paths.marker) {
|
|
18601
|
+
const host = readIfExists(paths.host) ?? "";
|
|
18602
|
+
if (!host.includes(paths.marker)) {
|
|
18603
|
+
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
18604
|
+
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
18605
|
+
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
18606
|
+
} else result.skipped.push(paths.host);
|
|
18607
|
+
}
|
|
18608
|
+
return result;
|
|
18609
|
+
};
|
|
18610
|
+
const uninstallRulesOnly = (opts, paths) => {
|
|
18611
|
+
const result = {
|
|
18612
|
+
removed: [],
|
|
18613
|
+
skipped: []
|
|
18614
|
+
};
|
|
18615
|
+
if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
|
|
18616
|
+
else result.skipped.push(paths.rules);
|
|
18617
|
+
if (paths.host && paths.marker) {
|
|
18618
|
+
const host = readIfExists(paths.host);
|
|
18619
|
+
if (host?.includes(paths.marker)) {
|
|
18620
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
18621
|
+
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
18622
|
+
} else result.skipped.push(paths.host);
|
|
18623
|
+
}
|
|
18624
|
+
return result;
|
|
18625
|
+
};
|
|
18626
|
+
//#endregion
|
|
18627
|
+
//#region src/hooks/install/antigravity.ts
|
|
18628
|
+
const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
|
|
18629
|
+
const installAntigravity = (opts) => {
|
|
18630
|
+
if (opts.scope !== "project") return {
|
|
18631
|
+
wrote: [],
|
|
18632
|
+
skipped: [],
|
|
18633
|
+
planned: [{
|
|
18634
|
+
path: ".agents/rules/antigravity-aislop-rules.md",
|
|
18635
|
+
summary: "Antigravity is project-scope only; pass --project"
|
|
18636
|
+
}]
|
|
18637
|
+
};
|
|
18638
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
|
|
18639
|
+
};
|
|
18640
|
+
const uninstallAntigravity = (opts) => {
|
|
18641
|
+
if (opts.scope !== "project") return {
|
|
18642
|
+
removed: [],
|
|
18643
|
+
skipped: []
|
|
18644
|
+
};
|
|
18645
|
+
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
18646
|
+
};
|
|
18647
|
+
//#endregion
|
|
18648
|
+
//#region src/hooks/io/json-patch.ts
|
|
18649
|
+
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
18650
|
+
const isAislopManaged = (x) => typeof x === "object" && x !== null && "__aislop" in x && x["__aislop"] != null;
|
|
18651
|
+
const groupIsAislop = (group) => {
|
|
18652
|
+
if (typeof group !== "object" || group === null) return false;
|
|
18653
|
+
const hooks = group.hooks;
|
|
18654
|
+
if (!Array.isArray(hooks)) return false;
|
|
18655
|
+
return hooks.some((h) => isAislopManaged(h));
|
|
18656
|
+
};
|
|
18657
|
+
const upsertHookGroup = (config, event, group) => {
|
|
18658
|
+
const next = { ...config };
|
|
18659
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18660
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
|
|
18661
|
+
next.hooks = {
|
|
18662
|
+
...hooks,
|
|
18663
|
+
[event]: [...cleaned, group]
|
|
18664
|
+
};
|
|
18665
|
+
return next;
|
|
18666
|
+
};
|
|
18667
|
+
const upsertFlatHook = (config, event, entry) => {
|
|
18668
|
+
const next = { ...config };
|
|
18669
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18670
|
+
const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
|
|
18671
|
+
next.hooks = {
|
|
18672
|
+
...hooks,
|
|
18673
|
+
[event]: [...cleaned, entry]
|
|
18674
|
+
};
|
|
18675
|
+
return next;
|
|
18676
|
+
};
|
|
18677
|
+
const removeAislopEntries = (config, event) => {
|
|
18678
|
+
const next = { ...config };
|
|
18679
|
+
const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
|
|
18680
|
+
const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
18681
|
+
const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
|
|
18682
|
+
const removed = existing.length - cleaned.length;
|
|
18683
|
+
const nextHooks = { ...hooks };
|
|
18684
|
+
if (cleaned.length === 0) delete nextHooks[event];
|
|
18685
|
+
else nextHooks[event] = cleaned;
|
|
18686
|
+
if (Object.keys(nextHooks).length === 0) delete next.hooks;
|
|
18687
|
+
else next.hooks = nextHooks;
|
|
18688
|
+
return {
|
|
18689
|
+
next,
|
|
18690
|
+
removed
|
|
18691
|
+
};
|
|
18692
|
+
};
|
|
18693
|
+
//#endregion
|
|
18694
|
+
//#region src/hooks/install/claude.ts
|
|
18695
|
+
const resolveClaudePaths = (opts) => {
|
|
18696
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
18697
|
+
return {
|
|
18698
|
+
settings: path.join(root, "settings.json"),
|
|
18699
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
18700
|
+
claudeMd: path.join(root, "CLAUDE.md")
|
|
18701
|
+
};
|
|
18702
|
+
};
|
|
18703
|
+
const buildHookGroup$1 = () => {
|
|
18704
|
+
const hashBody = JSON.stringify({
|
|
18705
|
+
command: "aislop hook claude",
|
|
18706
|
+
matcher: "Edit|Write|MultiEdit"
|
|
18707
|
+
});
|
|
18708
|
+
return {
|
|
18709
|
+
matcher: "Edit|Write|MultiEdit",
|
|
18710
|
+
hooks: [{
|
|
18711
|
+
type: "command",
|
|
18712
|
+
command: "aislop hook claude",
|
|
18713
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18714
|
+
v: 1,
|
|
18715
|
+
managed: true,
|
|
18716
|
+
hash: sentinelHash(hashBody)
|
|
18717
|
+
}
|
|
18718
|
+
}]
|
|
18719
|
+
};
|
|
18720
|
+
};
|
|
18721
|
+
const buildStopHookGroup = () => {
|
|
18722
|
+
const hashBody = JSON.stringify({ command: "aislop hook claude --stop" });
|
|
18723
|
+
return {
|
|
18724
|
+
matcher: "",
|
|
18725
|
+
hooks: [{
|
|
18726
|
+
type: "command",
|
|
18727
|
+
command: "aislop hook claude --stop",
|
|
18728
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18729
|
+
v: 1,
|
|
18730
|
+
managed: true,
|
|
18731
|
+
hash: sentinelHash(hashBody)
|
|
18732
|
+
}
|
|
18733
|
+
}]
|
|
18734
|
+
};
|
|
18735
|
+
};
|
|
18736
|
+
const FILE_CHANGED_MATCHER = ".raigal/config.yml|.raigal/rules.yml|.aislop/config.yml|.aislop/rules.yml|package.json";
|
|
18737
|
+
const buildFileChangedHookGroup = () => {
|
|
18738
|
+
const hashBody = JSON.stringify({
|
|
18739
|
+
command: "aislop hook claude --on-file-changed",
|
|
18740
|
+
matcher: FILE_CHANGED_MATCHER
|
|
18741
|
+
});
|
|
18742
|
+
return {
|
|
18743
|
+
matcher: FILE_CHANGED_MATCHER,
|
|
18744
|
+
hooks: [{
|
|
18745
|
+
type: "command",
|
|
18746
|
+
command: "aislop hook claude --on-file-changed",
|
|
18747
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18748
|
+
v: 1,
|
|
18749
|
+
managed: true,
|
|
18750
|
+
hash: sentinelHash(hashBody)
|
|
18751
|
+
}
|
|
18752
|
+
}]
|
|
18753
|
+
};
|
|
18754
|
+
};
|
|
18755
|
+
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
18756
|
+
let obj = {};
|
|
18757
|
+
if (existingRaw) try {
|
|
18758
|
+
obj = JSON.parse(existingRaw);
|
|
18759
|
+
} catch {
|
|
18760
|
+
obj = {};
|
|
18761
|
+
}
|
|
18762
|
+
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
18763
|
+
next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
|
|
18764
|
+
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
18765
|
+
else next = removeAislopEntries(next, "Stop").next;
|
|
18766
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
18767
|
+
};
|
|
18768
|
+
const installClaude = (opts) => {
|
|
18769
|
+
const paths = resolveClaudePaths(opts);
|
|
18770
|
+
const result = emptyResult();
|
|
18771
|
+
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
18772
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
18773
|
+
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
18774
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
|
|
18775
|
+
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
18776
|
+
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
18777
|
+
const marker = "@AISLOP.md";
|
|
18778
|
+
if (!existingClaudeMd.includes(marker)) {
|
|
18779
|
+
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
18780
|
+
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
18781
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
18782
|
+
} else result.skipped.push(paths.claudeMd);
|
|
18783
|
+
return result;
|
|
18784
|
+
};
|
|
18785
|
+
const uninstallClaude = (opts) => {
|
|
18786
|
+
const paths = resolveClaudePaths({
|
|
18787
|
+
...opts,
|
|
18788
|
+
qualityGate: false
|
|
18789
|
+
});
|
|
18790
|
+
const result = {
|
|
18791
|
+
removed: [],
|
|
18792
|
+
skipped: []
|
|
18793
|
+
};
|
|
18794
|
+
const settingsRaw = readIfExists(paths.settings);
|
|
18795
|
+
if (settingsRaw) {
|
|
18796
|
+
let obj = {};
|
|
18797
|
+
try {
|
|
18798
|
+
obj = JSON.parse(settingsRaw);
|
|
18799
|
+
} catch {
|
|
18800
|
+
obj = {};
|
|
18801
|
+
}
|
|
18802
|
+
const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
|
|
18803
|
+
const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
|
|
18804
|
+
const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
|
|
18805
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
18806
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
18807
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
18808
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
18809
|
+
} else result.skipped.push(paths.settings);
|
|
18810
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
18811
|
+
else result.skipped.push(paths.aislopMd);
|
|
18812
|
+
const claudeMd = readIfExists(paths.claudeMd);
|
|
18813
|
+
if (claudeMd?.includes("@AISLOP.md")) {
|
|
18814
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
18815
|
+
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
18816
|
+
} else result.skipped.push(paths.claudeMd);
|
|
18817
|
+
return result;
|
|
18818
|
+
};
|
|
18819
|
+
//#endregion
|
|
18820
|
+
//#region src/hooks/install/cline.ts
|
|
18821
|
+
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
18822
|
+
const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
|
|
18823
|
+
const installCline = (opts) => {
|
|
18824
|
+
if (opts.scope !== "project") return {
|
|
18825
|
+
wrote: [],
|
|
18826
|
+
skipped: [],
|
|
18827
|
+
planned: [{
|
|
18828
|
+
path: ".clinerules",
|
|
18829
|
+
summary: "Cline is project-scope only; pass --project"
|
|
18830
|
+
}]
|
|
18831
|
+
};
|
|
18832
|
+
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
18833
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
|
|
18834
|
+
return {
|
|
18835
|
+
wrote: [...cline.wrote, ...roo.wrote],
|
|
18836
|
+
skipped: [...cline.skipped, ...roo.skipped],
|
|
18837
|
+
planned: [...cline.planned, ...roo.planned]
|
|
18838
|
+
};
|
|
18839
|
+
};
|
|
18840
|
+
const uninstallCline = (opts) => {
|
|
18841
|
+
if (opts.scope !== "project") return {
|
|
18842
|
+
removed: [],
|
|
18843
|
+
skipped: []
|
|
18844
|
+
};
|
|
18845
|
+
const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
|
|
18846
|
+
const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
|
|
18847
|
+
return {
|
|
18848
|
+
removed: [...a.removed, ...b.removed],
|
|
18849
|
+
skipped: [...a.skipped, ...b.skipped]
|
|
18850
|
+
};
|
|
18851
|
+
};
|
|
18852
|
+
//#endregion
|
|
18853
|
+
//#region src/hooks/install/codex.ts
|
|
18854
|
+
const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
|
|
18855
|
+
const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
|
|
18856
|
+
const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
|
|
18857
|
+
//#endregion
|
|
18858
|
+
//#region src/hooks/install/copilot.ts
|
|
18859
|
+
const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
|
|
18860
|
+
const installCopilot = (opts) => {
|
|
18861
|
+
if (opts.scope !== "project") return {
|
|
18862
|
+
wrote: [],
|
|
18863
|
+
skipped: [],
|
|
18864
|
+
planned: [{
|
|
18865
|
+
path: ".github/copilot-instructions.md",
|
|
18866
|
+
summary: "Copilot is project-scope only; pass --project"
|
|
18867
|
+
}]
|
|
18868
|
+
};
|
|
18869
|
+
return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
|
|
18870
|
+
};
|
|
18871
|
+
const uninstallCopilot = (opts) => {
|
|
18872
|
+
if (opts.scope !== "project") return {
|
|
18873
|
+
removed: [],
|
|
18874
|
+
skipped: []
|
|
18875
|
+
};
|
|
18876
|
+
return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
|
|
18877
|
+
};
|
|
18878
|
+
//#endregion
|
|
18879
|
+
//#region src/hooks/install/cursor.ts
|
|
18880
|
+
const resolveCursorPaths = (opts) => {
|
|
18881
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
18882
|
+
return {
|
|
18883
|
+
hooks: path.join(root, "hooks.json"),
|
|
18884
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
18885
|
+
};
|
|
18886
|
+
};
|
|
18887
|
+
const buildHookEntry = () => {
|
|
18888
|
+
const hashBody = JSON.stringify({
|
|
18889
|
+
command: "aislop hook cursor",
|
|
18890
|
+
timeout: 5e3
|
|
18891
|
+
});
|
|
18892
|
+
return {
|
|
18893
|
+
command: "aislop hook cursor",
|
|
18894
|
+
type: "command",
|
|
18895
|
+
timeout: 5e3,
|
|
18896
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18897
|
+
v: 1,
|
|
18898
|
+
managed: true,
|
|
18899
|
+
hash: sentinelHash(hashBody)
|
|
18900
|
+
}
|
|
18901
|
+
};
|
|
18902
|
+
};
|
|
18903
|
+
const renderHooksJson = (existingRaw) => {
|
|
18904
|
+
let obj = { version: 1 };
|
|
18905
|
+
if (existingRaw) try {
|
|
18906
|
+
obj = JSON.parse(existingRaw);
|
|
18907
|
+
} catch {
|
|
18908
|
+
obj = { version: 1 };
|
|
18909
|
+
}
|
|
18910
|
+
if (typeof obj.version !== "number") obj.version = 1;
|
|
18911
|
+
const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
|
|
18912
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
18913
|
+
};
|
|
18914
|
+
const installCursor = (opts) => {
|
|
18915
|
+
const paths = resolveCursorPaths(opts);
|
|
18916
|
+
const result = emptyResult();
|
|
18917
|
+
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
18918
|
+
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
18919
|
+
if (opts.scope === "project") {
|
|
18920
|
+
const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
18921
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
|
|
18922
|
+
}
|
|
18923
|
+
return result;
|
|
18924
|
+
};
|
|
18925
|
+
const uninstallCursor = (opts) => {
|
|
18926
|
+
const paths = resolveCursorPaths(opts);
|
|
18927
|
+
const result = {
|
|
18928
|
+
removed: [],
|
|
18929
|
+
skipped: []
|
|
18930
|
+
};
|
|
18931
|
+
const raw = readIfExists(paths.hooks);
|
|
18932
|
+
if (raw) {
|
|
18933
|
+
let obj = {};
|
|
18934
|
+
try {
|
|
18935
|
+
obj = JSON.parse(raw);
|
|
18936
|
+
} catch {
|
|
18937
|
+
obj = {};
|
|
18938
|
+
}
|
|
18939
|
+
const stripped = removeAislopEntries(obj, "afterFileEdit").next;
|
|
18940
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
18941
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
|
|
18942
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
18943
|
+
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
18944
|
+
} else result.skipped.push(paths.hooks);
|
|
18945
|
+
if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
|
|
18946
|
+
return result;
|
|
18947
|
+
};
|
|
18948
|
+
//#endregion
|
|
18949
|
+
//#region src/hooks/install/gemini.ts
|
|
18950
|
+
const resolveGeminiPaths = (opts) => {
|
|
18951
|
+
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
18952
|
+
return {
|
|
18953
|
+
settings: path.join(root, "settings.json"),
|
|
18954
|
+
aislopMd: path.join(root, "AISLOP.md"),
|
|
18955
|
+
geminiMd: path.join(root, "GEMINI.md")
|
|
18956
|
+
};
|
|
18957
|
+
};
|
|
18958
|
+
const buildHookGroup = () => {
|
|
18959
|
+
const hashBody = JSON.stringify({
|
|
18960
|
+
command: "aislop hook gemini",
|
|
18961
|
+
matcher: "write_file|replace"
|
|
18962
|
+
});
|
|
18963
|
+
return {
|
|
18964
|
+
matcher: "write_file|replace",
|
|
18965
|
+
hooks: [{
|
|
18966
|
+
name: "aislop",
|
|
18967
|
+
type: "command",
|
|
18968
|
+
command: "aislop hook gemini",
|
|
18969
|
+
timeout: 5e3,
|
|
18970
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
18971
|
+
v: 1,
|
|
18972
|
+
managed: true,
|
|
18973
|
+
hash: sentinelHash(hashBody)
|
|
18974
|
+
}
|
|
18975
|
+
}]
|
|
18976
|
+
};
|
|
18977
|
+
};
|
|
18978
|
+
const renderSettings = (existingRaw) => {
|
|
18979
|
+
let obj = {};
|
|
18980
|
+
if (existingRaw) try {
|
|
18981
|
+
obj = JSON.parse(existingRaw);
|
|
18982
|
+
} catch {
|
|
18983
|
+
obj = {};
|
|
18984
|
+
}
|
|
18985
|
+
const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
|
|
18986
|
+
return `${JSON.stringify(next, null, 2)}\n`;
|
|
18987
|
+
};
|
|
18988
|
+
const installGemini = (opts) => {
|
|
18989
|
+
const paths = resolveGeminiPaths(opts);
|
|
18990
|
+
const result = emptyResult();
|
|
18991
|
+
const next = renderSettings(readIfExists(paths.settings));
|
|
18992
|
+
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
18993
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
|
|
18994
|
+
applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
|
|
18995
|
+
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
18996
|
+
const marker = "@AISLOP.md";
|
|
18997
|
+
if (!existingGeminiMd.includes(marker)) {
|
|
18998
|
+
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
18999
|
+
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
19000
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
|
|
19001
|
+
} else result.skipped.push(paths.geminiMd);
|
|
19002
|
+
return result;
|
|
19003
|
+
};
|
|
19004
|
+
const uninstallGemini = (opts) => {
|
|
19005
|
+
const paths = resolveGeminiPaths(opts);
|
|
19006
|
+
const result = {
|
|
19007
|
+
removed: [],
|
|
19008
|
+
skipped: []
|
|
19009
|
+
};
|
|
19010
|
+
const raw = readIfExists(paths.settings);
|
|
19011
|
+
if (raw) {
|
|
19012
|
+
let obj = {};
|
|
19013
|
+
try {
|
|
19014
|
+
obj = JSON.parse(raw);
|
|
19015
|
+
} catch {
|
|
19016
|
+
obj = {};
|
|
19017
|
+
}
|
|
19018
|
+
const stripped = removeAislopEntries(obj, "AfterTool").next;
|
|
19019
|
+
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
19020
|
+
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
19021
|
+
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
19022
|
+
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19023
|
+
} else result.skipped.push(paths.settings);
|
|
19024
|
+
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
19025
|
+
else result.skipped.push(paths.aislopMd);
|
|
19026
|
+
const geminiMd = readIfExists(paths.geminiMd);
|
|
19027
|
+
if (geminiMd?.includes("@AISLOP.md")) {
|
|
19028
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19029
|
+
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
19030
|
+
} else result.skipped.push(paths.geminiMd);
|
|
19031
|
+
return result;
|
|
19032
|
+
};
|
|
19033
|
+
//#endregion
|
|
19034
|
+
//#region src/hooks/install/kilocode.ts
|
|
19035
|
+
const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
|
|
19036
|
+
const installKilocode = (opts) => {
|
|
19037
|
+
if (opts.scope !== "project") return {
|
|
19038
|
+
wrote: [],
|
|
19039
|
+
skipped: [],
|
|
19040
|
+
planned: [{
|
|
19041
|
+
path: ".kilocode/rules/aislop-rules.md",
|
|
19042
|
+
summary: "Kilo Code is project-scope only; pass --project"
|
|
19043
|
+
}]
|
|
19044
|
+
};
|
|
19045
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
|
|
19046
|
+
};
|
|
19047
|
+
const uninstallKilocode = (opts) => {
|
|
19048
|
+
if (opts.scope !== "project") return {
|
|
19049
|
+
removed: [],
|
|
19050
|
+
skipped: []
|
|
19051
|
+
};
|
|
19052
|
+
return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
|
|
19053
|
+
};
|
|
19054
|
+
//#endregion
|
|
19055
|
+
//#region src/hooks/install/pi.ts
|
|
19056
|
+
const resolvePiPaths = (opts) => {
|
|
19057
|
+
return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
|
|
19058
|
+
};
|
|
19059
|
+
const PI_EXTENSION_SOURCE = `// aislop — auto-generated pi extension. Do not edit by hand.
|
|
19060
|
+
// Reinstall with: aislop hook install --pi
|
|
19061
|
+
import { spawn } from "node:child_process";
|
|
19062
|
+
|
|
19063
|
+
// Runs the CLI async so a slow scan never blocks pi's event loop, and forwards
|
|
19064
|
+
// ctx.signal so Esc actually kills the child instead of it running to completion.
|
|
19065
|
+
const runHook = (bin, args, input, signal) =>
|
|
19066
|
+
new Promise((resolve) => {
|
|
19067
|
+
const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
19068
|
+
const chunks = [];
|
|
19069
|
+
let settled = false;
|
|
19070
|
+
let timer;
|
|
19071
|
+
const onAbort = () => {
|
|
19072
|
+
child.kill();
|
|
19073
|
+
finish(null);
|
|
19074
|
+
};
|
|
19075
|
+
const cleanup = () => {
|
|
19076
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
19077
|
+
signal?.removeEventListener("abort", onAbort);
|
|
19078
|
+
};
|
|
19079
|
+
const finish = (value) => {
|
|
19080
|
+
if (settled) return;
|
|
19081
|
+
settled = true;
|
|
19082
|
+
cleanup();
|
|
19083
|
+
resolve(value);
|
|
19084
|
+
};
|
|
19085
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
19086
|
+
timer = setTimeout(() => {
|
|
19087
|
+
child.kill();
|
|
19088
|
+
finish(null);
|
|
19089
|
+
}, 15000);
|
|
19090
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
19091
|
+
child.on("error", () => finish(null));
|
|
19092
|
+
child.on("close", (code) => {
|
|
19093
|
+
finish(code === 0 && chunks.length > 0 ? Buffer.concat(chunks).toString("utf-8") : null);
|
|
19094
|
+
});
|
|
19095
|
+
child.stdin.end(input);
|
|
19096
|
+
});
|
|
19097
|
+
|
|
19098
|
+
export default function (pi) {
|
|
19099
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
19100
|
+
if (event.toolName !== "edit" && event.toolName !== "write") return;
|
|
19101
|
+
if (event.isError) return;
|
|
19102
|
+
const filePath = event.input && event.input.path;
|
|
19103
|
+
if (typeof filePath !== "string" || filePath.length === 0) return;
|
|
19104
|
+
|
|
19105
|
+
const bin = process.env.AISLOP_BIN || "aislop";
|
|
19106
|
+
const payload = JSON.stringify({
|
|
19107
|
+
cwd: ctx.cwd,
|
|
19108
|
+
file_path: filePath,
|
|
19109
|
+
tool_name: event.toolName,
|
|
19110
|
+
});
|
|
19111
|
+
|
|
19112
|
+
let out;
|
|
19113
|
+
try {
|
|
19114
|
+
const stdout = await runHook(bin, ["hook", "pi"], payload, ctx.signal);
|
|
19115
|
+
if (!stdout) return;
|
|
19116
|
+
out = JSON.parse(stdout);
|
|
19117
|
+
} catch {
|
|
19118
|
+
return;
|
|
19119
|
+
}
|
|
19120
|
+
if (!out || !out.message) return;
|
|
19121
|
+
|
|
19122
|
+
return {
|
|
19123
|
+
content: [...event.content, { type: "text", text: out.message }],
|
|
19124
|
+
isError: event.isError,
|
|
19125
|
+
};
|
|
19126
|
+
});
|
|
19127
|
+
}
|
|
19128
|
+
`;
|
|
19129
|
+
const installPi = (opts) => {
|
|
19130
|
+
const paths = resolvePiPaths(opts);
|
|
19131
|
+
const result = emptyResult();
|
|
19132
|
+
applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi aislop extension");
|
|
19133
|
+
return result;
|
|
19134
|
+
};
|
|
19135
|
+
const uninstallPi = (opts) => {
|
|
19136
|
+
const paths = resolvePiPaths(opts);
|
|
19137
|
+
const result = {
|
|
19138
|
+
removed: [],
|
|
19139
|
+
skipped: []
|
|
19140
|
+
};
|
|
19141
|
+
if (readIfExists(paths.extension) != null) applyRemoval(result, opts, paths.extension, null);
|
|
19142
|
+
else result.skipped.push(paths.extension);
|
|
19143
|
+
return result;
|
|
19144
|
+
};
|
|
19145
|
+
//#endregion
|
|
19146
|
+
//#region src/hooks/install/windsurf.ts
|
|
19147
|
+
const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
|
|
19148
|
+
const installWindsurf = (opts) => {
|
|
19149
|
+
if (opts.scope !== "project") return {
|
|
19150
|
+
wrote: [],
|
|
19151
|
+
skipped: [],
|
|
19152
|
+
planned: [{
|
|
19153
|
+
path: ".windsurfrules",
|
|
19154
|
+
summary: "Windsurf is project-scope only; pass --project"
|
|
19155
|
+
}]
|
|
19156
|
+
};
|
|
19157
|
+
return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
|
|
19158
|
+
};
|
|
19159
|
+
const uninstallWindsurf = (opts) => {
|
|
19160
|
+
if (opts.scope !== "project") return {
|
|
19161
|
+
removed: [],
|
|
19162
|
+
skipped: []
|
|
19163
|
+
};
|
|
19164
|
+
return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
|
|
19165
|
+
};
|
|
19166
|
+
//#endregion
|
|
19167
|
+
//#region src/hooks/install/registry.ts
|
|
19168
|
+
const ALL_AGENTS = [
|
|
19169
|
+
"claude",
|
|
19170
|
+
"cursor",
|
|
19171
|
+
"gemini",
|
|
19172
|
+
"pi",
|
|
19173
|
+
"codex",
|
|
19174
|
+
"windsurf",
|
|
19175
|
+
"cline",
|
|
19176
|
+
"kilocode",
|
|
19177
|
+
"antigravity",
|
|
19178
|
+
"copilot"
|
|
19179
|
+
];
|
|
19180
|
+
const AGENTS_PROJECT_ONLY = [
|
|
19181
|
+
"windsurf",
|
|
19182
|
+
"cline",
|
|
19183
|
+
"kilocode",
|
|
19184
|
+
"antigravity",
|
|
19185
|
+
"copilot"
|
|
19186
|
+
];
|
|
19187
|
+
const AGENTS_SUPPORTING_BOTH_SCOPES = [
|
|
19188
|
+
"claude",
|
|
19189
|
+
"cursor",
|
|
19190
|
+
"gemini",
|
|
19191
|
+
"pi",
|
|
19192
|
+
"codex"
|
|
19193
|
+
];
|
|
19194
|
+
const paths = {
|
|
19195
|
+
claude: (opts) => {
|
|
19196
|
+
const p = resolveClaudePaths(opts);
|
|
19197
|
+
return [
|
|
19198
|
+
p.settings,
|
|
19199
|
+
p.aislopMd,
|
|
19200
|
+
p.claudeMd
|
|
19201
|
+
];
|
|
19202
|
+
},
|
|
19203
|
+
cursor: (opts) => {
|
|
19204
|
+
const p = resolveCursorPaths(opts);
|
|
19205
|
+
return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
|
|
19206
|
+
},
|
|
19207
|
+
gemini: (opts) => {
|
|
19208
|
+
const p = resolveGeminiPaths(opts);
|
|
19209
|
+
return [
|
|
19210
|
+
p.settings,
|
|
19211
|
+
p.aislopMd,
|
|
19212
|
+
p.geminiMd
|
|
19213
|
+
];
|
|
19214
|
+
},
|
|
19215
|
+
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
19216
|
+
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
19217
|
+
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
19218
|
+
cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
|
|
19219
|
+
kilocode: (opts) => [resolveKilocodePaths(opts).rules],
|
|
19220
|
+
antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
|
|
19221
|
+
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
19222
|
+
};
|
|
19223
|
+
const REGISTRY = {
|
|
19224
|
+
claude: {
|
|
19225
|
+
install: installClaude,
|
|
19226
|
+
uninstall: uninstallClaude,
|
|
19227
|
+
paths: paths.claude
|
|
19228
|
+
},
|
|
19229
|
+
cursor: {
|
|
19230
|
+
install: installCursor,
|
|
19231
|
+
uninstall: uninstallCursor,
|
|
19232
|
+
paths: paths.cursor
|
|
19233
|
+
},
|
|
19234
|
+
gemini: {
|
|
19235
|
+
install: installGemini,
|
|
19236
|
+
uninstall: uninstallGemini,
|
|
19237
|
+
paths: paths.gemini
|
|
19238
|
+
},
|
|
19239
|
+
pi: {
|
|
19240
|
+
install: installPi,
|
|
19241
|
+
uninstall: uninstallPi,
|
|
19242
|
+
paths: paths.pi
|
|
19243
|
+
},
|
|
19244
|
+
codex: {
|
|
19245
|
+
install: installCodex,
|
|
19246
|
+
uninstall: uninstallCodex,
|
|
19247
|
+
paths: paths.codex
|
|
19248
|
+
},
|
|
19249
|
+
windsurf: {
|
|
19250
|
+
install: installWindsurf,
|
|
19251
|
+
uninstall: uninstallWindsurf,
|
|
19252
|
+
paths: paths.windsurf
|
|
19253
|
+
},
|
|
19254
|
+
cline: {
|
|
19255
|
+
install: installCline,
|
|
19256
|
+
uninstall: uninstallCline,
|
|
19257
|
+
paths: paths.cline
|
|
19258
|
+
},
|
|
19259
|
+
kilocode: {
|
|
19260
|
+
install: installKilocode,
|
|
19261
|
+
uninstall: uninstallKilocode,
|
|
19262
|
+
paths: paths.kilocode
|
|
19263
|
+
},
|
|
19264
|
+
antigravity: {
|
|
19265
|
+
install: installAntigravity,
|
|
19266
|
+
uninstall: uninstallAntigravity,
|
|
19267
|
+
paths: paths.antigravity
|
|
19268
|
+
},
|
|
19269
|
+
copilot: {
|
|
19270
|
+
install: installCopilot,
|
|
19271
|
+
uninstall: uninstallCopilot,
|
|
19272
|
+
paths: paths.copilot
|
|
19273
|
+
}
|
|
19274
|
+
};
|
|
19275
|
+
const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
|
|
19276
|
+
const HOOK_MARKERS = [
|
|
19277
|
+
"__raigal",
|
|
19278
|
+
"raigal:begin",
|
|
19279
|
+
"@RAIGAL.md",
|
|
19280
|
+
"__aislop",
|
|
19281
|
+
"aislop:begin",
|
|
19282
|
+
"@AISLOP.md"
|
|
19283
|
+
];
|
|
19284
|
+
const hasHook = (target) => {
|
|
19285
|
+
try {
|
|
19286
|
+
if (!fs.statSync(target).isFile()) return false;
|
|
19287
|
+
const base = path.basename(target).toLowerCase();
|
|
19288
|
+
if (base.startsWith("raigal") || base.startsWith("aislop")) return true;
|
|
19289
|
+
const content = fs.readFileSync(target, "utf8");
|
|
19290
|
+
return HOOK_MARKERS.some((marker) => content.includes(marker));
|
|
19291
|
+
} catch {
|
|
19292
|
+
return false;
|
|
19293
|
+
}
|
|
19294
|
+
};
|
|
19295
|
+
const detectionScopes = (agent) => AGENTS_SUPPORTING_BOTH_SCOPES.includes(agent) ? ["global", "project"] : [defaultScopeFor(agent)];
|
|
19296
|
+
const detectRaigalHooks = (opts) => ALL_AGENTS.filter((agent) => detectionScopes(agent).some((scope) => REGISTRY[agent].paths({
|
|
19297
|
+
home: opts.home,
|
|
19298
|
+
cwd: opts.cwd,
|
|
19299
|
+
scope
|
|
19300
|
+
}).some(hasHook)));
|
|
18201
19301
|
//#endregion
|
|
18202
19302
|
//#region src/output/terminal.ts
|
|
18203
19303
|
const groupBy = (items, key) => {
|
|
@@ -18365,27 +19465,16 @@ const printEngineStatus = (result) => {
|
|
|
18365
19465
|
}
|
|
18366
19466
|
};
|
|
18367
19467
|
//#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;
|
|
19468
|
+
//#region src/commands/scan-hook-nudge.ts
|
|
19469
|
+
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
19470
|
+
const buildHookNudge = (ctx) => {
|
|
19471
|
+
if (!shouldShowHookNudge(ctx)) return null;
|
|
19472
|
+
return [
|
|
19473
|
+
"",
|
|
19474
|
+
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
19475
|
+
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
19476
|
+
""
|
|
19477
|
+
].join("\n");
|
|
18389
19478
|
};
|
|
18390
19479
|
//#endregion
|
|
18391
19480
|
//#region src/commands/scan-coverage.ts
|
|
@@ -18960,7 +20049,7 @@ const buildScanNextSteps = (input) => {
|
|
|
18960
20049
|
emphasis: "primary",
|
|
18961
20050
|
label: "Provider",
|
|
18962
20051
|
command: `${input.invocation} agent --provider codex`,
|
|
18963
|
-
detail: "or use --provider claude/opencode"
|
|
20052
|
+
detail: "or use --provider claude/opencode/pi"
|
|
18964
20053
|
}, {
|
|
18965
20054
|
emphasis: "muted",
|
|
18966
20055
|
label: "Preview",
|
|
@@ -19119,6 +20208,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19119
20208
|
const configDir = findConfigDir(resolvedDir);
|
|
19120
20209
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
19121
20210
|
const engineConfig = {
|
|
20211
|
+
overrides: config.overrides,
|
|
20212
|
+
rules: config.rules,
|
|
19122
20213
|
quality: config.quality,
|
|
19123
20214
|
security: config.security,
|
|
19124
20215
|
lint: config.lint,
|
|
@@ -19140,7 +20231,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19140
20231
|
config: engineConfig
|
|
19141
20232
|
}, config.engines, machineOutput)).map((result) => ({
|
|
19142
20233
|
...result,
|
|
19143
|
-
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
20234
|
+
diagnostics: config.overrides.length ? result.diagnostics : applyRuleSeverities(result.diagnostics, config.rules)
|
|
19144
20235
|
})), resolvedDir);
|
|
19145
20236
|
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via raigal-ignore directives`);
|
|
19146
20237
|
const changeMap = options.changes && !options.staged ? getChangedLineMap(resolvedDir, options.base) : null;
|
|
@@ -19178,6 +20269,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19178
20269
|
engineIssues[r.engine] = r.diagnostics.length;
|
|
19179
20270
|
engineTimings[r.engine] = Math.round(r.elapsed);
|
|
19180
20271
|
}
|
|
20272
|
+
const enginesFailed = results.filter((r) => r.failed).map((r) => r.engine);
|
|
19181
20273
|
const completion = {
|
|
19182
20274
|
exitCode,
|
|
19183
20275
|
score: scoreable ? scoreResult.score : null,
|
|
@@ -19187,7 +20279,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19187
20279
|
warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
|
|
19188
20280
|
fixableCount: allDiagnostics.filter((d) => d.fixable).length,
|
|
19189
20281
|
engineIssues,
|
|
19190
|
-
engineTimings
|
|
20282
|
+
engineTimings,
|
|
20283
|
+
...enginesFailed.length > 0 ? { properties: { engines_failed: enginesFailed } } : {}
|
|
19191
20284
|
};
|
|
19192
20285
|
recordScanToCloud({
|
|
19193
20286
|
results,
|
|
@@ -19200,12 +20293,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19200
20293
|
scoreFileCount
|
|
19201
20294
|
});
|
|
19202
20295
|
if (options.sarif) {
|
|
19203
|
-
const { buildSarifLog } = await import("./sarif-
|
|
20296
|
+
const { buildSarifLog } = await import("./sarif-Ba5D8TPH.js");
|
|
19204
20297
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
19205
20298
|
return completion;
|
|
19206
20299
|
}
|
|
19207
20300
|
if (options.json) {
|
|
19208
|
-
const { buildJsonOutput } = await import("./json-
|
|
20301
|
+
const { buildJsonOutput } = await import("./json-DBoMe4xB.js");
|
|
19209
20302
|
const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
|
|
19210
20303
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
19211
20304
|
return completion;
|
|
@@ -19238,6 +20331,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
19238
20331
|
printBrand: options.printBrand,
|
|
19239
20332
|
showPilotInvitation
|
|
19240
20333
|
}));
|
|
20334
|
+
if (options.command !== "ci" && options.printBrand !== false) {
|
|
20335
|
+
const nudge = buildHookNudge({
|
|
20336
|
+
installedAgentCount: detectRaigalHooks({
|
|
20337
|
+
home: os.homedir(),
|
|
20338
|
+
cwd: resolvedDir
|
|
20339
|
+
}).length,
|
|
20340
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
20341
|
+
isCi: isCiEnv(),
|
|
20342
|
+
invocation: detectInvocation()
|
|
20343
|
+
});
|
|
20344
|
+
if (nudge) process.stdout.write(nudge);
|
|
20345
|
+
}
|
|
19241
20346
|
return completion;
|
|
19242
20347
|
};
|
|
19243
20348
|
//#endregion
|
|
@@ -19402,6 +20507,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19402
20507
|
const configDir = findConfigDir(resolvedDir);
|
|
19403
20508
|
const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
|
|
19404
20509
|
const engineConfig = {
|
|
20510
|
+
overrides: config.overrides,
|
|
20511
|
+
rules: config.rules,
|
|
19405
20512
|
quality: config.quality,
|
|
19406
20513
|
security: config.security,
|
|
19407
20514
|
lint: config.lint,
|
|
@@ -19427,7 +20534,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19427
20534
|
status: "done",
|
|
19428
20535
|
label: "Verification complete"
|
|
19429
20536
|
});
|
|
19430
|
-
const scanResults = appendPostFixLintResult(verificationResults, collectPostFixLintDiagnostics(steps));
|
|
20537
|
+
const scanResults = appendPostFixLintResult(verificationResults, applyFileOverrides(collectPostFixLintDiagnostics(steps), context));
|
|
19431
20538
|
const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
|
|
19432
20539
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, scope ? scope.scoreFileCount : projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
19433
20540
|
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
@@ -19489,6 +20596,13 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
19489
20596
|
};
|
|
19490
20597
|
};
|
|
19491
20598
|
//#endregion
|
|
20599
|
+
//#region src/ui/prompts.ts
|
|
20600
|
+
const runCancellable = async (fn) => {
|
|
20601
|
+
const value = await fn();
|
|
20602
|
+
if (isCancel(value)) return void 0;
|
|
20603
|
+
return value;
|
|
20604
|
+
};
|
|
20605
|
+
//#endregion
|
|
19492
20606
|
//#region src/commands/init.ts
|
|
19493
20607
|
const buildInitSuccessRender = (input) => {
|
|
19494
20608
|
const deps = {
|
|
@@ -19558,13 +20672,13 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
|
|
|
19558
20672
|
};
|
|
19559
20673
|
};
|
|
19560
20674
|
const promptForConfigChoices = async () => {
|
|
19561
|
-
const enginesSelection = await multiselect({
|
|
20675
|
+
const enginesSelection = await runCancellable(() => multiselect({
|
|
19562
20676
|
message: "Which engines should run?",
|
|
19563
20677
|
options: ENGINE_CHOICES,
|
|
19564
20678
|
initialValues: DEFAULT_ENGINE_SELECTION,
|
|
19565
20679
|
required: false
|
|
19566
|
-
});
|
|
19567
|
-
if (
|
|
20680
|
+
}));
|
|
20681
|
+
if (enginesSelection === void 0) return null;
|
|
19568
20682
|
const failBelowRaw = await text({
|
|
19569
20683
|
message: "CI quality gate — fail the build when the score drops below (0-100)",
|
|
19570
20684
|
initialValue: "70",
|