@methiu/raigal 0.2.5 → 0.2.7
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/LICENSE +6 -1
- package/README.md +75 -37
- package/THIRD_PARTY_NOTICES.md +70 -0
- package/dist/cli.js +588 -185
- package/dist/index.d.ts +1 -0
- package/dist/index.js +536 -177
- package/dist/{json-B8XGYwMv.js → json-B2Kc7_rw.js} +1 -1
- package/dist/mcp.js +187 -72
- package/dist/{sarif-DqH4dGEo.js → sarif-CpMse0iL.js} +1 -1
- package/dist/{version-BlgPglaO.js → version-CXMYu17z.js} +1 -1
- package/package.json +7 -4
- package/scripts/check-tarball-keys.mjs +26 -0
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-CXMYu17z.js";
|
|
2
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";
|
|
@@ -109,10 +109,53 @@ jobs:
|
|
|
109
109
|
runs-on: ubuntu-latest
|
|
110
110
|
steps:
|
|
111
111
|
- uses: actions/checkout@v4
|
|
112
|
+
with:
|
|
113
|
+
fetch-depth: 0
|
|
112
114
|
- uses: actions/setup-node@v4
|
|
113
115
|
with:
|
|
114
116
|
node-version: 22
|
|
115
117
|
- run: npx --yes @methiu/raigal@latest ci .
|
|
118
|
+
env:
|
|
119
|
+
RAIGAL_TOKEN: \${{ secrets.RAIGAL_TOKEN }}
|
|
120
|
+
# RAIGAL_ORG_ID: \${{ vars.RAIGAL_ORG_ID }} # Optional: required if repository owner belongs to multiple Raigal organizations
|
|
121
|
+
`;
|
|
122
|
+
const DEFAULT_GITLAB_CI_FILE = ".gitlab-ci.yml";
|
|
123
|
+
const DEFAULT_GITLAB_CI_YAML = `raigal_quality_gate:
|
|
124
|
+
image: node:24
|
|
125
|
+
stage: test
|
|
126
|
+
script:
|
|
127
|
+
- if [ -n "$CI_MERGE_REQUEST_IID" ]; then npx --yes @methiu/raigal@latest ci --changes --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"; else npx --yes @methiu/raigal@latest ci .; fi
|
|
128
|
+
variables:
|
|
129
|
+
RAIGAL_TOKEN: $RAIGAL_TOKEN
|
|
130
|
+
# RAIGAL_ORG_ID: $RAIGAL_ORG_ID # Optional: required if repository owner belongs to multiple Raigal organizations
|
|
131
|
+
rules:
|
|
132
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
133
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
134
|
+
`;
|
|
135
|
+
const DEFAULT_BITBUCKET_FILE = "bitbucket-pipelines.yml";
|
|
136
|
+
const DEFAULT_BITBUCKET_PIPELINES_YAML = `pipelines:
|
|
137
|
+
default:
|
|
138
|
+
- step:
|
|
139
|
+
name: raigal quality gate
|
|
140
|
+
image: node:24
|
|
141
|
+
script:
|
|
142
|
+
- npx --yes @methiu/raigal@latest ci .
|
|
143
|
+
secrets:
|
|
144
|
+
- RAIGAL_TOKEN
|
|
145
|
+
# - RAIGAL_ORG_ID
|
|
146
|
+
pull-requests:
|
|
147
|
+
"**":
|
|
148
|
+
- step:
|
|
149
|
+
name: raigal quality gate
|
|
150
|
+
image: node:24
|
|
151
|
+
clone:
|
|
152
|
+
depth: full
|
|
153
|
+
script:
|
|
154
|
+
- git fetch origin "$BITBUCKET_PR_DESTINATION_BRANCH"
|
|
155
|
+
- npx --yes @methiu/raigal@latest ci --changes --base FETCH_HEAD
|
|
156
|
+
secrets:
|
|
157
|
+
- RAIGAL_TOKEN
|
|
158
|
+
# - RAIGAL_ORG_ID
|
|
116
159
|
`;
|
|
117
160
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
118
161
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -1314,7 +1357,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
|
|
|
1314
1357
|
return false;
|
|
1315
1358
|
}
|
|
1316
1359
|
};
|
|
1317
|
-
const
|
|
1360
|
+
const SECRET_CONFIG_EXTENSIONS = [
|
|
1361
|
+
".env",
|
|
1362
|
+
".yaml",
|
|
1363
|
+
".yml",
|
|
1364
|
+
".json",
|
|
1365
|
+
".toml",
|
|
1366
|
+
".ini",
|
|
1367
|
+
".properties",
|
|
1368
|
+
".tfvars"
|
|
1369
|
+
];
|
|
1370
|
+
const LOCKFILE_RE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|packages\.lock\.json|composer\.lock|bun\.lock)$/i;
|
|
1371
|
+
const isDotenvFile = (filePath) => {
|
|
1372
|
+
const base = path.basename(filePath).toLowerCase();
|
|
1373
|
+
return base === ".env" || base.startsWith(".env.");
|
|
1374
|
+
};
|
|
1375
|
+
const hasAllowedSourceExtension = (filePath, extraExtensions) => {
|
|
1376
|
+
const extension = path.extname(filePath);
|
|
1377
|
+
if (SOURCE_EXTENSIONS$1.has(extension)) return true;
|
|
1378
|
+
if (LOCKFILE_RE.test(filePath)) return false;
|
|
1379
|
+
if (extraExtensions.has(".env") && isDotenvFile(filePath)) return true;
|
|
1380
|
+
return extraExtensions.has(extension);
|
|
1381
|
+
};
|
|
1318
1382
|
const isInExcludedDirectory = (filePath, excludedDirectories) => {
|
|
1319
1383
|
const normalized = filePath.toLowerCase();
|
|
1320
1384
|
return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
|
|
@@ -2431,12 +2495,13 @@ const baseRefExists = (cwd, ref) => {
|
|
|
2431
2495
|
return !result.error && result.status === 0;
|
|
2432
2496
|
};
|
|
2433
2497
|
const getChangedFiles = (cwd, base) => {
|
|
2498
|
+
const trimmedBase = base?.trim();
|
|
2434
2499
|
const diff = spawnSync("git", [
|
|
2435
2500
|
"diff",
|
|
2436
2501
|
"--no-color",
|
|
2437
2502
|
"--name-only",
|
|
2438
2503
|
"--diff-filter=ACMR",
|
|
2439
|
-
|
|
2504
|
+
trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
|
|
2440
2505
|
], {
|
|
2441
2506
|
cwd,
|
|
2442
2507
|
encoding: "utf-8",
|
|
@@ -2488,13 +2553,14 @@ const listUntrackedFiles = (cwd) => {
|
|
|
2488
2553
|
return untracked.stdout.split("\n").filter((line) => line.length > 0);
|
|
2489
2554
|
};
|
|
2490
2555
|
const getChangedLineMap = (cwd, base) => {
|
|
2556
|
+
const trimmedBase = base?.trim();
|
|
2491
2557
|
const diff = spawnSync("git", [
|
|
2492
2558
|
"diff",
|
|
2493
2559
|
"--no-color",
|
|
2494
2560
|
"--no-ext-diff",
|
|
2495
2561
|
"-U0",
|
|
2496
2562
|
"--diff-filter=ACMR",
|
|
2497
|
-
|
|
2563
|
+
trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
|
|
2498
2564
|
], {
|
|
2499
2565
|
cwd,
|
|
2500
2566
|
encoding: "utf-8",
|
|
@@ -2543,6 +2609,7 @@ const collectScanFileScope = (request) => {
|
|
|
2543
2609
|
projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...declarations])],
|
|
2544
2610
|
scoreFileCount: projectSourceFiles.length + projectTestFiles.length,
|
|
2545
2611
|
scopeLabel: candidates.label,
|
|
2612
|
+
secretFiles: filterEnumeratedProjectFiles(request.rootDirectory, candidates.files, [...SECRET_CONFIG_EXTENSIONS], request.excludePatterns, request.includePatterns),
|
|
2546
2613
|
testFiles
|
|
2547
2614
|
};
|
|
2548
2615
|
};
|
|
@@ -14762,13 +14829,19 @@ const SECRET_PATTERNS = [
|
|
|
14762
14829
|
},
|
|
14763
14830
|
{
|
|
14764
14831
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
14765
|
-
name: "AWS Access Key"
|
|
14832
|
+
name: "AWS Access Key",
|
|
14833
|
+
specific: true
|
|
14766
14834
|
},
|
|
14767
14835
|
{
|
|
14768
|
-
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
14836
|
+
pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
14769
14837
|
name: "AWS Secret Key",
|
|
14770
14838
|
keywordPrefixed: true
|
|
14771
14839
|
},
|
|
14840
|
+
{
|
|
14841
|
+
pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
|
|
14842
|
+
name: "Stripe secret key",
|
|
14843
|
+
specific: true
|
|
14844
|
+
},
|
|
14772
14845
|
{
|
|
14773
14846
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
14774
14847
|
name: "Hardcoded password/secret",
|
|
@@ -14776,11 +14849,13 @@ const SECRET_PATTERNS = [
|
|
|
14776
14849
|
},
|
|
14777
14850
|
{
|
|
14778
14851
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
14779
|
-
name: "Private key"
|
|
14852
|
+
name: "Private key",
|
|
14853
|
+
specific: true
|
|
14780
14854
|
},
|
|
14781
14855
|
{
|
|
14782
14856
|
pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
14783
|
-
name: "JWT token"
|
|
14857
|
+
name: "JWT token",
|
|
14858
|
+
specific: true
|
|
14784
14859
|
},
|
|
14785
14860
|
{
|
|
14786
14861
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
@@ -14789,17 +14864,51 @@ const SECRET_PATTERNS = [
|
|
|
14789
14864
|
},
|
|
14790
14865
|
{
|
|
14791
14866
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
14792
|
-
name: "GitHub token"
|
|
14867
|
+
name: "GitHub token",
|
|
14868
|
+
specific: true
|
|
14793
14869
|
},
|
|
14794
14870
|
{
|
|
14795
14871
|
pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
|
|
14796
|
-
name: "Slack token"
|
|
14872
|
+
name: "Slack token",
|
|
14873
|
+
specific: true
|
|
14797
14874
|
},
|
|
14798
14875
|
{
|
|
14799
14876
|
pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
|
|
14800
|
-
name: "Database connection string with credentials"
|
|
14877
|
+
name: "Database connection string with credentials",
|
|
14878
|
+
specific: true
|
|
14801
14879
|
}
|
|
14802
14880
|
];
|
|
14881
|
+
const FORMAT_PATTERNS = SECRET_PATTERNS.filter((p) => p.specific);
|
|
14882
|
+
const CONFIG_ASSIGNMENT_RE = /([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey)[A-Za-z0-9_.-]*)["']?[ \t]*[:=][ \t]*["']?([^\s"'#,]+)/gi;
|
|
14883
|
+
const CONFIG_REFERENCE_KEY_RE = /(?:name|ref|path|file|arn|id|type|version|url|uri|length|count|env|header|field|prefix|policy|manager|store|provider)$/i;
|
|
14884
|
+
const CONFIG_REFERENCE_VALUE_RE = /^[<{$%]/;
|
|
14885
|
+
const CONFIG_PLACEHOLDER_VALUE_RE = /^(?:your|example|placeholder|redacted|dummy|xxx)/i;
|
|
14886
|
+
const VERSION_VALUE_RE = /^[~^<>=v]*\d+(?:\.\d+)+/;
|
|
14887
|
+
const CONFIG_LITERAL_VALUES = /* @__PURE__ */ new Set([
|
|
14888
|
+
"true",
|
|
14889
|
+
"false",
|
|
14890
|
+
"null",
|
|
14891
|
+
"none",
|
|
14892
|
+
"nil"
|
|
14893
|
+
]);
|
|
14894
|
+
const isConfigFile = (filePath) => isDotenvFile(filePath) || SECRET_CONFIG_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
|
|
14895
|
+
const DOTENV_TEMPLATE_RE = /^\.env\.(?:example|sample|template|dist|defaults)$/i;
|
|
14896
|
+
const isLocalDatabaseUrl = (value) => {
|
|
14897
|
+
try {
|
|
14898
|
+
return LOCAL_DB_HOSTS.has(new URL(value).hostname.toLowerCase());
|
|
14899
|
+
} catch {
|
|
14900
|
+
return false;
|
|
14901
|
+
}
|
|
14902
|
+
};
|
|
14903
|
+
const configAssignmentName = (key) => {
|
|
14904
|
+
const lower = key.toLowerCase();
|
|
14905
|
+
if (/aws[_-]?secret/.test(lower)) return "AWS Secret Key";
|
|
14906
|
+
if (/api[_-]?key|apikey/.test(lower)) return "API key";
|
|
14907
|
+
if (/token/.test(lower) && !/password|passwd|pwd|secret/.test(lower)) return "Authentication token";
|
|
14908
|
+
return "Hardcoded password/secret";
|
|
14909
|
+
};
|
|
14910
|
+
const URL_VALUE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
14911
|
+
const isConfigValueIgnorable = (key, value) => value.length < 8 || URL_VALUE_RE.test(value) || CONFIG_REFERENCE_KEY_RE.test(key) || CONFIG_REFERENCE_VALUE_RE.test(value) || CONFIG_PLACEHOLDER_VALUE_RE.test(value) || VERSION_VALUE_RE.test(value) || CONFIG_LITERAL_VALUES.has(value.toLowerCase()) || isNonProductionCredentialValue(value);
|
|
14803
14912
|
const isInsideStringLiteral = (content, matchIndex) => {
|
|
14804
14913
|
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
14805
14914
|
const prefix = content.slice(lineStart, matchIndex);
|
|
@@ -14944,16 +15053,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
|
|
|
14944
15053
|
if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
|
|
14945
15054
|
return false;
|
|
14946
15055
|
};
|
|
15056
|
+
const collectSourceCandidates = (content, relativePath) => {
|
|
15057
|
+
const candidates = [];
|
|
15058
|
+
for (const { pattern, name, keywordPrefixed, specific } of SECRET_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
15059
|
+
const matchedText = match[1] ?? match[0];
|
|
15060
|
+
const lineText = lineForMatch(content, match.index);
|
|
15061
|
+
if (isPlaceholderValue(matchedText)) continue;
|
|
15062
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
|
|
15063
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
15064
|
+
candidates.push({
|
|
15065
|
+
name,
|
|
15066
|
+
start: match.index,
|
|
15067
|
+
end: match.index + match[0].length,
|
|
15068
|
+
specific: Boolean(specific)
|
|
15069
|
+
});
|
|
15070
|
+
}
|
|
15071
|
+
return candidates;
|
|
15072
|
+
};
|
|
15073
|
+
const collectConfigCandidates = (content, relativePath) => {
|
|
15074
|
+
const candidates = [];
|
|
15075
|
+
const template = DOTENV_TEMPLATE_RE.test(path.posix.basename(relativePath));
|
|
15076
|
+
for (const { pattern, name } of FORMAT_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
15077
|
+
const lineText = lineForMatch(content, match.index);
|
|
15078
|
+
if (isPlaceholderValue(match[0])) continue;
|
|
15079
|
+
if (template && isLocalDatabaseUrl(match[0])) continue;
|
|
15080
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, match[0])) continue;
|
|
15081
|
+
candidates.push({
|
|
15082
|
+
name,
|
|
15083
|
+
start: match.index,
|
|
15084
|
+
end: match.index + match[0].length,
|
|
15085
|
+
specific: true
|
|
15086
|
+
});
|
|
15087
|
+
}
|
|
15088
|
+
for (const match of content.matchAll(new RegExp(CONFIG_ASSIGNMENT_RE.source, CONFIG_ASSIGNMENT_RE.flags))) {
|
|
15089
|
+
const [, key, value] = match;
|
|
15090
|
+
const name = configAssignmentName(key);
|
|
15091
|
+
const lineText = lineForMatch(content, match.index);
|
|
15092
|
+
if (isConfigValueIgnorable(key, value) || isPlaceholderValue(value)) continue;
|
|
15093
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, value)) continue;
|
|
15094
|
+
candidates.push({
|
|
15095
|
+
name,
|
|
15096
|
+
start: match.index,
|
|
15097
|
+
end: match.index + match[0].length,
|
|
15098
|
+
specific: false
|
|
15099
|
+
});
|
|
15100
|
+
}
|
|
15101
|
+
return candidates;
|
|
15102
|
+
};
|
|
15103
|
+
const dedupeCandidates = (candidates) => {
|
|
15104
|
+
const ordered = [...candidates].sort((a, b) => Number(b.specific) - Number(a.specific) || a.start - b.start);
|
|
15105
|
+
const kept = [];
|
|
15106
|
+
for (const candidate of ordered) {
|
|
15107
|
+
if (kept.some((k) => candidate.start < k.end && k.start < candidate.end)) continue;
|
|
15108
|
+
kept.push(candidate);
|
|
15109
|
+
}
|
|
15110
|
+
return kept.sort((a, b) => a.start - b.start);
|
|
15111
|
+
};
|
|
15112
|
+
const resolveSecretFiles = (context) => {
|
|
15113
|
+
const extras = [...SECRET_CONFIG_EXTENSIONS];
|
|
15114
|
+
return context.secretFiles ? filterExplicitFiles(context.rootDirectory, context.secretFiles, extras) : getSourceFilesWithExtras(context, extras);
|
|
15115
|
+
};
|
|
14947
15116
|
const scanSecrets = async (context) => {
|
|
14948
|
-
const files = getSourceFilesWithExtras(context, [
|
|
14949
|
-
".env",
|
|
14950
|
-
".yaml",
|
|
14951
|
-
".yml",
|
|
14952
|
-
".json",
|
|
14953
|
-
".toml"
|
|
14954
|
-
]);
|
|
14955
15117
|
const diagnostics = [];
|
|
14956
|
-
for (const filePath of
|
|
15118
|
+
for (const filePath of resolveSecretFiles(context)) {
|
|
14957
15119
|
let content;
|
|
14958
15120
|
try {
|
|
14959
15121
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -14962,30 +15124,20 @@ const scanSecrets = async (context) => {
|
|
|
14962
15124
|
}
|
|
14963
15125
|
content = maskComments(content, path.extname(filePath));
|
|
14964
15126
|
const relativePath = relativePosix(context.rootDirectory, filePath);
|
|
14965
|
-
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
message: `Possible ${name} detected in source code`,
|
|
14980
|
-
help: "Move secrets to environment variables or a secrets manager",
|
|
14981
|
-
line,
|
|
14982
|
-
column: 0,
|
|
14983
|
-
category: "Security",
|
|
14984
|
-
fixable: false,
|
|
14985
|
-
redactSource: true
|
|
14986
|
-
});
|
|
14987
|
-
}
|
|
14988
|
-
}
|
|
15127
|
+
const candidates = isConfigFile(filePath) ? collectConfigCandidates(content, relativePath) : collectSourceCandidates(content, relativePath);
|
|
15128
|
+
for (const { name, start } of dedupeCandidates(candidates)) diagnostics.push({
|
|
15129
|
+
filePath: relativePath,
|
|
15130
|
+
engine: "security",
|
|
15131
|
+
rule: "security/hardcoded-secret",
|
|
15132
|
+
severity: "error",
|
|
15133
|
+
message: `Possible ${name} detected in source code`,
|
|
15134
|
+
help: "Move secrets to environment variables or a secrets manager",
|
|
15135
|
+
line: content.slice(0, start).split("\n").length,
|
|
15136
|
+
column: 0,
|
|
15137
|
+
category: "Security",
|
|
15138
|
+
fixable: false,
|
|
15139
|
+
redactSource: true
|
|
15140
|
+
});
|
|
14989
15141
|
}
|
|
14990
15142
|
return diagnostics;
|
|
14991
15143
|
};
|
|
@@ -16266,7 +16418,7 @@ const AllowedOwner = z.object({
|
|
|
16266
16418
|
login: z.string().max(100)
|
|
16267
16419
|
});
|
|
16268
16420
|
const EntitlementClaims = z.object({
|
|
16269
|
-
iss: z.literal(
|
|
16421
|
+
iss: z.literal("https://app.raigal.dev"),
|
|
16270
16422
|
sub: z.string(),
|
|
16271
16423
|
org_name: z.string(),
|
|
16272
16424
|
plan: z.enum(["trial", "active"]),
|
|
@@ -16323,6 +16475,8 @@ const requestApi = async (endpoint, options = {}) => {
|
|
|
16323
16475
|
};
|
|
16324
16476
|
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
16325
16477
|
if (options.oidcToken) headers["X-GitHub-OIDC"] = options.oidcToken;
|
|
16478
|
+
const orgId = options.orgId || process.env.RAIGAL_ORG_ID;
|
|
16479
|
+
if (orgId) headers["X-Raigal-Org-Id"] = orgId;
|
|
16326
16480
|
let body;
|
|
16327
16481
|
if (options.body !== void 0) {
|
|
16328
16482
|
headers["Content-Type"] = "application/json";
|
|
@@ -16354,11 +16508,12 @@ const requestApi = async (endpoint, options = {}) => {
|
|
|
16354
16508
|
}
|
|
16355
16509
|
return json;
|
|
16356
16510
|
};
|
|
16357
|
-
const fetchEntitlementFromApi = async (token, oidcToken) => {
|
|
16511
|
+
const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
|
|
16358
16512
|
const data = await requestApi("/v1/entitlement", {
|
|
16359
16513
|
method: "POST",
|
|
16360
16514
|
token: token || void 0,
|
|
16361
|
-
oidcToken
|
|
16515
|
+
oidcToken,
|
|
16516
|
+
orgId
|
|
16362
16517
|
});
|
|
16363
16518
|
return EntitlementResponse.parse(data);
|
|
16364
16519
|
};
|
|
@@ -16371,14 +16526,6 @@ const fetchEntitlementFromApi = async (token, oidcToken) => {
|
|
|
16371
16526
|
const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
|
|
16372
16527
|
const keyObjectCache = /* @__PURE__ */ new Map();
|
|
16373
16528
|
const getPublicKey = (kid) => {
|
|
16374
|
-
if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
|
|
16375
|
-
const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
|
|
16376
|
-
try {
|
|
16377
|
-
return createPublicKey(customKey);
|
|
16378
|
-
} catch {
|
|
16379
|
-
return null;
|
|
16380
|
-
}
|
|
16381
|
-
}
|
|
16382
16529
|
const cached = keyObjectCache.get(kid);
|
|
16383
16530
|
if (cached) return cached;
|
|
16384
16531
|
const pem = EMBEDDED_PUBLIC_KEYS[kid];
|
|
@@ -16438,9 +16585,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
16438
16585
|
const parsed = EntitlementClaims.safeParse(rawPayload);
|
|
16439
16586
|
if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
|
|
16440
16587
|
const claims = parsed.data;
|
|
16441
|
-
|
|
16442
|
-
if (claims.
|
|
16443
|
-
if (!isVersionAtLeast("0.2.5", 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");
|
|
16588
|
+
if (claims.iss !== "https://app.raigal.dev") throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
|
|
16589
|
+
if (!isVersionAtLeast("0.2.7", 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");
|
|
16444
16590
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
16445
16591
|
const grace = options.allowGraceSeconds ?? 0;
|
|
16446
16592
|
if (now > claims.exp + grace) {
|
|
@@ -16599,7 +16745,9 @@ const getEntitlement = async () => {
|
|
|
16599
16745
|
}
|
|
16600
16746
|
} else oidcToken = await fetchGitHubOidcToken();
|
|
16601
16747
|
try {
|
|
16602
|
-
const
|
|
16748
|
+
const tokenToPass = activeToken?.token ?? "";
|
|
16749
|
+
const orgId = process.env.RAIGAL_ORG_ID;
|
|
16750
|
+
const response = await fetchEntitlementFromApi(tokenToPass, oidcToken, orgId);
|
|
16603
16751
|
const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
|
|
16604
16752
|
saveCachedEntitlement(response.entitlement, response.refresh_after);
|
|
16605
16753
|
return {
|
|
@@ -16613,7 +16761,7 @@ const getEntitlement = async () => {
|
|
|
16613
16761
|
};
|
|
16614
16762
|
//#endregion
|
|
16615
16763
|
//#region src/cloud/owner.ts
|
|
16616
|
-
const
|
|
16764
|
+
const GENERIC_GIT_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)[^:/]+(?::(?:\d+\/)?|[:/])|https?:\/\/(?:[^@]+@)?[^/]+\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
|
|
16617
16765
|
const parseGitRemote = (rawUrl) => {
|
|
16618
16766
|
if (!rawUrl) return {
|
|
16619
16767
|
owner: null,
|
|
@@ -16621,18 +16769,19 @@ const parseGitRemote = (rawUrl) => {
|
|
|
16621
16769
|
hasRemote: false,
|
|
16622
16770
|
isGitHubRemote: false
|
|
16623
16771
|
};
|
|
16624
|
-
const match = rawUrl.match(
|
|
16772
|
+
const match = rawUrl.match(GENERIC_GIT_REMOTE_RE);
|
|
16625
16773
|
if (!match || !match[1] || !match[2]) return {
|
|
16626
16774
|
owner: null,
|
|
16627
16775
|
repo: null,
|
|
16628
16776
|
hasRemote: true,
|
|
16629
16777
|
isGitHubRemote: false
|
|
16630
16778
|
};
|
|
16779
|
+
const isGitHub = rawUrl.includes("github.com");
|
|
16631
16780
|
return {
|
|
16632
16781
|
owner: match[1].toLowerCase(),
|
|
16633
16782
|
repo: match[2].toLowerCase(),
|
|
16634
16783
|
hasRemote: true,
|
|
16635
|
-
isGitHubRemote:
|
|
16784
|
+
isGitHubRemote: isGitHub
|
|
16636
16785
|
};
|
|
16637
16786
|
};
|
|
16638
16787
|
const readGitConfigRemote = (dir) => {
|
|
@@ -16697,6 +16846,27 @@ const detectLocalRepoOwner = (directory) => {
|
|
|
16697
16846
|
isGitHubRemote: true
|
|
16698
16847
|
};
|
|
16699
16848
|
}
|
|
16849
|
+
const gitlabRepo = process.env.CI_PROJECT_PATH?.trim();
|
|
16850
|
+
if (gitlabRepo && gitlabRepo.includes("/")) {
|
|
16851
|
+
const parts = gitlabRepo.split("/");
|
|
16852
|
+
const repo = parts.pop() ?? "";
|
|
16853
|
+
return {
|
|
16854
|
+
owner: (parts[0] ?? "").toLowerCase(),
|
|
16855
|
+
repo: repo.toLowerCase(),
|
|
16856
|
+
hasRemote: true,
|
|
16857
|
+
isGitHubRemote: false
|
|
16858
|
+
};
|
|
16859
|
+
}
|
|
16860
|
+
const bitbucketRepo = (process.env.BITBUCKET_REPO_FULL_NAME || process.env.BITBUCKET_REPO_SLUG)?.trim();
|
|
16861
|
+
if (bitbucketRepo) {
|
|
16862
|
+
const [owner = process.env.BITBUCKET_WORKSPACE || "", repo = ""] = bitbucketRepo.includes("/") ? bitbucketRepo.split("/") : [process.env.BITBUCKET_WORKSPACE || "", bitbucketRepo];
|
|
16863
|
+
return {
|
|
16864
|
+
owner: owner.toLowerCase(),
|
|
16865
|
+
repo: repo.toLowerCase(),
|
|
16866
|
+
hasRemote: true,
|
|
16867
|
+
isGitHubRemote: false
|
|
16868
|
+
};
|
|
16869
|
+
}
|
|
16700
16870
|
const gitRoot = findGitRoot(directory);
|
|
16701
16871
|
if (!gitRoot) return {
|
|
16702
16872
|
owner: null,
|
|
@@ -16708,26 +16878,17 @@ const detectLocalRepoOwner = (directory) => {
|
|
|
16708
16878
|
if (rawUrl !== null) return parseGitRemote(rawUrl);
|
|
16709
16879
|
return parseGitRemote(spawnGitRemote(gitRoot));
|
|
16710
16880
|
};
|
|
16711
|
-
const isNonGitHubCi = () => {
|
|
16712
|
-
const env = process.env;
|
|
16713
|
-
if (!(env.CI === "true" || env.CI === "1")) return false;
|
|
16714
|
-
return Boolean(env.GITLAB_CI || env.BITBUCKET_BUILD_NUMBER || env.CIRCLECI && !env.CIRCLE_PROJECT_REPONAME?.includes("/") || env.BUILDKITE || env.TRAVIS || env.JENKINS_URL);
|
|
16715
|
-
};
|
|
16716
16881
|
const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
|
|
16717
|
-
if (isNonGitHubCi()) return {
|
|
16718
|
-
allowed: false,
|
|
16719
|
-
reason: "Non-GitHub CI environments are unsupported under organization allowlists."
|
|
16720
|
-
};
|
|
16721
16882
|
if (!detected.hasRemote) {
|
|
16722
16883
|
if (tokenKind === "session") return { allowed: true };
|
|
16723
16884
|
return {
|
|
16724
16885
|
allowed: false,
|
|
16725
|
-
reason: "Repository has no
|
|
16886
|
+
reason: "Repository has no origin remote. CI and API key runs require an origin remote matching an allowed organization."
|
|
16726
16887
|
};
|
|
16727
16888
|
}
|
|
16728
|
-
if (!detected.
|
|
16889
|
+
if (!detected.owner) return {
|
|
16729
16890
|
allowed: false,
|
|
16730
|
-
reason: "Repository remote
|
|
16891
|
+
reason: "Repository remote owner could not be determined."
|
|
16731
16892
|
};
|
|
16732
16893
|
const lowerOwner = detected.owner.toLowerCase();
|
|
16733
16894
|
if (!allowedOwners.some((allowed) => allowed.login.toLowerCase() === lowerOwner)) {
|
|
@@ -16735,7 +16896,7 @@ const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
|
|
|
16735
16896
|
return {
|
|
16736
16897
|
allowed: false,
|
|
16737
16898
|
owner: lowerOwner,
|
|
16738
|
-
reason: `
|
|
16899
|
+
reason: `Repository owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
|
|
16739
16900
|
};
|
|
16740
16901
|
}
|
|
16741
16902
|
return {
|
|
@@ -17990,10 +18151,13 @@ const isHistoryComparableScan = (options) => !options.staged && !options.changes
|
|
|
17990
18151
|
const isFullProjectScan = (options) => isHistoryComparableScan(options) && !options.include?.length && !options.exclude?.length;
|
|
17991
18152
|
const resolveScanScopeMode = (options) => {
|
|
17992
18153
|
if (options.staged) return { kind: "staged" };
|
|
17993
|
-
if (options.changes)
|
|
17994
|
-
|
|
17995
|
-
base
|
|
17996
|
-
|
|
18154
|
+
if (options.changes) {
|
|
18155
|
+
const base = options.base?.trim();
|
|
18156
|
+
return base ? {
|
|
18157
|
+
kind: "changes",
|
|
18158
|
+
base
|
|
18159
|
+
} : { kind: "changes" };
|
|
18160
|
+
}
|
|
17997
18161
|
return { kind: "full" };
|
|
17998
18162
|
};
|
|
17999
18163
|
//#endregion
|
|
@@ -18001,7 +18165,8 @@ const resolveScanScopeMode = (options) => {
|
|
|
18001
18165
|
const scanTargetError = (resolvedDir, options) => {
|
|
18002
18166
|
if (!fs.existsSync(resolvedDir)) return `Path does not exist: ${resolvedDir}`;
|
|
18003
18167
|
if (!fs.statSync(resolvedDir).isDirectory()) return `Not a directory: ${resolvedDir}`;
|
|
18004
|
-
|
|
18168
|
+
const base = options.base?.trim();
|
|
18169
|
+
if (options.changes && base && !baseRefExists(resolvedDir, base)) return `Could not resolve base ref "${base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${base}\`).`;
|
|
18005
18170
|
return null;
|
|
18006
18171
|
};
|
|
18007
18172
|
//#endregion
|
|
@@ -18656,13 +18821,21 @@ const recordFullScanActivity = (history, allowPilotInvitation, telemetryConfig)
|
|
|
18656
18821
|
appendHistory(history);
|
|
18657
18822
|
return allowPilotInvitation && !isHistoryDisabled() && !isTelemetryDisabled(telemetryConfig) && recordCompletedFullScan();
|
|
18658
18823
|
};
|
|
18659
|
-
|
|
18824
|
+
//#endregion
|
|
18825
|
+
//#region src/hooks/assets.ts
|
|
18826
|
+
const buildRaigalMdBody = (agent) => {
|
|
18827
|
+
let onEditSection;
|
|
18828
|
+
if (agent === "claude") onEditSection = `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.`;
|
|
18829
|
+
else if (agent === "gemini") onEditSection = `An AfterTool hook runs \`raigal hook gemini\` after file modifications. It scans the touched files and returns findings. Act on them the same turn; fix reported issues before completing.`;
|
|
18830
|
+
else if (agent === "cursor") onEditSection = `An afterFileEdit hook runs \`raigal hook cursor\` after file modifications. It scans the touched files and reports quality findings. Fix reported errors and fixable warnings before completing your turn.`;
|
|
18831
|
+
else onEditSection = `Raigal scans changed files and reports code quality, security, and AI pattern findings. Act on reported findings during your turn; fix errors and fixable warnings before completing your response.`;
|
|
18832
|
+
return `# Raigal - agent instructions
|
|
18660
18833
|
|
|
18661
18834
|
[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.
|
|
18662
18835
|
|
|
18663
18836
|
## On every edit
|
|
18664
18837
|
|
|
18665
|
-
|
|
18838
|
+
${onEditSection}
|
|
18666
18839
|
|
|
18667
18840
|
## Severity ladder
|
|
18668
18841
|
|
|
@@ -18682,6 +18855,8 @@ A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or Multi
|
|
|
18682
18855
|
- If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
|
|
18683
18856
|
- The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
|
|
18684
18857
|
`;
|
|
18858
|
+
};
|
|
18859
|
+
buildRaigalMdBody("claude");
|
|
18685
18860
|
//#endregion
|
|
18686
18861
|
//#region src/hooks/io/atomic-write.ts
|
|
18687
18862
|
const atomicWrite = (targetPath, content) => {
|
|
@@ -18712,12 +18887,12 @@ const readIfExists = (targetPath) => {
|
|
|
18712
18887
|
//#endregion
|
|
18713
18888
|
//#region src/hooks/io/sentinel.ts
|
|
18714
18889
|
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
18715
|
-
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
18716
|
-
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
18890
|
+
const BEGIN_RE = /<!--\s*(?:raigal|aislop):begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
18891
|
+
const END_RE = /<!--\s*(?:raigal|aislop):end\s+v\d+\s*-->/;
|
|
18717
18892
|
const renderFence = (body, hash) => [
|
|
18718
|
-
`<!--
|
|
18893
|
+
`<!-- raigal:begin v1 hash=${hash} -->`,
|
|
18719
18894
|
body.trimEnd(),
|
|
18720
|
-
"<!--
|
|
18895
|
+
"<!-- raigal:end v1 -->"
|
|
18721
18896
|
].join("\n");
|
|
18722
18897
|
const upsertMarkdownFence = (existing, body, hash) => {
|
|
18723
18898
|
const fenced = renderFence(body, hash);
|
|
@@ -18782,11 +18957,20 @@ const applyRemoval = (result, opts, target, nextContent) => {
|
|
|
18782
18957
|
//#region src/hooks/install/rules-only.ts
|
|
18783
18958
|
const installRulesOnly = (opts, paths, summary) => {
|
|
18784
18959
|
const result = emptyResult();
|
|
18785
|
-
const
|
|
18960
|
+
const body = paths.body ?? buildRaigalMdBody();
|
|
18961
|
+
const next = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
|
|
18786
18962
|
applyContent(result, opts, paths.rules, next, summary);
|
|
18963
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
18964
|
+
if (!opts.dryRun) try {
|
|
18965
|
+
fs.rmSync(paths.legacyRules, { force: true });
|
|
18966
|
+
} catch {}
|
|
18967
|
+
}
|
|
18787
18968
|
if (paths.host && paths.marker) {
|
|
18788
18969
|
const host = readIfExists(paths.host) ?? "";
|
|
18789
|
-
if (
|
|
18970
|
+
if (paths.legacyMarker && host.includes(paths.legacyMarker)) {
|
|
18971
|
+
const migrated = host.split("\n").map((l) => l.trim() === paths.legacyMarker ? paths.marker : l).join("\n");
|
|
18972
|
+
applyContent(result, opts, paths.host, migrated, `update ${paths.marker} reference`);
|
|
18973
|
+
} else if (!host.includes(paths.marker)) {
|
|
18790
18974
|
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
18791
18975
|
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
18792
18976
|
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
@@ -18799,30 +18983,41 @@ const uninstallRulesOnly = (opts, paths) => {
|
|
|
18799
18983
|
removed: [],
|
|
18800
18984
|
skipped: []
|
|
18801
18985
|
};
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18986
|
+
let removedRule = false;
|
|
18987
|
+
if (readIfExists(paths.rules) != null) {
|
|
18988
|
+
applyRemoval(result, opts, paths.rules, null);
|
|
18989
|
+
removedRule = true;
|
|
18990
|
+
}
|
|
18991
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
18992
|
+
applyRemoval(result, opts, paths.legacyRules, null);
|
|
18993
|
+
removedRule = true;
|
|
18994
|
+
}
|
|
18995
|
+
if (!removedRule) result.skipped.push(paths.rules);
|
|
18996
|
+
if (paths.host && (paths.marker || paths.legacyMarker)) {
|
|
18805
18997
|
const host = readIfExists(paths.host);
|
|
18806
|
-
if (host
|
|
18807
|
-
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
18998
|
+
if (host && (paths.marker && host.includes(paths.marker) || paths.legacyMarker && host.includes(paths.legacyMarker))) {
|
|
18999
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker && l.trim() !== paths.legacyMarker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
18808
19000
|
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
18809
|
-
} else result.skipped.push(paths.host);
|
|
19001
|
+
} else if (paths.host) result.skipped.push(paths.host);
|
|
18810
19002
|
}
|
|
18811
19003
|
return result;
|
|
18812
19004
|
};
|
|
18813
19005
|
//#endregion
|
|
18814
19006
|
//#region src/hooks/install/antigravity.ts
|
|
18815
|
-
const resolveAntigravityPaths = (opts) => ({
|
|
19007
|
+
const resolveAntigravityPaths = (opts) => ({
|
|
19008
|
+
rules: path.join(opts.cwd, ".agents", "rules", "antigravity-raigal-rules.md"),
|
|
19009
|
+
legacyRules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md")
|
|
19010
|
+
});
|
|
18816
19011
|
const installAntigravity = (opts) => {
|
|
18817
19012
|
if (opts.scope !== "project") return {
|
|
18818
19013
|
wrote: [],
|
|
18819
19014
|
skipped: [],
|
|
18820
19015
|
planned: [{
|
|
18821
|
-
path: ".agents/rules/antigravity-
|
|
19016
|
+
path: ".agents/rules/antigravity-raigal-rules.md",
|
|
18822
19017
|
summary: "Antigravity is project-scope only; pass --project"
|
|
18823
19018
|
}]
|
|
18824
19019
|
};
|
|
18825
|
-
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-
|
|
19020
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-raigal-rules.md");
|
|
18826
19021
|
};
|
|
18827
19022
|
const uninstallAntigravity = (opts) => {
|
|
18828
19023
|
if (opts.scope !== "project") return {
|
|
@@ -18831,9 +19026,7 @@ const uninstallAntigravity = (opts) => {
|
|
|
18831
19026
|
};
|
|
18832
19027
|
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
18833
19028
|
};
|
|
18834
|
-
|
|
18835
|
-
//#region src/hooks/io/json-patch.ts
|
|
18836
|
-
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
19029
|
+
const RAIGAL_SENTINEL_KEY = "__raigal";
|
|
18837
19030
|
const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
|
|
18838
19031
|
const groupIsAislop = (group) => {
|
|
18839
19032
|
if (typeof group !== "object" || group === null) return false;
|
|
@@ -18883,6 +19076,7 @@ const resolveClaudePaths = (opts) => {
|
|
|
18883
19076
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
18884
19077
|
return {
|
|
18885
19078
|
settings: path.join(root, "settings.json"),
|
|
19079
|
+
raigalMd: path.join(root, "RAIGAL.md"),
|
|
18886
19080
|
aislopMd: path.join(root, "AISLOP.md"),
|
|
18887
19081
|
claudeMd: path.join(root, "CLAUDE.md")
|
|
18888
19082
|
};
|
|
@@ -18897,7 +19091,7 @@ const buildHookGroup$1 = () => {
|
|
|
18897
19091
|
hooks: [{
|
|
18898
19092
|
type: "command",
|
|
18899
19093
|
command: "raigal hook claude",
|
|
18900
|
-
[
|
|
19094
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
18901
19095
|
v: 1,
|
|
18902
19096
|
managed: true,
|
|
18903
19097
|
hash: sentinelHash(hashBody)
|
|
@@ -18912,7 +19106,7 @@ const buildStopHookGroup = () => {
|
|
|
18912
19106
|
hooks: [{
|
|
18913
19107
|
type: "command",
|
|
18914
19108
|
command: "raigal hook claude --stop",
|
|
18915
|
-
[
|
|
19109
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
18916
19110
|
v: 1,
|
|
18917
19111
|
managed: true,
|
|
18918
19112
|
hash: sentinelHash(hashBody)
|
|
@@ -18931,7 +19125,7 @@ const buildFileChangedHookGroup = () => {
|
|
|
18931
19125
|
hooks: [{
|
|
18932
19126
|
type: "command",
|
|
18933
19127
|
command: "raigal hook claude --on-file-changed",
|
|
18934
|
-
[
|
|
19128
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
18935
19129
|
v: 1,
|
|
18936
19130
|
managed: true,
|
|
18937
19131
|
hash: sentinelHash(hashBody)
|
|
@@ -18957,15 +19151,25 @@ const installClaude = (opts) => {
|
|
|
18957
19151
|
const result = emptyResult();
|
|
18958
19152
|
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
18959
19153
|
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
18960
|
-
const
|
|
18961
|
-
const
|
|
18962
|
-
|
|
19154
|
+
const body = buildRaigalMdBody("claude");
|
|
19155
|
+
const mdHash = sentinelHash(body);
|
|
19156
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, mdHash);
|
|
19157
|
+
applyContent(result, opts, paths.raigalMd, fenced.nextContent, "write RAIGAL.md rules");
|
|
19158
|
+
if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
|
|
19159
|
+
if (!opts.dryRun) try {
|
|
19160
|
+
fs.rmSync(paths.aislopMd, { force: true });
|
|
19161
|
+
} catch {}
|
|
19162
|
+
}
|
|
18963
19163
|
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
18964
|
-
const marker = "@
|
|
18965
|
-
|
|
19164
|
+
const marker = "@RAIGAL.md";
|
|
19165
|
+
const legacyMarker = "@AISLOP.md";
|
|
19166
|
+
if (existingClaudeMd.includes(legacyMarker)) {
|
|
19167
|
+
const migrated = existingClaudeMd.split("\n").map((line) => line.trim() === legacyMarker ? marker : line).join("\n");
|
|
19168
|
+
applyContent(result, opts, paths.claudeMd, migrated, "update @RAIGAL.md reference");
|
|
19169
|
+
} else if (!existingClaudeMd.includes(marker)) {
|
|
18966
19170
|
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
18967
19171
|
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
18968
|
-
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @
|
|
19172
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
|
|
18969
19173
|
} else result.skipped.push(paths.claudeMd);
|
|
18970
19174
|
return result;
|
|
18971
19175
|
};
|
|
@@ -18994,11 +19198,19 @@ const uninstallClaude = (opts) => {
|
|
|
18994
19198
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
18995
19199
|
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
18996
19200
|
} else result.skipped.push(paths.settings);
|
|
18997
|
-
|
|
18998
|
-
|
|
19201
|
+
let removedMd = false;
|
|
19202
|
+
if (readIfExists(paths.raigalMd) != null) {
|
|
19203
|
+
applyRemoval(result, opts, paths.raigalMd, null);
|
|
19204
|
+
removedMd = true;
|
|
19205
|
+
}
|
|
19206
|
+
if (readIfExists(paths.aislopMd) != null) {
|
|
19207
|
+
applyRemoval(result, opts, paths.aislopMd, null);
|
|
19208
|
+
removedMd = true;
|
|
19209
|
+
}
|
|
19210
|
+
if (!removedMd) result.skipped.push(paths.raigalMd);
|
|
18999
19211
|
const claudeMd = readIfExists(paths.claudeMd);
|
|
19000
|
-
if (claudeMd?.includes("@AISLOP.md")) {
|
|
19001
|
-
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19212
|
+
if (claudeMd?.includes("@RAIGAL.md") || claudeMd?.includes("@AISLOP.md")) {
|
|
19213
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@RAIGAL.md" && line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19002
19214
|
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
19003
19215
|
} else result.skipped.push(paths.claudeMd);
|
|
19004
19216
|
return result;
|
|
@@ -19006,7 +19218,10 @@ const uninstallClaude = (opts) => {
|
|
|
19006
19218
|
//#endregion
|
|
19007
19219
|
//#region src/hooks/install/cline.ts
|
|
19008
19220
|
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
19009
|
-
const resolveRooPaths = (opts) => ({
|
|
19221
|
+
const resolveRooPaths = (opts) => ({
|
|
19222
|
+
rules: path.join(opts.cwd, ".roo", "rules", "raigal.md"),
|
|
19223
|
+
legacyRules: path.join(opts.cwd, ".roo", "rules", "aislop.md")
|
|
19224
|
+
});
|
|
19010
19225
|
const installCline = (opts) => {
|
|
19011
19226
|
if (opts.scope !== "project") return {
|
|
19012
19227
|
wrote: [],
|
|
@@ -19017,7 +19232,7 @@ const installCline = (opts) => {
|
|
|
19017
19232
|
}]
|
|
19018
19233
|
};
|
|
19019
19234
|
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
19020
|
-
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/
|
|
19235
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/raigal.md");
|
|
19021
19236
|
return {
|
|
19022
19237
|
wrote: [...cline.wrote, ...roo.wrote],
|
|
19023
19238
|
skipped: [...cline.skipped, ...roo.skipped],
|
|
@@ -19068,7 +19283,8 @@ const resolveCursorPaths = (opts) => {
|
|
|
19068
19283
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
19069
19284
|
return {
|
|
19070
19285
|
hooks: path.join(root, "hooks.json"),
|
|
19071
|
-
rules: path.join(opts.cwd, ".cursor", "rules", "
|
|
19286
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "raigal.mdc"),
|
|
19287
|
+
legacyRules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
19072
19288
|
};
|
|
19073
19289
|
};
|
|
19074
19290
|
const buildHookEntry = () => {
|
|
@@ -19080,7 +19296,7 @@ const buildHookEntry = () => {
|
|
|
19080
19296
|
command: "raigal hook cursor",
|
|
19081
19297
|
type: "command",
|
|
19082
19298
|
timeout: 5e3,
|
|
19083
|
-
[
|
|
19299
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
19084
19300
|
v: 1,
|
|
19085
19301
|
managed: true,
|
|
19086
19302
|
hash: sentinelHash(hashBody)
|
|
@@ -19104,8 +19320,14 @@ const installCursor = (opts) => {
|
|
|
19104
19320
|
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
19105
19321
|
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
19106
19322
|
if (opts.scope === "project") {
|
|
19107
|
-
const
|
|
19108
|
-
|
|
19323
|
+
const body = buildRaigalMdBody("cursor");
|
|
19324
|
+
const rules = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
|
|
19325
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/raigal.mdc");
|
|
19326
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
19327
|
+
if (!opts.dryRun) try {
|
|
19328
|
+
fs.rmSync(paths.legacyRules, { force: true });
|
|
19329
|
+
} catch {}
|
|
19330
|
+
}
|
|
19109
19331
|
}
|
|
19110
19332
|
return result;
|
|
19111
19333
|
};
|
|
@@ -19129,7 +19351,18 @@ const uninstallCursor = (opts) => {
|
|
|
19129
19351
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
19130
19352
|
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19131
19353
|
} else result.skipped.push(paths.hooks);
|
|
19132
|
-
if (opts.scope === "project")
|
|
19354
|
+
if (opts.scope === "project") {
|
|
19355
|
+
let removedRule = false;
|
|
19356
|
+
if (readIfExists(paths.rules) != null) {
|
|
19357
|
+
applyRemoval(result, opts, paths.rules, null);
|
|
19358
|
+
removedRule = true;
|
|
19359
|
+
}
|
|
19360
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
19361
|
+
applyRemoval(result, opts, paths.legacyRules, null);
|
|
19362
|
+
removedRule = true;
|
|
19363
|
+
}
|
|
19364
|
+
if (!removedRule) result.skipped.push(paths.rules);
|
|
19365
|
+
}
|
|
19133
19366
|
return result;
|
|
19134
19367
|
};
|
|
19135
19368
|
//#endregion
|
|
@@ -19138,6 +19371,7 @@ const resolveGeminiPaths = (opts) => {
|
|
|
19138
19371
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
19139
19372
|
return {
|
|
19140
19373
|
settings: path.join(root, "settings.json"),
|
|
19374
|
+
raigalMd: path.join(root, "RAIGAL.md"),
|
|
19141
19375
|
aislopMd: path.join(root, "AISLOP.md"),
|
|
19142
19376
|
geminiMd: path.join(root, "GEMINI.md")
|
|
19143
19377
|
};
|
|
@@ -19154,7 +19388,7 @@ const buildHookGroup = () => {
|
|
|
19154
19388
|
type: "command",
|
|
19155
19389
|
command: "raigal hook gemini",
|
|
19156
19390
|
timeout: 5e3,
|
|
19157
|
-
[
|
|
19391
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
19158
19392
|
v: 1,
|
|
19159
19393
|
managed: true,
|
|
19160
19394
|
hash: sentinelHash(hashBody)
|
|
@@ -19177,14 +19411,25 @@ const installGemini = (opts) => {
|
|
|
19177
19411
|
const result = emptyResult();
|
|
19178
19412
|
const next = renderSettings(readIfExists(paths.settings));
|
|
19179
19413
|
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
19180
|
-
const
|
|
19181
|
-
|
|
19414
|
+
const body = buildRaigalMdBody("gemini");
|
|
19415
|
+
const hash = sentinelHash(body);
|
|
19416
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, hash).nextContent;
|
|
19417
|
+
applyContent(result, opts, paths.raigalMd, fenced, "write RAIGAL.md rules");
|
|
19418
|
+
if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
|
|
19419
|
+
if (!opts.dryRun) try {
|
|
19420
|
+
fs.rmSync(paths.aislopMd, { force: true });
|
|
19421
|
+
} catch {}
|
|
19422
|
+
}
|
|
19182
19423
|
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
19183
|
-
const marker = "@
|
|
19184
|
-
|
|
19424
|
+
const marker = "@RAIGAL.md";
|
|
19425
|
+
const legacyMarker = "@AISLOP.md";
|
|
19426
|
+
if (existingGeminiMd.includes(legacyMarker)) {
|
|
19427
|
+
const migrated = existingGeminiMd.split("\n").map((l) => l.trim() === legacyMarker ? marker : l).join("\n");
|
|
19428
|
+
applyContent(result, opts, paths.geminiMd, migrated, "update @RAIGAL.md reference");
|
|
19429
|
+
} else if (!existingGeminiMd.includes(marker)) {
|
|
19185
19430
|
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
19186
19431
|
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
19187
|
-
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @
|
|
19432
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
|
|
19188
19433
|
} else result.skipped.push(paths.geminiMd);
|
|
19189
19434
|
return result;
|
|
19190
19435
|
};
|
|
@@ -19208,28 +19453,39 @@ const uninstallGemini = (opts) => {
|
|
|
19208
19453
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
19209
19454
|
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
19210
19455
|
} else result.skipped.push(paths.settings);
|
|
19211
|
-
|
|
19212
|
-
|
|
19456
|
+
let removedMd = false;
|
|
19457
|
+
if (readIfExists(paths.raigalMd) != null) {
|
|
19458
|
+
applyRemoval(result, opts, paths.raigalMd, null);
|
|
19459
|
+
removedMd = true;
|
|
19460
|
+
}
|
|
19461
|
+
if (readIfExists(paths.aislopMd) != null) {
|
|
19462
|
+
applyRemoval(result, opts, paths.aislopMd, null);
|
|
19463
|
+
removedMd = true;
|
|
19464
|
+
}
|
|
19465
|
+
if (!removedMd) result.skipped.push(paths.raigalMd);
|
|
19213
19466
|
const geminiMd = readIfExists(paths.geminiMd);
|
|
19214
|
-
if (geminiMd?.includes("@AISLOP.md")) {
|
|
19215
|
-
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19467
|
+
if (geminiMd?.includes("@RAIGAL.md") || geminiMd?.includes("@AISLOP.md")) {
|
|
19468
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@RAIGAL.md" && l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
19216
19469
|
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
19217
19470
|
} else result.skipped.push(paths.geminiMd);
|
|
19218
19471
|
return result;
|
|
19219
19472
|
};
|
|
19220
19473
|
//#endregion
|
|
19221
19474
|
//#region src/hooks/install/kilocode.ts
|
|
19222
|
-
const resolveKilocodePaths = (opts) => ({
|
|
19475
|
+
const resolveKilocodePaths = (opts) => ({
|
|
19476
|
+
rules: path.join(opts.cwd, ".kilocode", "rules", "raigal-rules.md"),
|
|
19477
|
+
legacyRules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md")
|
|
19478
|
+
});
|
|
19223
19479
|
const installKilocode = (opts) => {
|
|
19224
19480
|
if (opts.scope !== "project") return {
|
|
19225
19481
|
wrote: [],
|
|
19226
19482
|
skipped: [],
|
|
19227
19483
|
planned: [{
|
|
19228
|
-
path: ".kilocode/rules/
|
|
19484
|
+
path: ".kilocode/rules/raigal-rules.md",
|
|
19229
19485
|
summary: "Kilo Code is project-scope only; pass --project"
|
|
19230
19486
|
}]
|
|
19231
19487
|
};
|
|
19232
|
-
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/
|
|
19488
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/raigal-rules.md");
|
|
19233
19489
|
};
|
|
19234
19490
|
const uninstallKilocode = (opts) => {
|
|
19235
19491
|
if (opts.scope !== "project") return {
|
|
@@ -19383,18 +19639,24 @@ const paths = {
|
|
|
19383
19639
|
const p = resolveClaudePaths(opts);
|
|
19384
19640
|
return [
|
|
19385
19641
|
p.settings,
|
|
19642
|
+
p.raigalMd,
|
|
19386
19643
|
p.aislopMd,
|
|
19387
19644
|
p.claudeMd
|
|
19388
19645
|
];
|
|
19389
19646
|
},
|
|
19390
19647
|
cursor: (opts) => {
|
|
19391
19648
|
const p = resolveCursorPaths(opts);
|
|
19392
|
-
return opts.scope === "project" ? [
|
|
19649
|
+
return opts.scope === "project" ? [
|
|
19650
|
+
p.hooks,
|
|
19651
|
+
p.rules,
|
|
19652
|
+
...p.legacyRules ? [p.legacyRules] : []
|
|
19653
|
+
] : [p.hooks];
|
|
19393
19654
|
},
|
|
19394
19655
|
gemini: (opts) => {
|
|
19395
19656
|
const p = resolveGeminiPaths(opts);
|
|
19396
19657
|
return [
|
|
19397
19658
|
p.settings,
|
|
19659
|
+
p.raigalMd,
|
|
19398
19660
|
p.aislopMd,
|
|
19399
19661
|
p.geminiMd
|
|
19400
19662
|
];
|
|
@@ -19402,9 +19664,22 @@ const paths = {
|
|
|
19402
19664
|
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
19403
19665
|
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
19404
19666
|
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
19405
|
-
cline: (opts) =>
|
|
19406
|
-
|
|
19407
|
-
|
|
19667
|
+
cline: (opts) => {
|
|
19668
|
+
const roo = resolveRooPaths(opts);
|
|
19669
|
+
return [
|
|
19670
|
+
resolveClinePaths(opts).rules,
|
|
19671
|
+
roo.rules,
|
|
19672
|
+
...roo.legacyRules ? [roo.legacyRules] : []
|
|
19673
|
+
];
|
|
19674
|
+
},
|
|
19675
|
+
kilocode: (opts) => {
|
|
19676
|
+
const k = resolveKilocodePaths(opts);
|
|
19677
|
+
return [k.rules, ...k.legacyRules ? [k.legacyRules] : []];
|
|
19678
|
+
},
|
|
19679
|
+
antigravity: (opts) => {
|
|
19680
|
+
const a = resolveAntigravityPaths(opts);
|
|
19681
|
+
return [a.rules, ...a.legacyRules ? [a.legacyRules] : []];
|
|
19682
|
+
},
|
|
19408
19683
|
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
19409
19684
|
};
|
|
19410
19685
|
const REGISTRY = {
|
|
@@ -19652,22 +19927,10 @@ const printEngineStatus = (result) => {
|
|
|
19652
19927
|
}
|
|
19653
19928
|
};
|
|
19654
19929
|
//#endregion
|
|
19655
|
-
//#region src/commands/scan-hook-nudge.ts
|
|
19656
|
-
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
19657
|
-
const buildHookNudge = (ctx) => {
|
|
19658
|
-
if (!shouldShowHookNudge(ctx)) return null;
|
|
19659
|
-
return [
|
|
19660
|
-
"",
|
|
19661
|
-
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
19662
|
-
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
19663
|
-
""
|
|
19664
|
-
].join("\n");
|
|
19665
|
-
};
|
|
19666
|
-
//#endregion
|
|
19667
19930
|
//#region src/commands/scan-coverage.ts
|
|
19668
19931
|
const coverageReason = (c) => {
|
|
19669
19932
|
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which Raigal does not analyze. No score — it would not reflect this code.`;
|
|
19670
|
-
if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP,
|
|
19933
|
+
if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, C#, C/C++). Nothing to score.";
|
|
19671
19934
|
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
19672
19935
|
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
19673
19936
|
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); Raigal analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
@@ -19868,6 +20131,18 @@ const computeScanExitCode = (opts) => {
|
|
|
19868
20131
|
*/
|
|
19869
20132
|
const computeCiExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
19870
20133
|
//#endregion
|
|
20134
|
+
//#region src/commands/scan-hook-nudge.ts
|
|
20135
|
+
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
20136
|
+
const buildHookNudge = (ctx) => {
|
|
20137
|
+
if (!shouldShowHookNudge(ctx)) return null;
|
|
20138
|
+
return [
|
|
20139
|
+
"",
|
|
20140
|
+
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
20141
|
+
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
20142
|
+
""
|
|
20143
|
+
].join("\n");
|
|
20144
|
+
};
|
|
20145
|
+
//#endregion
|
|
19871
20146
|
//#region src/output/rule-labels.ts
|
|
19872
20147
|
const RULE_LABELS = {
|
|
19873
20148
|
formatting: "Code not formatted",
|
|
@@ -20379,7 +20654,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
20379
20654
|
const projectName = projectInfo.projectName ?? "project";
|
|
20380
20655
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
20381
20656
|
const printedHumanHeader = !machineOutput && showHeader;
|
|
20382
|
-
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, testFiles } = scanScope;
|
|
20657
|
+
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, secretFiles, testFiles } = scanScope;
|
|
20383
20658
|
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
20384
20659
|
const scanCoverage = deriveScanCoverage(projectInfo.coverage, scoreFileCount);
|
|
20385
20660
|
const reportProjectInfo = {
|
|
@@ -20420,6 +20695,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
20420
20695
|
excludePatterns,
|
|
20421
20696
|
testFiles,
|
|
20422
20697
|
projectFiles,
|
|
20698
|
+
secretFiles,
|
|
20423
20699
|
installedTools: projectInfo.installedTools,
|
|
20424
20700
|
config: engineConfig
|
|
20425
20701
|
}, config.engines, machineOutput)).map((result) => ({
|
|
@@ -20486,12 +20762,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
20486
20762
|
scoreFileCount
|
|
20487
20763
|
});
|
|
20488
20764
|
if (options.sarif) {
|
|
20489
|
-
const { buildSarifLog } = await import("./sarif-
|
|
20765
|
+
const { buildSarifLog } = await import("./sarif-CpMse0iL.js");
|
|
20490
20766
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
20491
20767
|
return completion;
|
|
20492
20768
|
}
|
|
20493
20769
|
if (options.json) {
|
|
20494
|
-
const { buildJsonOutput } = await import("./json-
|
|
20770
|
+
const { buildJsonOutput } = await import("./json-B2Kc7_rw.js");
|
|
20495
20771
|
const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
|
|
20496
20772
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
20497
20773
|
return completion;
|
|
@@ -20810,10 +21086,12 @@ const buildInitSuccessRender = (input) => {
|
|
|
20810
21086
|
}, deps);
|
|
20811
21087
|
const writtenCount = input.steps.filter((s) => s.status === "done").length;
|
|
20812
21088
|
const footer = writtenCount === 0 ? "Nothing to write" : `Done · wrote ${writtenCount} file${writtenCount === 1 ? "" : "s"}`;
|
|
20813
|
-
|
|
21089
|
+
const rail = renderRail({
|
|
20814
21090
|
steps: input.steps,
|
|
20815
21091
|
footer
|
|
20816
|
-
}, deps)
|
|
21092
|
+
}, deps);
|
|
21093
|
+
const extraHint = input.hint ? `\n${renderHintLine(input.hint, deps)}` : "";
|
|
21094
|
+
return `${header}${rail}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}${extraHint}`;
|
|
20817
21095
|
};
|
|
20818
21096
|
const ENGINE_CHOICES = [
|
|
20819
21097
|
{
|
|
@@ -20864,7 +21142,45 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
|
|
|
20864
21142
|
relativePath
|
|
20865
21143
|
};
|
|
20866
21144
|
};
|
|
20867
|
-
const
|
|
21145
|
+
const writeGitlabWorkflow = (rootDirectory, enabled) => {
|
|
21146
|
+
const relativePath = DEFAULT_GITLAB_CI_FILE;
|
|
21147
|
+
if (!enabled) return { status: "declined" };
|
|
21148
|
+
const workflowPath = path.join(rootDirectory, DEFAULT_GITLAB_CI_FILE);
|
|
21149
|
+
if (fs.existsSync(workflowPath)) return {
|
|
21150
|
+
status: "skipped-exists",
|
|
21151
|
+
relativePath
|
|
21152
|
+
};
|
|
21153
|
+
fs.writeFileSync(workflowPath, DEFAULT_GITLAB_CI_YAML);
|
|
21154
|
+
return {
|
|
21155
|
+
status: "written",
|
|
21156
|
+
relativePath
|
|
21157
|
+
};
|
|
21158
|
+
};
|
|
21159
|
+
const writeBitbucketWorkflow = (rootDirectory, enabled) => {
|
|
21160
|
+
const relativePath = DEFAULT_BITBUCKET_FILE;
|
|
21161
|
+
if (!enabled) return { status: "declined" };
|
|
21162
|
+
const workflowPath = path.join(rootDirectory, DEFAULT_BITBUCKET_FILE);
|
|
21163
|
+
if (fs.existsSync(workflowPath)) return {
|
|
21164
|
+
status: "skipped-exists",
|
|
21165
|
+
relativePath
|
|
21166
|
+
};
|
|
21167
|
+
fs.writeFileSync(workflowPath, DEFAULT_BITBUCKET_PIPELINES_YAML);
|
|
21168
|
+
return {
|
|
21169
|
+
status: "written",
|
|
21170
|
+
relativePath
|
|
21171
|
+
};
|
|
21172
|
+
};
|
|
21173
|
+
const detectDefaultCiProvider = (directory) => {
|
|
21174
|
+
if (fs.existsSync(path.join(directory, ".gitlab-ci.yml"))) return "gitlab";
|
|
21175
|
+
if (fs.existsSync(path.join(directory, "bitbucket-pipelines.yml"))) return "bitbucket";
|
|
21176
|
+
if (fs.existsSync(path.join(directory, ".github/workflows"))) return "github";
|
|
21177
|
+
const detectedOwner = detectLocalRepoOwner(directory);
|
|
21178
|
+
if (detectedOwner.hasRemote) {
|
|
21179
|
+
if (detectedOwner.isGitHubRemote) return "github";
|
|
21180
|
+
}
|
|
21181
|
+
return "github";
|
|
21182
|
+
};
|
|
21183
|
+
const promptForConfigChoices = async (directory) => {
|
|
20868
21184
|
const enginesSelection = await runCancellable(() => multiselect({
|
|
20869
21185
|
message: "Which engines should run?",
|
|
20870
21186
|
options: ENGINE_CHOICES,
|
|
@@ -20894,15 +21210,26 @@ const promptForConfigChoices = async () => {
|
|
|
20894
21210
|
});
|
|
20895
21211
|
if (isCancel(telemetryChoice)) return null;
|
|
20896
21212
|
const workflowChoice = await select({
|
|
20897
|
-
message: "Add
|
|
20898
|
-
options: [
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20902
|
-
|
|
20903
|
-
|
|
20904
|
-
|
|
20905
|
-
|
|
21213
|
+
message: "Add automated CI quality gate workflow?",
|
|
21214
|
+
options: [
|
|
21215
|
+
{
|
|
21216
|
+
value: "github",
|
|
21217
|
+
label: "GitHub Actions — writes .github/workflows/raigal.yml"
|
|
21218
|
+
},
|
|
21219
|
+
{
|
|
21220
|
+
value: "gitlab",
|
|
21221
|
+
label: "GitLab CI — writes .gitlab-ci.yml"
|
|
21222
|
+
},
|
|
21223
|
+
{
|
|
21224
|
+
value: "bitbucket",
|
|
21225
|
+
label: "Bitbucket Pipelines — writes bitbucket-pipelines.yml"
|
|
21226
|
+
},
|
|
21227
|
+
{
|
|
21228
|
+
value: "none",
|
|
21229
|
+
label: "No, I'll wire CI myself"
|
|
21230
|
+
}
|
|
21231
|
+
],
|
|
21232
|
+
initialValue: detectDefaultCiProvider(directory)
|
|
20906
21233
|
});
|
|
20907
21234
|
if (isCancel(workflowChoice)) return null;
|
|
20908
21235
|
return {
|
|
@@ -20910,7 +21237,8 @@ const promptForConfigChoices = async () => {
|
|
|
20910
21237
|
failBelow: Number(failBelowRaw),
|
|
20911
21238
|
typecheck: DEFAULT_CONFIG.lint.typecheck,
|
|
20912
21239
|
telemetryEnabled: telemetryChoice === "enabled",
|
|
20913
|
-
writeGithubWorkflow: workflowChoice === "
|
|
21240
|
+
writeGithubWorkflow: workflowChoice === "github",
|
|
21241
|
+
ciProvider: workflowChoice
|
|
20914
21242
|
};
|
|
20915
21243
|
};
|
|
20916
21244
|
const strictChoices = () => ({
|
|
@@ -20918,7 +21246,8 @@ const strictChoices = () => ({
|
|
|
20918
21246
|
failBelow: 85,
|
|
20919
21247
|
typecheck: true,
|
|
20920
21248
|
telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
|
|
20921
|
-
writeGithubWorkflow: true
|
|
21249
|
+
writeGithubWorkflow: true,
|
|
21250
|
+
ciProvider: "github"
|
|
20922
21251
|
});
|
|
20923
21252
|
const writeAislopConfig = (configDir, configPath, choices) => {
|
|
20924
21253
|
const selected = new Set(choices.engines);
|
|
@@ -20995,7 +21324,7 @@ const initCommand = async (directory, options = {}) => {
|
|
|
20995
21324
|
return;
|
|
20996
21325
|
}
|
|
20997
21326
|
}
|
|
20998
|
-
const choices = options.strict ? strictChoices() : await promptForConfigChoices();
|
|
21327
|
+
const choices = options.strict ? strictChoices() : await promptForConfigChoices(resolvedDir);
|
|
20999
21328
|
if (!choices) return;
|
|
21000
21329
|
writeAislopConfig(configDir, configPath, choices);
|
|
21001
21330
|
const steps = [{
|
|
@@ -21012,19 +21341,49 @@ const initCommand = async (directory, options = {}) => {
|
|
|
21012
21341
|
status: "skipped",
|
|
21013
21342
|
label: `${CONFIG_DIR}/${RULES_FILE} already exists`
|
|
21014
21343
|
});
|
|
21015
|
-
|
|
21016
|
-
if (
|
|
21017
|
-
|
|
21018
|
-
|
|
21019
|
-
|
|
21020
|
-
|
|
21021
|
-
|
|
21022
|
-
|
|
21023
|
-
|
|
21344
|
+
let ciHint;
|
|
21345
|
+
if (choices.ciProvider === "gitlab") {
|
|
21346
|
+
const gitlabResult = writeGitlabWorkflow(resolvedDir, true);
|
|
21347
|
+
if (gitlabResult.status === "written") {
|
|
21348
|
+
steps.push({
|
|
21349
|
+
status: "done",
|
|
21350
|
+
label: `Wrote ${gitlabResult.relativePath}`
|
|
21351
|
+
});
|
|
21352
|
+
ciHint = "Set RAIGAL_TOKEN in GitLab CI/CD Variables (Settings > CI/CD > Variables).";
|
|
21353
|
+
} else if (gitlabResult.status === "skipped-exists") steps.push({
|
|
21354
|
+
status: "skipped",
|
|
21355
|
+
label: `${gitlabResult.relativePath} already exists`
|
|
21356
|
+
});
|
|
21357
|
+
} else if (choices.ciProvider === "bitbucket") {
|
|
21358
|
+
const bbResult = writeBitbucketWorkflow(resolvedDir, true);
|
|
21359
|
+
if (bbResult.status === "written") {
|
|
21360
|
+
steps.push({
|
|
21361
|
+
status: "done",
|
|
21362
|
+
label: `Wrote ${bbResult.relativePath}`
|
|
21363
|
+
});
|
|
21364
|
+
ciHint = "Set RAIGAL_TOKEN in Bitbucket Repository Variables (Repository settings > Repository variables).";
|
|
21365
|
+
} else if (bbResult.status === "skipped-exists") steps.push({
|
|
21366
|
+
status: "skipped",
|
|
21367
|
+
label: `${bbResult.relativePath} already exists`
|
|
21368
|
+
});
|
|
21369
|
+
} else if (choices.writeGithubWorkflow || choices.ciProvider === "github") {
|
|
21370
|
+
const workflowResult = writeGithubWorkflow(resolvedDir, true);
|
|
21371
|
+
if (workflowResult.status === "written") {
|
|
21372
|
+
steps.push({
|
|
21373
|
+
status: "done",
|
|
21374
|
+
label: `Wrote ${workflowResult.relativePath}`
|
|
21375
|
+
});
|
|
21376
|
+
ciHint = "GitHub Actions zero-token OIDC enabled (permissions: id-token: write).";
|
|
21377
|
+
} else if (workflowResult.status === "skipped-exists") steps.push({
|
|
21378
|
+
status: "skipped",
|
|
21379
|
+
label: `${workflowResult.relativePath} already exists`
|
|
21380
|
+
});
|
|
21381
|
+
}
|
|
21024
21382
|
process.stdout.write(buildInitSuccessRender({
|
|
21025
21383
|
steps,
|
|
21026
21384
|
nextCommand: `${invocation} scan`,
|
|
21027
|
-
includeHeader: false
|
|
21385
|
+
includeHeader: false,
|
|
21386
|
+
hint: ciHint
|
|
21028
21387
|
}));
|
|
21029
21388
|
};
|
|
21030
21389
|
//#endregion
|