@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/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire, isBuiltin } from "node:module";
|
|
3
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import fs, { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
-
import { Command, Option } from "commander";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import { Command, Option } from "commander";
|
|
6
8
|
import { performance } from "node:perf_hooks";
|
|
7
|
-
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
8
9
|
import os, { tmpdir } from "node:os";
|
|
9
10
|
import pc from "picocolors";
|
|
10
11
|
import wcwidth from "wcwidth";
|
|
11
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
13
|
import crypto, { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
|
|
14
14
|
import YAML, { parse } from "yaml";
|
|
@@ -923,7 +923,7 @@ const createLogger = (deps = {}) => {
|
|
|
923
923
|
const log = createLogger();
|
|
924
924
|
//#endregion
|
|
925
925
|
//#region src/version.ts
|
|
926
|
-
const APP_VERSION = "0.2.
|
|
926
|
+
const APP_VERSION = "0.2.7";
|
|
927
927
|
//#endregion
|
|
928
928
|
//#region src/commands/agent-connect.ts
|
|
929
929
|
const commandText = (status) => [status.provider.loginCommand.command, ...status.provider.loginCommand.args].join(" ");
|
|
@@ -1209,7 +1209,7 @@ const AllowedOwner = z.object({
|
|
|
1209
1209
|
login: z.string().max(100)
|
|
1210
1210
|
});
|
|
1211
1211
|
const EntitlementClaims = z.object({
|
|
1212
|
-
iss: z.literal(
|
|
1212
|
+
iss: z.literal("https://app.raigal.dev"),
|
|
1213
1213
|
sub: z.string(),
|
|
1214
1214
|
org_name: z.string(),
|
|
1215
1215
|
plan: z.enum(["trial", "active"]),
|
|
@@ -1266,6 +1266,8 @@ const requestApi = async (endpoint, options = {}) => {
|
|
|
1266
1266
|
};
|
|
1267
1267
|
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
1268
1268
|
if (options.oidcToken) headers["X-GitHub-OIDC"] = options.oidcToken;
|
|
1269
|
+
const orgId = options.orgId || process.env.RAIGAL_ORG_ID;
|
|
1270
|
+
if (orgId) headers["X-Raigal-Org-Id"] = orgId;
|
|
1269
1271
|
let body;
|
|
1270
1272
|
if (options.body !== void 0) {
|
|
1271
1273
|
headers["Content-Type"] = "application/json";
|
|
@@ -1327,11 +1329,12 @@ const pollDeviceToken = async (deviceCode) => {
|
|
|
1327
1329
|
throw err;
|
|
1328
1330
|
}
|
|
1329
1331
|
};
|
|
1330
|
-
const fetchEntitlementFromApi = async (token, oidcToken) => {
|
|
1332
|
+
const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
|
|
1331
1333
|
const data = await requestApi("/v1/entitlement", {
|
|
1332
1334
|
method: "POST",
|
|
1333
1335
|
token: token || void 0,
|
|
1334
|
-
oidcToken
|
|
1336
|
+
oidcToken,
|
|
1337
|
+
orgId
|
|
1335
1338
|
});
|
|
1336
1339
|
return EntitlementResponse.parse(data);
|
|
1337
1340
|
};
|
|
@@ -1353,14 +1356,6 @@ const revokeSessionApi = async (token) => {
|
|
|
1353
1356
|
const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
|
|
1354
1357
|
const keyObjectCache = /* @__PURE__ */ new Map();
|
|
1355
1358
|
const getPublicKey = (kid) => {
|
|
1356
|
-
if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
|
|
1357
|
-
const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
|
|
1358
|
-
try {
|
|
1359
|
-
return createPublicKey(customKey);
|
|
1360
|
-
} catch {
|
|
1361
|
-
return null;
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
1359
|
const cached = keyObjectCache.get(kid);
|
|
1365
1360
|
if (cached) return cached;
|
|
1366
1361
|
const pem = EMBEDDED_PUBLIC_KEYS[kid];
|
|
@@ -1420,9 +1415,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
|
|
|
1420
1415
|
const parsed = EntitlementClaims.safeParse(rawPayload);
|
|
1421
1416
|
if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
|
|
1422
1417
|
const claims = parsed.data;
|
|
1423
|
-
|
|
1424
|
-
if (claims.
|
|
1425
|
-
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");
|
|
1418
|
+
if (claims.iss !== "https://app.raigal.dev") throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
|
|
1419
|
+
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");
|
|
1426
1420
|
const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
1427
1421
|
const grace = options.allowGraceSeconds ?? 0;
|
|
1428
1422
|
if (now > claims.exp + grace) {
|
|
@@ -1581,7 +1575,9 @@ const getEntitlement = async () => {
|
|
|
1581
1575
|
}
|
|
1582
1576
|
} else oidcToken = await fetchGitHubOidcToken();
|
|
1583
1577
|
try {
|
|
1584
|
-
const
|
|
1578
|
+
const tokenToPass = activeToken?.token ?? "";
|
|
1579
|
+
const orgId = process.env.RAIGAL_ORG_ID;
|
|
1580
|
+
const response = await fetchEntitlementFromApi(tokenToPass, oidcToken, orgId);
|
|
1585
1581
|
const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
|
|
1586
1582
|
saveCachedEntitlement(response.entitlement, response.refresh_after);
|
|
1587
1583
|
return {
|
|
@@ -1595,7 +1591,7 @@ const getEntitlement = async () => {
|
|
|
1595
1591
|
};
|
|
1596
1592
|
//#endregion
|
|
1597
1593
|
//#region src/cloud/owner.ts
|
|
1598
|
-
const
|
|
1594
|
+
const GENERIC_GIT_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)[^:/]+(?::(?:\d+\/)?|[:/])|https?:\/\/(?:[^@]+@)?[^/]+\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
|
|
1599
1595
|
const parseGitRemote = (rawUrl) => {
|
|
1600
1596
|
if (!rawUrl) return {
|
|
1601
1597
|
owner: null,
|
|
@@ -1603,18 +1599,19 @@ const parseGitRemote = (rawUrl) => {
|
|
|
1603
1599
|
hasRemote: false,
|
|
1604
1600
|
isGitHubRemote: false
|
|
1605
1601
|
};
|
|
1606
|
-
const match = rawUrl.match(
|
|
1602
|
+
const match = rawUrl.match(GENERIC_GIT_REMOTE_RE);
|
|
1607
1603
|
if (!match || !match[1] || !match[2]) return {
|
|
1608
1604
|
owner: null,
|
|
1609
1605
|
repo: null,
|
|
1610
1606
|
hasRemote: true,
|
|
1611
1607
|
isGitHubRemote: false
|
|
1612
1608
|
};
|
|
1609
|
+
const isGitHub = rawUrl.includes("github.com");
|
|
1613
1610
|
return {
|
|
1614
1611
|
owner: match[1].toLowerCase(),
|
|
1615
1612
|
repo: match[2].toLowerCase(),
|
|
1616
1613
|
hasRemote: true,
|
|
1617
|
-
isGitHubRemote:
|
|
1614
|
+
isGitHubRemote: isGitHub
|
|
1618
1615
|
};
|
|
1619
1616
|
};
|
|
1620
1617
|
const readGitConfigRemote = (dir) => {
|
|
@@ -1679,6 +1676,27 @@ const detectLocalRepoOwner = (directory) => {
|
|
|
1679
1676
|
isGitHubRemote: true
|
|
1680
1677
|
};
|
|
1681
1678
|
}
|
|
1679
|
+
const gitlabRepo = process.env.CI_PROJECT_PATH?.trim();
|
|
1680
|
+
if (gitlabRepo && gitlabRepo.includes("/")) {
|
|
1681
|
+
const parts = gitlabRepo.split("/");
|
|
1682
|
+
const repo = parts.pop() ?? "";
|
|
1683
|
+
return {
|
|
1684
|
+
owner: (parts[0] ?? "").toLowerCase(),
|
|
1685
|
+
repo: repo.toLowerCase(),
|
|
1686
|
+
hasRemote: true,
|
|
1687
|
+
isGitHubRemote: false
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
const bitbucketRepo = (process.env.BITBUCKET_REPO_FULL_NAME || process.env.BITBUCKET_REPO_SLUG)?.trim();
|
|
1691
|
+
if (bitbucketRepo) {
|
|
1692
|
+
const [owner = process.env.BITBUCKET_WORKSPACE || "", repo = ""] = bitbucketRepo.includes("/") ? bitbucketRepo.split("/") : [process.env.BITBUCKET_WORKSPACE || "", bitbucketRepo];
|
|
1693
|
+
return {
|
|
1694
|
+
owner: owner.toLowerCase(),
|
|
1695
|
+
repo: repo.toLowerCase(),
|
|
1696
|
+
hasRemote: true,
|
|
1697
|
+
isGitHubRemote: false
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1682
1700
|
const gitRoot = findGitRoot$1(directory);
|
|
1683
1701
|
if (!gitRoot) return {
|
|
1684
1702
|
owner: null,
|
|
@@ -1690,26 +1708,17 @@ const detectLocalRepoOwner = (directory) => {
|
|
|
1690
1708
|
if (rawUrl !== null) return parseGitRemote(rawUrl);
|
|
1691
1709
|
return parseGitRemote(spawnGitRemote(gitRoot));
|
|
1692
1710
|
};
|
|
1693
|
-
const isNonGitHubCi = () => {
|
|
1694
|
-
const env = process.env;
|
|
1695
|
-
if (!(env.CI === "true" || env.CI === "1")) return false;
|
|
1696
|
-
return Boolean(env.GITLAB_CI || env.BITBUCKET_BUILD_NUMBER || env.CIRCLECI && !env.CIRCLE_PROJECT_REPONAME?.includes("/") || env.BUILDKITE || env.TRAVIS || env.JENKINS_URL);
|
|
1697
|
-
};
|
|
1698
1711
|
const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
|
|
1699
|
-
if (isNonGitHubCi()) return {
|
|
1700
|
-
allowed: false,
|
|
1701
|
-
reason: "Non-GitHub CI environments are unsupported under organization allowlists."
|
|
1702
|
-
};
|
|
1703
1712
|
if (!detected.hasRemote) {
|
|
1704
1713
|
if (tokenKind === "session") return { allowed: true };
|
|
1705
1714
|
return {
|
|
1706
1715
|
allowed: false,
|
|
1707
|
-
reason: "Repository has no
|
|
1716
|
+
reason: "Repository has no origin remote. CI and API key runs require an origin remote matching an allowed organization."
|
|
1708
1717
|
};
|
|
1709
1718
|
}
|
|
1710
|
-
if (!detected.
|
|
1719
|
+
if (!detected.owner) return {
|
|
1711
1720
|
allowed: false,
|
|
1712
|
-
reason: "Repository remote
|
|
1721
|
+
reason: "Repository remote owner could not be determined."
|
|
1713
1722
|
};
|
|
1714
1723
|
const lowerOwner = detected.owner.toLowerCase();
|
|
1715
1724
|
if (!allowedOwners.some((allowed) => allowed.login.toLowerCase() === lowerOwner)) {
|
|
@@ -1717,7 +1726,7 @@ const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
|
|
|
1717
1726
|
return {
|
|
1718
1727
|
allowed: false,
|
|
1719
1728
|
owner: lowerOwner,
|
|
1720
|
-
reason: `
|
|
1729
|
+
reason: `Repository owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
|
|
1721
1730
|
};
|
|
1722
1731
|
}
|
|
1723
1732
|
return {
|
|
@@ -2805,8 +2814,8 @@ const detectGitContext = (directory) => {
|
|
|
2805
2814
|
const eventPayload = readGitHubEventPayload();
|
|
2806
2815
|
const pr = eventPayload?.pull_request;
|
|
2807
2816
|
let headSha = pr?.head?.sha;
|
|
2808
|
-
if (!headSha) headSha = process.env.GITHUB_SHA || execGit(["rev-parse", "HEAD"], directory);
|
|
2809
|
-
let branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME;
|
|
2817
|
+
if (!headSha) headSha = process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || execGit(["rev-parse", "HEAD"], directory);
|
|
2818
|
+
let branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME || process.env.CI_COMMIT_REF_NAME || process.env.BITBUCKET_BRANCH;
|
|
2810
2819
|
if (!branch) {
|
|
2811
2820
|
branch = execGit([
|
|
2812
2821
|
"rev-parse",
|
|
@@ -2815,7 +2824,7 @@ const detectGitContext = (directory) => {
|
|
|
2815
2824
|
], directory);
|
|
2816
2825
|
if (branch === "HEAD") branch = void 0;
|
|
2817
2826
|
}
|
|
2818
|
-
let baseRef = pr?.base?.ref || process.env.GITHUB_BASE_REF;
|
|
2827
|
+
let baseRef = pr?.base?.ref || process.env.GITHUB_BASE_REF || process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
2819
2828
|
if (!baseRef) baseRef = void 0;
|
|
2820
2829
|
let prNumber;
|
|
2821
2830
|
if (typeof pr?.number === "number") prNumber = pr.number;
|
|
@@ -2823,6 +2832,12 @@ const detectGitContext = (directory) => {
|
|
|
2823
2832
|
else if (process.env.RAIGAL_PR_NUMBER || process.env.PR_NUMBER) {
|
|
2824
2833
|
const parsed = Number.parseInt(process.env.RAIGAL_PR_NUMBER || process.env.PR_NUMBER || "", 10);
|
|
2825
2834
|
if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
|
|
2835
|
+
} else if (process.env.CI_MERGE_REQUEST_IID) {
|
|
2836
|
+
const parsed = Number.parseInt(process.env.CI_MERGE_REQUEST_IID, 10);
|
|
2837
|
+
if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
|
|
2838
|
+
} else if (process.env.BITBUCKET_PR_ID) {
|
|
2839
|
+
const parsed = Number.parseInt(process.env.BITBUCKET_PR_ID, 10);
|
|
2840
|
+
if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
|
|
2826
2841
|
} else if (process.env.GITHUB_REF) {
|
|
2827
2842
|
const match = process.env.GITHUB_REF.match(/refs\/pull\/(\d+)(?:\/|$)/);
|
|
2828
2843
|
if (match && match[1]) prNumber = Number.parseInt(match[1], 10);
|
|
@@ -2879,7 +2894,31 @@ const detectCiContext = () => {
|
|
|
2879
2894
|
...runUrl ? { run_url: runUrl } : {}
|
|
2880
2895
|
};
|
|
2881
2896
|
}
|
|
2882
|
-
if (process.env.
|
|
2897
|
+
if (process.env.GITLAB_CI) {
|
|
2898
|
+
const runUrl = process.env.CI_JOB_URL || process.env.CI_PIPELINE_URL;
|
|
2899
|
+
return {
|
|
2900
|
+
provider: "gitlab-ci",
|
|
2901
|
+
...runUrl ? { run_url: runUrl } : {}
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
if (process.env.BITBUCKET_BUILD_NUMBER) {
|
|
2905
|
+
const workspace = process.env.BITBUCKET_WORKSPACE;
|
|
2906
|
+
const repoSlug = process.env.BITBUCKET_REPO_SLUG;
|
|
2907
|
+
const buildNumber = process.env.BITBUCKET_BUILD_NUMBER;
|
|
2908
|
+
const runUrl = workspace && repoSlug && buildNumber ? `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${buildNumber}` : void 0;
|
|
2909
|
+
return {
|
|
2910
|
+
provider: "bitbucket-pipelines",
|
|
2911
|
+
...runUrl ? { run_url: runUrl } : {}
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
if (process.env.CI === "true" || process.env.CI === "1") {
|
|
2915
|
+
const provider = process.env.CIRCLECI && "circleci" || process.env.BUILDKITE && "buildkite" || process.env.TRAVIS && "travis-ci" || "generic-ci";
|
|
2916
|
+
const runUrl = process.env.CIRCLE_BUILD_URL || process.env.BUILDKITE_BUILD_URL || process.env.TRAVIS_BUILD_WEB_URL;
|
|
2917
|
+
return {
|
|
2918
|
+
provider,
|
|
2919
|
+
...runUrl ? { run_url: runUrl } : {}
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2883
2922
|
};
|
|
2884
2923
|
//#endregion
|
|
2885
2924
|
//#region src/cloud/outbox.ts
|
|
@@ -4791,10 +4830,53 @@ jobs:
|
|
|
4791
4830
|
runs-on: ubuntu-latest
|
|
4792
4831
|
steps:
|
|
4793
4832
|
- uses: actions/checkout@v4
|
|
4833
|
+
with:
|
|
4834
|
+
fetch-depth: 0
|
|
4794
4835
|
- uses: actions/setup-node@v4
|
|
4795
4836
|
with:
|
|
4796
4837
|
node-version: 22
|
|
4797
4838
|
- run: npx --yes @methiu/raigal@latest ci .
|
|
4839
|
+
env:
|
|
4840
|
+
RAIGAL_TOKEN: \${{ secrets.RAIGAL_TOKEN }}
|
|
4841
|
+
# RAIGAL_ORG_ID: \${{ vars.RAIGAL_ORG_ID }} # Optional: required if repository owner belongs to multiple Raigal organizations
|
|
4842
|
+
`;
|
|
4843
|
+
const DEFAULT_GITLAB_CI_FILE = ".gitlab-ci.yml";
|
|
4844
|
+
const DEFAULT_GITLAB_CI_YAML = `raigal_quality_gate:
|
|
4845
|
+
image: node:24
|
|
4846
|
+
stage: test
|
|
4847
|
+
script:
|
|
4848
|
+
- 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
|
|
4849
|
+
variables:
|
|
4850
|
+
RAIGAL_TOKEN: $RAIGAL_TOKEN
|
|
4851
|
+
# RAIGAL_ORG_ID: $RAIGAL_ORG_ID # Optional: required if repository owner belongs to multiple Raigal organizations
|
|
4852
|
+
rules:
|
|
4853
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
4854
|
+
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
4855
|
+
`;
|
|
4856
|
+
const DEFAULT_BITBUCKET_FILE = "bitbucket-pipelines.yml";
|
|
4857
|
+
const DEFAULT_BITBUCKET_PIPELINES_YAML = `pipelines:
|
|
4858
|
+
default:
|
|
4859
|
+
- step:
|
|
4860
|
+
name: raigal quality gate
|
|
4861
|
+
image: node:24
|
|
4862
|
+
script:
|
|
4863
|
+
- npx --yes @methiu/raigal@latest ci .
|
|
4864
|
+
secrets:
|
|
4865
|
+
- RAIGAL_TOKEN
|
|
4866
|
+
# - RAIGAL_ORG_ID
|
|
4867
|
+
pull-requests:
|
|
4868
|
+
"**":
|
|
4869
|
+
- step:
|
|
4870
|
+
name: raigal quality gate
|
|
4871
|
+
image: node:24
|
|
4872
|
+
clone:
|
|
4873
|
+
depth: full
|
|
4874
|
+
script:
|
|
4875
|
+
- git fetch origin "$BITBUCKET_PR_DESTINATION_BRANCH"
|
|
4876
|
+
- npx --yes @methiu/raigal@latest ci --changes --base FETCH_HEAD
|
|
4877
|
+
secrets:
|
|
4878
|
+
- RAIGAL_TOKEN
|
|
4879
|
+
# - RAIGAL_ORG_ID
|
|
4798
4880
|
`;
|
|
4799
4881
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
4800
4882
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -8851,7 +8933,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
|
|
|
8851
8933
|
return false;
|
|
8852
8934
|
}
|
|
8853
8935
|
};
|
|
8854
|
-
const
|
|
8936
|
+
const SECRET_CONFIG_EXTENSIONS = [
|
|
8937
|
+
".env",
|
|
8938
|
+
".yaml",
|
|
8939
|
+
".yml",
|
|
8940
|
+
".json",
|
|
8941
|
+
".toml",
|
|
8942
|
+
".ini",
|
|
8943
|
+
".properties",
|
|
8944
|
+
".tfvars"
|
|
8945
|
+
];
|
|
8946
|
+
const LOCKFILE_RE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|packages\.lock\.json|composer\.lock|bun\.lock)$/i;
|
|
8947
|
+
const isDotenvFile = (filePath) => {
|
|
8948
|
+
const base = path.basename(filePath).toLowerCase();
|
|
8949
|
+
return base === ".env" || base.startsWith(".env.");
|
|
8950
|
+
};
|
|
8951
|
+
const hasAllowedSourceExtension = (filePath, extraExtensions) => {
|
|
8952
|
+
const extension = path.extname(filePath);
|
|
8953
|
+
if (SOURCE_EXTENSIONS$1.has(extension)) return true;
|
|
8954
|
+
if (LOCKFILE_RE.test(filePath)) return false;
|
|
8955
|
+
if (extraExtensions.has(".env") && isDotenvFile(filePath)) return true;
|
|
8956
|
+
return extraExtensions.has(extension);
|
|
8957
|
+
};
|
|
8855
8958
|
const isInExcludedDirectory = (filePath, excludedDirectories) => {
|
|
8856
8959
|
const normalized = filePath.toLowerCase();
|
|
8857
8960
|
return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
|
|
@@ -21223,13 +21326,19 @@ const SECRET_PATTERNS = [
|
|
|
21223
21326
|
},
|
|
21224
21327
|
{
|
|
21225
21328
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
21226
|
-
name: "AWS Access Key"
|
|
21329
|
+
name: "AWS Access Key",
|
|
21330
|
+
specific: true
|
|
21227
21331
|
},
|
|
21228
21332
|
{
|
|
21229
|
-
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
21333
|
+
pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
21230
21334
|
name: "AWS Secret Key",
|
|
21231
21335
|
keywordPrefixed: true
|
|
21232
21336
|
},
|
|
21337
|
+
{
|
|
21338
|
+
pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
|
|
21339
|
+
name: "Stripe secret key",
|
|
21340
|
+
specific: true
|
|
21341
|
+
},
|
|
21233
21342
|
{
|
|
21234
21343
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
21235
21344
|
name: "Hardcoded password/secret",
|
|
@@ -21237,11 +21346,13 @@ const SECRET_PATTERNS = [
|
|
|
21237
21346
|
},
|
|
21238
21347
|
{
|
|
21239
21348
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
21240
|
-
name: "Private key"
|
|
21349
|
+
name: "Private key",
|
|
21350
|
+
specific: true
|
|
21241
21351
|
},
|
|
21242
21352
|
{
|
|
21243
21353
|
pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
21244
|
-
name: "JWT token"
|
|
21354
|
+
name: "JWT token",
|
|
21355
|
+
specific: true
|
|
21245
21356
|
},
|
|
21246
21357
|
{
|
|
21247
21358
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
@@ -21250,17 +21361,51 @@ const SECRET_PATTERNS = [
|
|
|
21250
21361
|
},
|
|
21251
21362
|
{
|
|
21252
21363
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
21253
|
-
name: "GitHub token"
|
|
21364
|
+
name: "GitHub token",
|
|
21365
|
+
specific: true
|
|
21254
21366
|
},
|
|
21255
21367
|
{
|
|
21256
21368
|
pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
|
|
21257
|
-
name: "Slack token"
|
|
21369
|
+
name: "Slack token",
|
|
21370
|
+
specific: true
|
|
21258
21371
|
},
|
|
21259
21372
|
{
|
|
21260
21373
|
pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
|
|
21261
|
-
name: "Database connection string with credentials"
|
|
21374
|
+
name: "Database connection string with credentials",
|
|
21375
|
+
specific: true
|
|
21262
21376
|
}
|
|
21263
21377
|
];
|
|
21378
|
+
const FORMAT_PATTERNS = SECRET_PATTERNS.filter((p) => p.specific);
|
|
21379
|
+
const CONFIG_ASSIGNMENT_RE = /([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey)[A-Za-z0-9_.-]*)["']?[ \t]*[:=][ \t]*["']?([^\s"'#,]+)/gi;
|
|
21380
|
+
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;
|
|
21381
|
+
const CONFIG_REFERENCE_VALUE_RE = /^[<{$%]/;
|
|
21382
|
+
const CONFIG_PLACEHOLDER_VALUE_RE = /^(?:your|example|placeholder|redacted|dummy|xxx)/i;
|
|
21383
|
+
const VERSION_VALUE_RE = /^[~^<>=v]*\d+(?:\.\d+)+/;
|
|
21384
|
+
const CONFIG_LITERAL_VALUES = /* @__PURE__ */ new Set([
|
|
21385
|
+
"true",
|
|
21386
|
+
"false",
|
|
21387
|
+
"null",
|
|
21388
|
+
"none",
|
|
21389
|
+
"nil"
|
|
21390
|
+
]);
|
|
21391
|
+
const isConfigFile = (filePath) => isDotenvFile(filePath) || SECRET_CONFIG_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
|
|
21392
|
+
const DOTENV_TEMPLATE_RE = /^\.env\.(?:example|sample|template|dist|defaults)$/i;
|
|
21393
|
+
const isLocalDatabaseUrl = (value) => {
|
|
21394
|
+
try {
|
|
21395
|
+
return LOCAL_DB_HOSTS.has(new URL(value).hostname.toLowerCase());
|
|
21396
|
+
} catch {
|
|
21397
|
+
return false;
|
|
21398
|
+
}
|
|
21399
|
+
};
|
|
21400
|
+
const configAssignmentName = (key) => {
|
|
21401
|
+
const lower = key.toLowerCase();
|
|
21402
|
+
if (/aws[_-]?secret/.test(lower)) return "AWS Secret Key";
|
|
21403
|
+
if (/api[_-]?key|apikey/.test(lower)) return "API key";
|
|
21404
|
+
if (/token/.test(lower) && !/password|passwd|pwd|secret/.test(lower)) return "Authentication token";
|
|
21405
|
+
return "Hardcoded password/secret";
|
|
21406
|
+
};
|
|
21407
|
+
const URL_VALUE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
21408
|
+
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);
|
|
21264
21409
|
const isInsideStringLiteral = (content, matchIndex) => {
|
|
21265
21410
|
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
21266
21411
|
const prefix = content.slice(lineStart, matchIndex);
|
|
@@ -21405,16 +21550,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
|
|
|
21405
21550
|
if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
|
|
21406
21551
|
return false;
|
|
21407
21552
|
};
|
|
21553
|
+
const collectSourceCandidates = (content, relativePath) => {
|
|
21554
|
+
const candidates = [];
|
|
21555
|
+
for (const { pattern, name, keywordPrefixed, specific } of SECRET_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
21556
|
+
const matchedText = match[1] ?? match[0];
|
|
21557
|
+
const lineText = lineForMatch(content, match.index);
|
|
21558
|
+
if (isPlaceholderValue(matchedText)) continue;
|
|
21559
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
|
|
21560
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
21561
|
+
candidates.push({
|
|
21562
|
+
name,
|
|
21563
|
+
start: match.index,
|
|
21564
|
+
end: match.index + match[0].length,
|
|
21565
|
+
specific: Boolean(specific)
|
|
21566
|
+
});
|
|
21567
|
+
}
|
|
21568
|
+
return candidates;
|
|
21569
|
+
};
|
|
21570
|
+
const collectConfigCandidates = (content, relativePath) => {
|
|
21571
|
+
const candidates = [];
|
|
21572
|
+
const template = DOTENV_TEMPLATE_RE.test(path.posix.basename(relativePath));
|
|
21573
|
+
for (const { pattern, name } of FORMAT_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
|
|
21574
|
+
const lineText = lineForMatch(content, match.index);
|
|
21575
|
+
if (isPlaceholderValue(match[0])) continue;
|
|
21576
|
+
if (template && isLocalDatabaseUrl(match[0])) continue;
|
|
21577
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, match[0])) continue;
|
|
21578
|
+
candidates.push({
|
|
21579
|
+
name,
|
|
21580
|
+
start: match.index,
|
|
21581
|
+
end: match.index + match[0].length,
|
|
21582
|
+
specific: true
|
|
21583
|
+
});
|
|
21584
|
+
}
|
|
21585
|
+
for (const match of content.matchAll(new RegExp(CONFIG_ASSIGNMENT_RE.source, CONFIG_ASSIGNMENT_RE.flags))) {
|
|
21586
|
+
const [, key, value] = match;
|
|
21587
|
+
const name = configAssignmentName(key);
|
|
21588
|
+
const lineText = lineForMatch(content, match.index);
|
|
21589
|
+
if (isConfigValueIgnorable(key, value) || isPlaceholderValue(value)) continue;
|
|
21590
|
+
if (shouldSkipSecretFinding(relativePath, name, lineText, value)) continue;
|
|
21591
|
+
candidates.push({
|
|
21592
|
+
name,
|
|
21593
|
+
start: match.index,
|
|
21594
|
+
end: match.index + match[0].length,
|
|
21595
|
+
specific: false
|
|
21596
|
+
});
|
|
21597
|
+
}
|
|
21598
|
+
return candidates;
|
|
21599
|
+
};
|
|
21600
|
+
const dedupeCandidates = (candidates) => {
|
|
21601
|
+
const ordered = [...candidates].sort((a, b) => Number(b.specific) - Number(a.specific) || a.start - b.start);
|
|
21602
|
+
const kept = [];
|
|
21603
|
+
for (const candidate of ordered) {
|
|
21604
|
+
if (kept.some((k) => candidate.start < k.end && k.start < candidate.end)) continue;
|
|
21605
|
+
kept.push(candidate);
|
|
21606
|
+
}
|
|
21607
|
+
return kept.sort((a, b) => a.start - b.start);
|
|
21608
|
+
};
|
|
21609
|
+
const resolveSecretFiles = (context) => {
|
|
21610
|
+
const extras = [...SECRET_CONFIG_EXTENSIONS];
|
|
21611
|
+
return context.secretFiles ? filterExplicitFiles(context.rootDirectory, context.secretFiles, extras) : getSourceFilesWithExtras(context, extras);
|
|
21612
|
+
};
|
|
21408
21613
|
const scanSecrets = async (context) => {
|
|
21409
|
-
const files = getSourceFilesWithExtras(context, [
|
|
21410
|
-
".env",
|
|
21411
|
-
".yaml",
|
|
21412
|
-
".yml",
|
|
21413
|
-
".json",
|
|
21414
|
-
".toml"
|
|
21415
|
-
]);
|
|
21416
21614
|
const diagnostics = [];
|
|
21417
|
-
for (const filePath of
|
|
21615
|
+
for (const filePath of resolveSecretFiles(context)) {
|
|
21418
21616
|
let content;
|
|
21419
21617
|
try {
|
|
21420
21618
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -21423,30 +21621,20 @@ const scanSecrets = async (context) => {
|
|
|
21423
21621
|
}
|
|
21424
21622
|
content = maskComments(content, path.extname(filePath));
|
|
21425
21623
|
const relativePath = relativePosix(context.rootDirectory, filePath);
|
|
21426
|
-
|
|
21427
|
-
|
|
21428
|
-
|
|
21429
|
-
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
|
|
21436
|
-
|
|
21437
|
-
|
|
21438
|
-
|
|
21439
|
-
|
|
21440
|
-
message: `Possible ${name} detected in source code`,
|
|
21441
|
-
help: "Move secrets to environment variables or a secrets manager",
|
|
21442
|
-
line,
|
|
21443
|
-
column: 0,
|
|
21444
|
-
category: "Security",
|
|
21445
|
-
fixable: false,
|
|
21446
|
-
redactSource: true
|
|
21447
|
-
});
|
|
21448
|
-
}
|
|
21449
|
-
}
|
|
21624
|
+
const candidates = isConfigFile(filePath) ? collectConfigCandidates(content, relativePath) : collectSourceCandidates(content, relativePath);
|
|
21625
|
+
for (const { name, start } of dedupeCandidates(candidates)) diagnostics.push({
|
|
21626
|
+
filePath: relativePath,
|
|
21627
|
+
engine: "security",
|
|
21628
|
+
rule: "security/hardcoded-secret",
|
|
21629
|
+
severity: "error",
|
|
21630
|
+
message: `Possible ${name} detected in source code`,
|
|
21631
|
+
help: "Move secrets to environment variables or a secrets manager",
|
|
21632
|
+
line: content.slice(0, start).split("\n").length,
|
|
21633
|
+
column: 0,
|
|
21634
|
+
category: "Security",
|
|
21635
|
+
fixable: false,
|
|
21636
|
+
redactSource: true
|
|
21637
|
+
});
|
|
21450
21638
|
}
|
|
21451
21639
|
return diagnostics;
|
|
21452
21640
|
};
|
|
@@ -22658,13 +22846,21 @@ const runPiHook = async (deps = {}) => {
|
|
|
22658
22846
|
release();
|
|
22659
22847
|
}
|
|
22660
22848
|
};
|
|
22661
|
-
|
|
22849
|
+
//#endregion
|
|
22850
|
+
//#region src/hooks/assets.ts
|
|
22851
|
+
const buildRaigalMdBody = (agent) => {
|
|
22852
|
+
let onEditSection;
|
|
22853
|
+
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.`;
|
|
22854
|
+
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.`;
|
|
22855
|
+
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.`;
|
|
22856
|
+
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.`;
|
|
22857
|
+
return `# Raigal - agent instructions
|
|
22662
22858
|
|
|
22663
22859
|
[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.
|
|
22664
22860
|
|
|
22665
22861
|
## On every edit
|
|
22666
22862
|
|
|
22667
|
-
|
|
22863
|
+
${onEditSection}
|
|
22668
22864
|
|
|
22669
22865
|
## Severity ladder
|
|
22670
22866
|
|
|
@@ -22684,15 +22880,17 @@ A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or Multi
|
|
|
22684
22880
|
- If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
|
|
22685
22881
|
- The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
|
|
22686
22882
|
`;
|
|
22883
|
+
};
|
|
22884
|
+
buildRaigalMdBody("claude");
|
|
22687
22885
|
//#endregion
|
|
22688
22886
|
//#region src/hooks/io/sentinel.ts
|
|
22689
22887
|
const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
|
|
22690
|
-
const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
22691
|
-
const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
|
|
22888
|
+
const BEGIN_RE = /<!--\s*(?:raigal|aislop):begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
|
|
22889
|
+
const END_RE = /<!--\s*(?:raigal|aislop):end\s+v\d+\s*-->/;
|
|
22692
22890
|
const renderFence = (body, hash) => [
|
|
22693
|
-
`<!--
|
|
22891
|
+
`<!-- raigal:begin v1 hash=${hash} -->`,
|
|
22694
22892
|
body.trimEnd(),
|
|
22695
|
-
"<!--
|
|
22893
|
+
"<!-- raigal:end v1 -->"
|
|
22696
22894
|
].join("\n");
|
|
22697
22895
|
const upsertMarkdownFence = (existing, body, hash) => {
|
|
22698
22896
|
const fenced = renderFence(body, hash);
|
|
@@ -22757,11 +22955,20 @@ const applyRemoval = (result, opts, target, nextContent) => {
|
|
|
22757
22955
|
//#region src/hooks/install/rules-only.ts
|
|
22758
22956
|
const installRulesOnly = (opts, paths, summary) => {
|
|
22759
22957
|
const result = emptyResult();
|
|
22760
|
-
const
|
|
22958
|
+
const body = paths.body ?? buildRaigalMdBody();
|
|
22959
|
+
const next = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
|
|
22761
22960
|
applyContent(result, opts, paths.rules, next, summary);
|
|
22961
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
22962
|
+
if (!opts.dryRun) try {
|
|
22963
|
+
fs.rmSync(paths.legacyRules, { force: true });
|
|
22964
|
+
} catch {}
|
|
22965
|
+
}
|
|
22762
22966
|
if (paths.host && paths.marker) {
|
|
22763
22967
|
const host = readIfExists(paths.host) ?? "";
|
|
22764
|
-
if (
|
|
22968
|
+
if (paths.legacyMarker && host.includes(paths.legacyMarker)) {
|
|
22969
|
+
const migrated = host.split("\n").map((l) => l.trim() === paths.legacyMarker ? paths.marker : l).join("\n");
|
|
22970
|
+
applyContent(result, opts, paths.host, migrated, `update ${paths.marker} reference`);
|
|
22971
|
+
} else if (!host.includes(paths.marker)) {
|
|
22765
22972
|
const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
|
|
22766
22973
|
const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
|
|
22767
22974
|
applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
|
|
@@ -22774,30 +22981,41 @@ const uninstallRulesOnly = (opts, paths) => {
|
|
|
22774
22981
|
removed: [],
|
|
22775
22982
|
skipped: []
|
|
22776
22983
|
};
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22984
|
+
let removedRule = false;
|
|
22985
|
+
if (readIfExists(paths.rules) != null) {
|
|
22986
|
+
applyRemoval(result, opts, paths.rules, null);
|
|
22987
|
+
removedRule = true;
|
|
22988
|
+
}
|
|
22989
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
22990
|
+
applyRemoval(result, opts, paths.legacyRules, null);
|
|
22991
|
+
removedRule = true;
|
|
22992
|
+
}
|
|
22993
|
+
if (!removedRule) result.skipped.push(paths.rules);
|
|
22994
|
+
if (paths.host && (paths.marker || paths.legacyMarker)) {
|
|
22780
22995
|
const host = readIfExists(paths.host);
|
|
22781
|
-
if (host
|
|
22782
|
-
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
22996
|
+
if (host && (paths.marker && host.includes(paths.marker) || paths.legacyMarker && host.includes(paths.legacyMarker))) {
|
|
22997
|
+
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker && l.trim() !== paths.legacyMarker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
22783
22998
|
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
22784
|
-
} else result.skipped.push(paths.host);
|
|
22999
|
+
} else if (paths.host) result.skipped.push(paths.host);
|
|
22785
23000
|
}
|
|
22786
23001
|
return result;
|
|
22787
23002
|
};
|
|
22788
23003
|
//#endregion
|
|
22789
23004
|
//#region src/hooks/install/antigravity.ts
|
|
22790
|
-
const resolveAntigravityPaths = (opts) => ({
|
|
23005
|
+
const resolveAntigravityPaths = (opts) => ({
|
|
23006
|
+
rules: path.join(opts.cwd, ".agents", "rules", "antigravity-raigal-rules.md"),
|
|
23007
|
+
legacyRules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md")
|
|
23008
|
+
});
|
|
22791
23009
|
const installAntigravity = (opts) => {
|
|
22792
23010
|
if (opts.scope !== "project") return {
|
|
22793
23011
|
wrote: [],
|
|
22794
23012
|
skipped: [],
|
|
22795
23013
|
planned: [{
|
|
22796
|
-
path: ".agents/rules/antigravity-
|
|
23014
|
+
path: ".agents/rules/antigravity-raigal-rules.md",
|
|
22797
23015
|
summary: "Antigravity is project-scope only; pass --project"
|
|
22798
23016
|
}]
|
|
22799
23017
|
};
|
|
22800
|
-
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-
|
|
23018
|
+
return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-raigal-rules.md");
|
|
22801
23019
|
};
|
|
22802
23020
|
const uninstallAntigravity = (opts) => {
|
|
22803
23021
|
if (opts.scope !== "project") return {
|
|
@@ -22806,9 +23024,7 @@ const uninstallAntigravity = (opts) => {
|
|
|
22806
23024
|
};
|
|
22807
23025
|
return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
|
|
22808
23026
|
};
|
|
22809
|
-
|
|
22810
|
-
//#region src/hooks/io/json-patch.ts
|
|
22811
|
-
const AISLOP_SENTINEL_KEY = "__aislop";
|
|
23027
|
+
const RAIGAL_SENTINEL_KEY = "__raigal";
|
|
22812
23028
|
const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
|
|
22813
23029
|
const groupIsAislop = (group) => {
|
|
22814
23030
|
if (typeof group !== "object" || group === null) return false;
|
|
@@ -22858,6 +23074,7 @@ const resolveClaudePaths = (opts) => {
|
|
|
22858
23074
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
|
|
22859
23075
|
return {
|
|
22860
23076
|
settings: path.join(root, "settings.json"),
|
|
23077
|
+
raigalMd: path.join(root, "RAIGAL.md"),
|
|
22861
23078
|
aislopMd: path.join(root, "AISLOP.md"),
|
|
22862
23079
|
claudeMd: path.join(root, "CLAUDE.md")
|
|
22863
23080
|
};
|
|
@@ -22872,7 +23089,7 @@ const buildHookGroup$1 = () => {
|
|
|
22872
23089
|
hooks: [{
|
|
22873
23090
|
type: "command",
|
|
22874
23091
|
command: "raigal hook claude",
|
|
22875
|
-
[
|
|
23092
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
22876
23093
|
v: 1,
|
|
22877
23094
|
managed: true,
|
|
22878
23095
|
hash: sentinelHash(hashBody)
|
|
@@ -22887,7 +23104,7 @@ const buildStopHookGroup = () => {
|
|
|
22887
23104
|
hooks: [{
|
|
22888
23105
|
type: "command",
|
|
22889
23106
|
command: "raigal hook claude --stop",
|
|
22890
|
-
[
|
|
23107
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
22891
23108
|
v: 1,
|
|
22892
23109
|
managed: true,
|
|
22893
23110
|
hash: sentinelHash(hashBody)
|
|
@@ -22906,7 +23123,7 @@ const buildFileChangedHookGroup = () => {
|
|
|
22906
23123
|
hooks: [{
|
|
22907
23124
|
type: "command",
|
|
22908
23125
|
command: "raigal hook claude --on-file-changed",
|
|
22909
|
-
[
|
|
23126
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
22910
23127
|
v: 1,
|
|
22911
23128
|
managed: true,
|
|
22912
23129
|
hash: sentinelHash(hashBody)
|
|
@@ -22932,15 +23149,25 @@ const installClaude = (opts) => {
|
|
|
22932
23149
|
const result = emptyResult();
|
|
22933
23150
|
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
22934
23151
|
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
22935
|
-
const
|
|
22936
|
-
const
|
|
22937
|
-
|
|
23152
|
+
const body = buildRaigalMdBody("claude");
|
|
23153
|
+
const mdHash = sentinelHash(body);
|
|
23154
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, mdHash);
|
|
23155
|
+
applyContent(result, opts, paths.raigalMd, fenced.nextContent, "write RAIGAL.md rules");
|
|
23156
|
+
if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
|
|
23157
|
+
if (!opts.dryRun) try {
|
|
23158
|
+
fs.rmSync(paths.aislopMd, { force: true });
|
|
23159
|
+
} catch {}
|
|
23160
|
+
}
|
|
22938
23161
|
const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
|
|
22939
|
-
const marker = "@
|
|
22940
|
-
|
|
23162
|
+
const marker = "@RAIGAL.md";
|
|
23163
|
+
const legacyMarker = "@AISLOP.md";
|
|
23164
|
+
if (existingClaudeMd.includes(legacyMarker)) {
|
|
23165
|
+
const migrated = existingClaudeMd.split("\n").map((line) => line.trim() === legacyMarker ? marker : line).join("\n");
|
|
23166
|
+
applyContent(result, opts, paths.claudeMd, migrated, "update @RAIGAL.md reference");
|
|
23167
|
+
} else if (!existingClaudeMd.includes(marker)) {
|
|
22941
23168
|
const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
|
|
22942
23169
|
const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
|
|
22943
|
-
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @
|
|
23170
|
+
applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
|
|
22944
23171
|
} else result.skipped.push(paths.claudeMd);
|
|
22945
23172
|
return result;
|
|
22946
23173
|
};
|
|
@@ -22969,11 +23196,19 @@ const uninstallClaude = (opts) => {
|
|
|
22969
23196
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
22970
23197
|
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
22971
23198
|
} else result.skipped.push(paths.settings);
|
|
22972
|
-
|
|
22973
|
-
|
|
23199
|
+
let removedMd = false;
|
|
23200
|
+
if (readIfExists(paths.raigalMd) != null) {
|
|
23201
|
+
applyRemoval(result, opts, paths.raigalMd, null);
|
|
23202
|
+
removedMd = true;
|
|
23203
|
+
}
|
|
23204
|
+
if (readIfExists(paths.aislopMd) != null) {
|
|
23205
|
+
applyRemoval(result, opts, paths.aislopMd, null);
|
|
23206
|
+
removedMd = true;
|
|
23207
|
+
}
|
|
23208
|
+
if (!removedMd) result.skipped.push(paths.raigalMd);
|
|
22974
23209
|
const claudeMd = readIfExists(paths.claudeMd);
|
|
22975
|
-
if (claudeMd?.includes("@AISLOP.md")) {
|
|
22976
|
-
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
23210
|
+
if (claudeMd?.includes("@RAIGAL.md") || claudeMd?.includes("@AISLOP.md")) {
|
|
23211
|
+
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@RAIGAL.md" && line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
22977
23212
|
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
22978
23213
|
} else result.skipped.push(paths.claudeMd);
|
|
22979
23214
|
return result;
|
|
@@ -22981,7 +23216,10 @@ const uninstallClaude = (opts) => {
|
|
|
22981
23216
|
//#endregion
|
|
22982
23217
|
//#region src/hooks/install/cline.ts
|
|
22983
23218
|
const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
|
|
22984
|
-
const resolveRooPaths = (opts) => ({
|
|
23219
|
+
const resolveRooPaths = (opts) => ({
|
|
23220
|
+
rules: path.join(opts.cwd, ".roo", "rules", "raigal.md"),
|
|
23221
|
+
legacyRules: path.join(opts.cwd, ".roo", "rules", "aislop.md")
|
|
23222
|
+
});
|
|
22985
23223
|
const installCline = (opts) => {
|
|
22986
23224
|
if (opts.scope !== "project") return {
|
|
22987
23225
|
wrote: [],
|
|
@@ -22992,7 +23230,7 @@ const installCline = (opts) => {
|
|
|
22992
23230
|
}]
|
|
22993
23231
|
};
|
|
22994
23232
|
const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
|
|
22995
|
-
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/
|
|
23233
|
+
const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/raigal.md");
|
|
22996
23234
|
return {
|
|
22997
23235
|
wrote: [...cline.wrote, ...roo.wrote],
|
|
22998
23236
|
skipped: [...cline.skipped, ...roo.skipped],
|
|
@@ -23043,7 +23281,8 @@ const resolveCursorPaths = (opts) => {
|
|
|
23043
23281
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
|
|
23044
23282
|
return {
|
|
23045
23283
|
hooks: path.join(root, "hooks.json"),
|
|
23046
|
-
rules: path.join(opts.cwd, ".cursor", "rules", "
|
|
23284
|
+
rules: path.join(opts.cwd, ".cursor", "rules", "raigal.mdc"),
|
|
23285
|
+
legacyRules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
|
|
23047
23286
|
};
|
|
23048
23287
|
};
|
|
23049
23288
|
const buildHookEntry = () => {
|
|
@@ -23055,7 +23294,7 @@ const buildHookEntry = () => {
|
|
|
23055
23294
|
command: "raigal hook cursor",
|
|
23056
23295
|
type: "command",
|
|
23057
23296
|
timeout: 5e3,
|
|
23058
|
-
[
|
|
23297
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
23059
23298
|
v: 1,
|
|
23060
23299
|
managed: true,
|
|
23061
23300
|
hash: sentinelHash(hashBody)
|
|
@@ -23079,8 +23318,14 @@ const installCursor = (opts) => {
|
|
|
23079
23318
|
const nextHooks = renderHooksJson(readIfExists(paths.hooks));
|
|
23080
23319
|
applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
|
|
23081
23320
|
if (opts.scope === "project") {
|
|
23082
|
-
const
|
|
23083
|
-
|
|
23321
|
+
const body = buildRaigalMdBody("cursor");
|
|
23322
|
+
const rules = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
|
|
23323
|
+
applyContent(result, opts, paths.rules, rules, "write .cursor/rules/raigal.mdc");
|
|
23324
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
23325
|
+
if (!opts.dryRun) try {
|
|
23326
|
+
fs.rmSync(paths.legacyRules, { force: true });
|
|
23327
|
+
} catch {}
|
|
23328
|
+
}
|
|
23084
23329
|
}
|
|
23085
23330
|
return result;
|
|
23086
23331
|
};
|
|
@@ -23104,7 +23349,18 @@ const uninstallCursor = (opts) => {
|
|
|
23104
23349
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
|
|
23105
23350
|
else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
23106
23351
|
} else result.skipped.push(paths.hooks);
|
|
23107
|
-
if (opts.scope === "project")
|
|
23352
|
+
if (opts.scope === "project") {
|
|
23353
|
+
let removedRule = false;
|
|
23354
|
+
if (readIfExists(paths.rules) != null) {
|
|
23355
|
+
applyRemoval(result, opts, paths.rules, null);
|
|
23356
|
+
removedRule = true;
|
|
23357
|
+
}
|
|
23358
|
+
if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
|
|
23359
|
+
applyRemoval(result, opts, paths.legacyRules, null);
|
|
23360
|
+
removedRule = true;
|
|
23361
|
+
}
|
|
23362
|
+
if (!removedRule) result.skipped.push(paths.rules);
|
|
23363
|
+
}
|
|
23108
23364
|
return result;
|
|
23109
23365
|
};
|
|
23110
23366
|
//#endregion
|
|
@@ -23113,6 +23369,7 @@ const resolveGeminiPaths = (opts) => {
|
|
|
23113
23369
|
const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
|
|
23114
23370
|
return {
|
|
23115
23371
|
settings: path.join(root, "settings.json"),
|
|
23372
|
+
raigalMd: path.join(root, "RAIGAL.md"),
|
|
23116
23373
|
aislopMd: path.join(root, "AISLOP.md"),
|
|
23117
23374
|
geminiMd: path.join(root, "GEMINI.md")
|
|
23118
23375
|
};
|
|
@@ -23129,7 +23386,7 @@ const buildHookGroup = () => {
|
|
|
23129
23386
|
type: "command",
|
|
23130
23387
|
command: "raigal hook gemini",
|
|
23131
23388
|
timeout: 5e3,
|
|
23132
|
-
[
|
|
23389
|
+
[RAIGAL_SENTINEL_KEY]: {
|
|
23133
23390
|
v: 1,
|
|
23134
23391
|
managed: true,
|
|
23135
23392
|
hash: sentinelHash(hashBody)
|
|
@@ -23152,14 +23409,25 @@ const installGemini = (opts) => {
|
|
|
23152
23409
|
const result = emptyResult();
|
|
23153
23410
|
const next = renderSettings(readIfExists(paths.settings));
|
|
23154
23411
|
applyContent(result, opts, paths.settings, next, "register AfterTool hook");
|
|
23155
|
-
const
|
|
23156
|
-
|
|
23412
|
+
const body = buildRaigalMdBody("gemini");
|
|
23413
|
+
const hash = sentinelHash(body);
|
|
23414
|
+
const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, hash).nextContent;
|
|
23415
|
+
applyContent(result, opts, paths.raigalMd, fenced, "write RAIGAL.md rules");
|
|
23416
|
+
if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
|
|
23417
|
+
if (!opts.dryRun) try {
|
|
23418
|
+
fs.rmSync(paths.aislopMd, { force: true });
|
|
23419
|
+
} catch {}
|
|
23420
|
+
}
|
|
23157
23421
|
const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
|
|
23158
|
-
const marker = "@
|
|
23159
|
-
|
|
23422
|
+
const marker = "@RAIGAL.md";
|
|
23423
|
+
const legacyMarker = "@AISLOP.md";
|
|
23424
|
+
if (existingGeminiMd.includes(legacyMarker)) {
|
|
23425
|
+
const migrated = existingGeminiMd.split("\n").map((l) => l.trim() === legacyMarker ? marker : l).join("\n");
|
|
23426
|
+
applyContent(result, opts, paths.geminiMd, migrated, "update @RAIGAL.md reference");
|
|
23427
|
+
} else if (!existingGeminiMd.includes(marker)) {
|
|
23160
23428
|
const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
|
|
23161
23429
|
const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
|
|
23162
|
-
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @
|
|
23430
|
+
applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
|
|
23163
23431
|
} else result.skipped.push(paths.geminiMd);
|
|
23164
23432
|
return result;
|
|
23165
23433
|
};
|
|
@@ -23183,28 +23451,39 @@ const uninstallGemini = (opts) => {
|
|
|
23183
23451
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
23184
23452
|
else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
|
|
23185
23453
|
} else result.skipped.push(paths.settings);
|
|
23186
|
-
|
|
23187
|
-
|
|
23454
|
+
let removedMd = false;
|
|
23455
|
+
if (readIfExists(paths.raigalMd) != null) {
|
|
23456
|
+
applyRemoval(result, opts, paths.raigalMd, null);
|
|
23457
|
+
removedMd = true;
|
|
23458
|
+
}
|
|
23459
|
+
if (readIfExists(paths.aislopMd) != null) {
|
|
23460
|
+
applyRemoval(result, opts, paths.aislopMd, null);
|
|
23461
|
+
removedMd = true;
|
|
23462
|
+
}
|
|
23463
|
+
if (!removedMd) result.skipped.push(paths.raigalMd);
|
|
23188
23464
|
const geminiMd = readIfExists(paths.geminiMd);
|
|
23189
|
-
if (geminiMd?.includes("@AISLOP.md")) {
|
|
23190
|
-
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
23465
|
+
if (geminiMd?.includes("@RAIGAL.md") || geminiMd?.includes("@AISLOP.md")) {
|
|
23466
|
+
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@RAIGAL.md" && l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
23191
23467
|
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
23192
23468
|
} else result.skipped.push(paths.geminiMd);
|
|
23193
23469
|
return result;
|
|
23194
23470
|
};
|
|
23195
23471
|
//#endregion
|
|
23196
23472
|
//#region src/hooks/install/kilocode.ts
|
|
23197
|
-
const resolveKilocodePaths = (opts) => ({
|
|
23473
|
+
const resolveKilocodePaths = (opts) => ({
|
|
23474
|
+
rules: path.join(opts.cwd, ".kilocode", "rules", "raigal-rules.md"),
|
|
23475
|
+
legacyRules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md")
|
|
23476
|
+
});
|
|
23198
23477
|
const installKilocode = (opts) => {
|
|
23199
23478
|
if (opts.scope !== "project") return {
|
|
23200
23479
|
wrote: [],
|
|
23201
23480
|
skipped: [],
|
|
23202
23481
|
planned: [{
|
|
23203
|
-
path: ".kilocode/rules/
|
|
23482
|
+
path: ".kilocode/rules/raigal-rules.md",
|
|
23204
23483
|
summary: "Kilo Code is project-scope only; pass --project"
|
|
23205
23484
|
}]
|
|
23206
23485
|
};
|
|
23207
|
-
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/
|
|
23486
|
+
return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/raigal-rules.md");
|
|
23208
23487
|
};
|
|
23209
23488
|
const uninstallKilocode = (opts) => {
|
|
23210
23489
|
if (opts.scope !== "project") return {
|
|
@@ -23358,18 +23637,24 @@ const paths = {
|
|
|
23358
23637
|
const p = resolveClaudePaths(opts);
|
|
23359
23638
|
return [
|
|
23360
23639
|
p.settings,
|
|
23640
|
+
p.raigalMd,
|
|
23361
23641
|
p.aislopMd,
|
|
23362
23642
|
p.claudeMd
|
|
23363
23643
|
];
|
|
23364
23644
|
},
|
|
23365
23645
|
cursor: (opts) => {
|
|
23366
23646
|
const p = resolveCursorPaths(opts);
|
|
23367
|
-
return opts.scope === "project" ? [
|
|
23647
|
+
return opts.scope === "project" ? [
|
|
23648
|
+
p.hooks,
|
|
23649
|
+
p.rules,
|
|
23650
|
+
...p.legacyRules ? [p.legacyRules] : []
|
|
23651
|
+
] : [p.hooks];
|
|
23368
23652
|
},
|
|
23369
23653
|
gemini: (opts) => {
|
|
23370
23654
|
const p = resolveGeminiPaths(opts);
|
|
23371
23655
|
return [
|
|
23372
23656
|
p.settings,
|
|
23657
|
+
p.raigalMd,
|
|
23373
23658
|
p.aislopMd,
|
|
23374
23659
|
p.geminiMd
|
|
23375
23660
|
];
|
|
@@ -23377,9 +23662,22 @@ const paths = {
|
|
|
23377
23662
|
pi: (opts) => [resolvePiPaths(opts).extension],
|
|
23378
23663
|
codex: (opts) => [resolveCodexPaths(opts).rules],
|
|
23379
23664
|
windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
|
|
23380
|
-
cline: (opts) =>
|
|
23381
|
-
|
|
23382
|
-
|
|
23665
|
+
cline: (opts) => {
|
|
23666
|
+
const roo = resolveRooPaths(opts);
|
|
23667
|
+
return [
|
|
23668
|
+
resolveClinePaths(opts).rules,
|
|
23669
|
+
roo.rules,
|
|
23670
|
+
...roo.legacyRules ? [roo.legacyRules] : []
|
|
23671
|
+
];
|
|
23672
|
+
},
|
|
23673
|
+
kilocode: (opts) => {
|
|
23674
|
+
const k = resolveKilocodePaths(opts);
|
|
23675
|
+
return [k.rules, ...k.legacyRules ? [k.legacyRules] : []];
|
|
23676
|
+
},
|
|
23677
|
+
antigravity: (opts) => {
|
|
23678
|
+
const a = resolveAntigravityPaths(opts);
|
|
23679
|
+
return [a.rules, ...a.legacyRules ? [a.legacyRules] : []];
|
|
23680
|
+
},
|
|
23383
23681
|
copilot: (opts) => [resolveCopilotPaths(opts).rules]
|
|
23384
23682
|
};
|
|
23385
23683
|
const REGISTRY = {
|
|
@@ -25030,7 +25328,7 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
|
|
|
25030
25328
|
if (!process.stderr.isTTY) return;
|
|
25031
25329
|
const cachePath = resolveUpdateCachePath();
|
|
25032
25330
|
const cache = readCache(cachePath);
|
|
25033
|
-
if (cache && isOutdated("0.2.
|
|
25331
|
+
if (cache && isOutdated("0.2.7", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
|
|
25034
25332
|
if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
|
|
25035
25333
|
const latest = await fetchLatestVersion$1();
|
|
25036
25334
|
if (latest) writeCache(cachePath, {
|
|
@@ -25304,6 +25602,10 @@ const COMMAND_REFERENCE = [
|
|
|
25304
25602
|
summary: "Stop a running background agent session",
|
|
25305
25603
|
flags: ["--root <directory>", "--force"]
|
|
25306
25604
|
},
|
|
25605
|
+
{
|
|
25606
|
+
command: "raigal mcp",
|
|
25607
|
+
summary: "Run the Model Context Protocol (MCP) server over stdio"
|
|
25608
|
+
},
|
|
25307
25609
|
{
|
|
25308
25610
|
command: "raigal ci [directory]",
|
|
25309
25611
|
summary: "Run the CI quality gate with thresholded exit codes",
|
|
@@ -25483,6 +25785,7 @@ const commandGroupLabel = (command) => {
|
|
|
25483
25785
|
if (command.startsWith("raigal scan")) return "Core Workflow";
|
|
25484
25786
|
if (command.startsWith("raigal fix")) return "Core Workflow";
|
|
25485
25787
|
if (command.startsWith("raigal ci")) return "Core Workflow";
|
|
25788
|
+
if (command.startsWith("raigal mcp")) return "Local Agent";
|
|
25486
25789
|
if (command.startsWith("raigal hook")) return "Hooks";
|
|
25487
25790
|
if (command === "raigal hooks") return "Hooks";
|
|
25488
25791
|
if (command.startsWith("raigal install")) return "Hooks";
|
|
@@ -25550,7 +25853,7 @@ const renderCommandGroup = (group, commandWidth) => {
|
|
|
25550
25853
|
return lines;
|
|
25551
25854
|
};
|
|
25552
25855
|
const renderCommandReference = (input = {}) => {
|
|
25553
|
-
const version = input.version ?? "0.2.
|
|
25856
|
+
const version = input.version ?? "0.2.7";
|
|
25554
25857
|
const commandWidth = Math.max(...COMMAND_REFERENCE.map((item) => item.command.length));
|
|
25555
25858
|
const lines = [renderHeader({
|
|
25556
25859
|
version,
|
|
@@ -25773,7 +26076,7 @@ const renderHelpDetails = () => [
|
|
|
25773
26076
|
].map((line) => highlightRaigal(line, theme)).join("\n");
|
|
25774
26077
|
const renderHome = (input = {}) => {
|
|
25775
26078
|
let out = renderHeader({
|
|
25776
|
-
version: input.version ?? "0.2.
|
|
26079
|
+
version: input.version ?? "0.2.7",
|
|
25777
26080
|
command: "--bare",
|
|
25778
26081
|
context: []
|
|
25779
26082
|
});
|
|
@@ -26444,12 +26747,13 @@ const baseRefExists = (cwd, ref) => {
|
|
|
26444
26747
|
return !result.error && result.status === 0;
|
|
26445
26748
|
};
|
|
26446
26749
|
const getChangedFiles = (cwd, base) => {
|
|
26750
|
+
const trimmedBase = base?.trim();
|
|
26447
26751
|
const diff = spawnSync("git", [
|
|
26448
26752
|
"diff",
|
|
26449
26753
|
"--no-color",
|
|
26450
26754
|
"--name-only",
|
|
26451
26755
|
"--diff-filter=ACMR",
|
|
26452
|
-
|
|
26756
|
+
trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
|
|
26453
26757
|
], {
|
|
26454
26758
|
cwd,
|
|
26455
26759
|
encoding: "utf-8",
|
|
@@ -26501,13 +26805,14 @@ const listUntrackedFiles = (cwd) => {
|
|
|
26501
26805
|
return untracked.stdout.split("\n").filter((line) => line.length > 0);
|
|
26502
26806
|
};
|
|
26503
26807
|
const getChangedLineMap = (cwd, base) => {
|
|
26808
|
+
const trimmedBase = base?.trim();
|
|
26504
26809
|
const diff = spawnSync("git", [
|
|
26505
26810
|
"diff",
|
|
26506
26811
|
"--no-color",
|
|
26507
26812
|
"--no-ext-diff",
|
|
26508
26813
|
"-U0",
|
|
26509
26814
|
"--diff-filter=ACMR",
|
|
26510
|
-
|
|
26815
|
+
trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
|
|
26511
26816
|
], {
|
|
26512
26817
|
cwd,
|
|
26513
26818
|
encoding: "utf-8",
|
|
@@ -26518,22 +26823,10 @@ const getChangedLineMap = (cwd, base) => {
|
|
|
26518
26823
|
return map;
|
|
26519
26824
|
};
|
|
26520
26825
|
//#endregion
|
|
26521
|
-
//#region src/commands/scan-hook-nudge.ts
|
|
26522
|
-
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
26523
|
-
const buildHookNudge = (ctx) => {
|
|
26524
|
-
if (!shouldShowHookNudge(ctx)) return null;
|
|
26525
|
-
return [
|
|
26526
|
-
"",
|
|
26527
|
-
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
26528
|
-
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
26529
|
-
""
|
|
26530
|
-
].join("\n");
|
|
26531
|
-
};
|
|
26532
|
-
//#endregion
|
|
26533
26826
|
//#region src/commands/scan-coverage.ts
|
|
26534
26827
|
const coverageReason = (c) => {
|
|
26535
26828
|
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.`;
|
|
26536
|
-
if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP,
|
|
26829
|
+
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.";
|
|
26537
26830
|
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
26538
26831
|
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
26539
26832
|
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); Raigal analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
@@ -26772,20 +27065,36 @@ const collectScanFileScope = (request) => {
|
|
|
26772
27065
|
projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...declarations])],
|
|
26773
27066
|
scoreFileCount: projectSourceFiles.length + projectTestFiles.length,
|
|
26774
27067
|
scopeLabel: candidates.label,
|
|
27068
|
+
secretFiles: filterEnumeratedProjectFiles(request.rootDirectory, candidates.files, [...SECRET_CONFIG_EXTENSIONS], request.excludePatterns, request.includePatterns),
|
|
26775
27069
|
testFiles
|
|
26776
27070
|
};
|
|
26777
27071
|
};
|
|
26778
27072
|
//#endregion
|
|
27073
|
+
//#region src/commands/scan-hook-nudge.ts
|
|
27074
|
+
const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
|
|
27075
|
+
const buildHookNudge = (ctx) => {
|
|
27076
|
+
if (!shouldShowHookNudge(ctx)) return null;
|
|
27077
|
+
return [
|
|
27078
|
+
"",
|
|
27079
|
+
"Next: install the per-edit hook so your coding agent fixes slop before it lands.",
|
|
27080
|
+
ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
|
|
27081
|
+
""
|
|
27082
|
+
].join("\n");
|
|
27083
|
+
};
|
|
27084
|
+
//#endregion
|
|
26779
27085
|
//#region src/commands/scan-options.ts
|
|
26780
27086
|
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
26781
27087
|
const isHistoryComparableScan = (options) => !options.staged && !options.changes && options.command !== "ci";
|
|
26782
27088
|
const isFullProjectScan = (options) => isHistoryComparableScan(options) && !options.include?.length && !options.exclude?.length;
|
|
26783
27089
|
const resolveScanScopeMode = (options) => {
|
|
26784
27090
|
if (options.staged) return { kind: "staged" };
|
|
26785
|
-
if (options.changes)
|
|
26786
|
-
|
|
26787
|
-
base
|
|
26788
|
-
|
|
27091
|
+
if (options.changes) {
|
|
27092
|
+
const base = options.base?.trim();
|
|
27093
|
+
return base ? {
|
|
27094
|
+
kind: "changes",
|
|
27095
|
+
base
|
|
27096
|
+
} : { kind: "changes" };
|
|
27097
|
+
}
|
|
26789
27098
|
return { kind: "full" };
|
|
26790
27099
|
};
|
|
26791
27100
|
//#endregion
|
|
@@ -27144,7 +27453,8 @@ const buildScanRender = (input) => {
|
|
|
27144
27453
|
const scanTargetError = (resolvedDir, options) => {
|
|
27145
27454
|
if (!fs.existsSync(resolvedDir)) return `Path does not exist: ${resolvedDir}`;
|
|
27146
27455
|
if (!fs.statSync(resolvedDir).isDirectory()) return `Not a directory: ${resolvedDir}`;
|
|
27147
|
-
|
|
27456
|
+
const base = options.base?.trim();
|
|
27457
|
+
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}\`).`;
|
|
27148
27458
|
return null;
|
|
27149
27459
|
};
|
|
27150
27460
|
//#endregion
|
|
@@ -27216,7 +27526,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
27216
27526
|
const projectName = projectInfo.projectName ?? "project";
|
|
27217
27527
|
const language = projectInfo.languages[0] ?? "unknown";
|
|
27218
27528
|
const printedHumanHeader = !machineOutput && showHeader;
|
|
27219
|
-
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, testFiles } = scanScope;
|
|
27529
|
+
const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, secretFiles, testFiles } = scanScope;
|
|
27220
27530
|
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
27221
27531
|
const scanCoverage = deriveScanCoverage(projectInfo.coverage, scoreFileCount);
|
|
27222
27532
|
const reportProjectInfo = {
|
|
@@ -27257,6 +27567,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
|
|
|
27257
27567
|
excludePatterns,
|
|
27258
27568
|
testFiles,
|
|
27259
27569
|
projectFiles,
|
|
27570
|
+
secretFiles,
|
|
27260
27571
|
installedTools: projectInfo.installedTools,
|
|
27261
27572
|
config: engineConfig
|
|
27262
27573
|
}, config.engines, machineOutput)).map((result) => ({
|
|
@@ -29900,10 +30211,12 @@ const buildInitSuccessRender = (input) => {
|
|
|
29900
30211
|
}, deps);
|
|
29901
30212
|
const writtenCount = input.steps.filter((s) => s.status === "done").length;
|
|
29902
30213
|
const footer = writtenCount === 0 ? "Nothing to write" : `Done · wrote ${writtenCount} file${writtenCount === 1 ? "" : "s"}`;
|
|
29903
|
-
|
|
30214
|
+
const rail = renderRail({
|
|
29904
30215
|
steps: input.steps,
|
|
29905
30216
|
footer
|
|
29906
|
-
}, deps)
|
|
30217
|
+
}, deps);
|
|
30218
|
+
const extraHint = input.hint ? `\n${renderHintLine(input.hint, deps)}` : "";
|
|
30219
|
+
return `${header}${rail}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}${extraHint}`;
|
|
29907
30220
|
};
|
|
29908
30221
|
const ENGINE_CHOICES = [
|
|
29909
30222
|
{
|
|
@@ -29954,7 +30267,45 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
|
|
|
29954
30267
|
relativePath
|
|
29955
30268
|
};
|
|
29956
30269
|
};
|
|
29957
|
-
const
|
|
30270
|
+
const writeGitlabWorkflow = (rootDirectory, enabled) => {
|
|
30271
|
+
const relativePath = DEFAULT_GITLAB_CI_FILE;
|
|
30272
|
+
if (!enabled) return { status: "declined" };
|
|
30273
|
+
const workflowPath = path.join(rootDirectory, DEFAULT_GITLAB_CI_FILE);
|
|
30274
|
+
if (fs.existsSync(workflowPath)) return {
|
|
30275
|
+
status: "skipped-exists",
|
|
30276
|
+
relativePath
|
|
30277
|
+
};
|
|
30278
|
+
fs.writeFileSync(workflowPath, DEFAULT_GITLAB_CI_YAML);
|
|
30279
|
+
return {
|
|
30280
|
+
status: "written",
|
|
30281
|
+
relativePath
|
|
30282
|
+
};
|
|
30283
|
+
};
|
|
30284
|
+
const writeBitbucketWorkflow = (rootDirectory, enabled) => {
|
|
30285
|
+
const relativePath = DEFAULT_BITBUCKET_FILE;
|
|
30286
|
+
if (!enabled) return { status: "declined" };
|
|
30287
|
+
const workflowPath = path.join(rootDirectory, DEFAULT_BITBUCKET_FILE);
|
|
30288
|
+
if (fs.existsSync(workflowPath)) return {
|
|
30289
|
+
status: "skipped-exists",
|
|
30290
|
+
relativePath
|
|
30291
|
+
};
|
|
30292
|
+
fs.writeFileSync(workflowPath, DEFAULT_BITBUCKET_PIPELINES_YAML);
|
|
30293
|
+
return {
|
|
30294
|
+
status: "written",
|
|
30295
|
+
relativePath
|
|
30296
|
+
};
|
|
30297
|
+
};
|
|
30298
|
+
const detectDefaultCiProvider = (directory) => {
|
|
30299
|
+
if (fs.existsSync(path.join(directory, ".gitlab-ci.yml"))) return "gitlab";
|
|
30300
|
+
if (fs.existsSync(path.join(directory, "bitbucket-pipelines.yml"))) return "bitbucket";
|
|
30301
|
+
if (fs.existsSync(path.join(directory, ".github/workflows"))) return "github";
|
|
30302
|
+
const detectedOwner = detectLocalRepoOwner(directory);
|
|
30303
|
+
if (detectedOwner.hasRemote) {
|
|
30304
|
+
if (detectedOwner.isGitHubRemote) return "github";
|
|
30305
|
+
}
|
|
30306
|
+
return "github";
|
|
30307
|
+
};
|
|
30308
|
+
const promptForConfigChoices = async (directory) => {
|
|
29958
30309
|
const enginesSelection = await runCancellable(() => multiselect({
|
|
29959
30310
|
message: "Which engines should run?",
|
|
29960
30311
|
options: ENGINE_CHOICES,
|
|
@@ -29984,15 +30335,26 @@ const promptForConfigChoices = async () => {
|
|
|
29984
30335
|
});
|
|
29985
30336
|
if (isCancel(telemetryChoice)) return null;
|
|
29986
30337
|
const workflowChoice = await select({
|
|
29987
|
-
message: "Add
|
|
29988
|
-
options: [
|
|
29989
|
-
|
|
29990
|
-
|
|
29991
|
-
|
|
29992
|
-
|
|
29993
|
-
|
|
29994
|
-
|
|
29995
|
-
|
|
30338
|
+
message: "Add automated CI quality gate workflow?",
|
|
30339
|
+
options: [
|
|
30340
|
+
{
|
|
30341
|
+
value: "github",
|
|
30342
|
+
label: "GitHub Actions — writes .github/workflows/raigal.yml"
|
|
30343
|
+
},
|
|
30344
|
+
{
|
|
30345
|
+
value: "gitlab",
|
|
30346
|
+
label: "GitLab CI — writes .gitlab-ci.yml"
|
|
30347
|
+
},
|
|
30348
|
+
{
|
|
30349
|
+
value: "bitbucket",
|
|
30350
|
+
label: "Bitbucket Pipelines — writes bitbucket-pipelines.yml"
|
|
30351
|
+
},
|
|
30352
|
+
{
|
|
30353
|
+
value: "none",
|
|
30354
|
+
label: "No, I'll wire CI myself"
|
|
30355
|
+
}
|
|
30356
|
+
],
|
|
30357
|
+
initialValue: detectDefaultCiProvider(directory)
|
|
29996
30358
|
});
|
|
29997
30359
|
if (isCancel(workflowChoice)) return null;
|
|
29998
30360
|
return {
|
|
@@ -30000,7 +30362,8 @@ const promptForConfigChoices = async () => {
|
|
|
30000
30362
|
failBelow: Number(failBelowRaw),
|
|
30001
30363
|
typecheck: DEFAULT_CONFIG.lint.typecheck,
|
|
30002
30364
|
telemetryEnabled: telemetryChoice === "enabled",
|
|
30003
|
-
writeGithubWorkflow: workflowChoice === "
|
|
30365
|
+
writeGithubWorkflow: workflowChoice === "github",
|
|
30366
|
+
ciProvider: workflowChoice
|
|
30004
30367
|
};
|
|
30005
30368
|
};
|
|
30006
30369
|
const strictChoices = () => ({
|
|
@@ -30008,7 +30371,8 @@ const strictChoices = () => ({
|
|
|
30008
30371
|
failBelow: 85,
|
|
30009
30372
|
typecheck: true,
|
|
30010
30373
|
telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
|
|
30011
|
-
writeGithubWorkflow: true
|
|
30374
|
+
writeGithubWorkflow: true,
|
|
30375
|
+
ciProvider: "github"
|
|
30012
30376
|
});
|
|
30013
30377
|
const writeAislopConfig = (configDir, configPath, choices) => {
|
|
30014
30378
|
const selected = new Set(choices.engines);
|
|
@@ -30085,7 +30449,7 @@ const initCommand = async (directory, options = {}) => {
|
|
|
30085
30449
|
return;
|
|
30086
30450
|
}
|
|
30087
30451
|
}
|
|
30088
|
-
const choices = options.strict ? strictChoices() : await promptForConfigChoices();
|
|
30452
|
+
const choices = options.strict ? strictChoices() : await promptForConfigChoices(resolvedDir);
|
|
30089
30453
|
if (!choices) return;
|
|
30090
30454
|
writeAislopConfig(configDir, configPath, choices);
|
|
30091
30455
|
const steps = [{
|
|
@@ -30102,19 +30466,49 @@ const initCommand = async (directory, options = {}) => {
|
|
|
30102
30466
|
status: "skipped",
|
|
30103
30467
|
label: `${CONFIG_DIR}/${RULES_FILE} already exists`
|
|
30104
30468
|
});
|
|
30105
|
-
|
|
30106
|
-
if (
|
|
30107
|
-
|
|
30108
|
-
|
|
30109
|
-
|
|
30110
|
-
|
|
30111
|
-
|
|
30112
|
-
|
|
30113
|
-
|
|
30469
|
+
let ciHint;
|
|
30470
|
+
if (choices.ciProvider === "gitlab") {
|
|
30471
|
+
const gitlabResult = writeGitlabWorkflow(resolvedDir, true);
|
|
30472
|
+
if (gitlabResult.status === "written") {
|
|
30473
|
+
steps.push({
|
|
30474
|
+
status: "done",
|
|
30475
|
+
label: `Wrote ${gitlabResult.relativePath}`
|
|
30476
|
+
});
|
|
30477
|
+
ciHint = "Set RAIGAL_TOKEN in GitLab CI/CD Variables (Settings > CI/CD > Variables).";
|
|
30478
|
+
} else if (gitlabResult.status === "skipped-exists") steps.push({
|
|
30479
|
+
status: "skipped",
|
|
30480
|
+
label: `${gitlabResult.relativePath} already exists`
|
|
30481
|
+
});
|
|
30482
|
+
} else if (choices.ciProvider === "bitbucket") {
|
|
30483
|
+
const bbResult = writeBitbucketWorkflow(resolvedDir, true);
|
|
30484
|
+
if (bbResult.status === "written") {
|
|
30485
|
+
steps.push({
|
|
30486
|
+
status: "done",
|
|
30487
|
+
label: `Wrote ${bbResult.relativePath}`
|
|
30488
|
+
});
|
|
30489
|
+
ciHint = "Set RAIGAL_TOKEN in Bitbucket Repository Variables (Repository settings > Repository variables).";
|
|
30490
|
+
} else if (bbResult.status === "skipped-exists") steps.push({
|
|
30491
|
+
status: "skipped",
|
|
30492
|
+
label: `${bbResult.relativePath} already exists`
|
|
30493
|
+
});
|
|
30494
|
+
} else if (choices.writeGithubWorkflow || choices.ciProvider === "github") {
|
|
30495
|
+
const workflowResult = writeGithubWorkflow(resolvedDir, true);
|
|
30496
|
+
if (workflowResult.status === "written") {
|
|
30497
|
+
steps.push({
|
|
30498
|
+
status: "done",
|
|
30499
|
+
label: `Wrote ${workflowResult.relativePath}`
|
|
30500
|
+
});
|
|
30501
|
+
ciHint = "GitHub Actions zero-token OIDC enabled (permissions: id-token: write).";
|
|
30502
|
+
} else if (workflowResult.status === "skipped-exists") steps.push({
|
|
30503
|
+
status: "skipped",
|
|
30504
|
+
label: `${workflowResult.relativePath} already exists`
|
|
30505
|
+
});
|
|
30506
|
+
}
|
|
30114
30507
|
process.stdout.write(buildInitSuccessRender({
|
|
30115
30508
|
steps,
|
|
30116
30509
|
nextCommand: `${invocation} scan`,
|
|
30117
|
-
includeHeader: false
|
|
30510
|
+
includeHeader: false,
|
|
30511
|
+
hint: ciHint
|
|
30118
30512
|
}));
|
|
30119
30513
|
};
|
|
30120
30514
|
//#endregion
|
|
@@ -30446,6 +30840,15 @@ program.command("doctor [directory]").description("Check toolchain coverage for
|
|
|
30446
30840
|
return { exitCode: 0 };
|
|
30447
30841
|
});
|
|
30448
30842
|
});
|
|
30843
|
+
program.command("mcp").description("Run the Model Context Protocol (MCP) server over stdio").action(async () => {
|
|
30844
|
+
const mcpPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "mcp.js");
|
|
30845
|
+
spawn(process.execPath, [mcpPath], {
|
|
30846
|
+
stdio: "inherit",
|
|
30847
|
+
env: process.env
|
|
30848
|
+
}).on("exit", (code) => {
|
|
30849
|
+
process.exit(code ?? 0);
|
|
30850
|
+
});
|
|
30851
|
+
});
|
|
30449
30852
|
const ciProgram = program.command("ci [directory]").description("Run the quality gate for CI");
|
|
30450
30853
|
for (const [flag, description] of [
|
|
30451
30854
|
["--changes", "only gate files changed vs --base (or HEAD)"],
|