@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/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.5";
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(process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev"),
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
- const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
1424
- if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
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 response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
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 GITHUB_REMOTE_RE$1 = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
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(GITHUB_REMOTE_RE$1);
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: true
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 GitHub remote. CI and API key runs require an origin remote matching an allowed organization."
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.isGitHubRemote || !detected.owner) return {
1719
+ if (!detected.owner) return {
1711
1720
  allowed: false,
1712
- reason: "Repository remote is not hosted on GitHub or could not be determined."
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: `GitHub owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
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.CI === "true" || process.env.CI === "1") return { provider: process.env.GITLAB_CI && "gitlab-ci" || process.env.BITBUCKET_BUILD_NUMBER && "bitbucket-pipelines" || process.env.CIRCLECI && "circleci" || process.env.BUILDKITE && "buildkite" || process.env.TRAVIS && "travis-ci" || "generic-ci" };
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 hasAllowedSourceExtension = (filePath, extraExtensions) => SOURCE_EXTENSIONS$1.has(path.extname(filePath)) || extraExtensions.has(path.extname(filePath));
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 files) {
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
- for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
21427
- const regex = new RegExp(pattern.source, pattern.flags);
21428
- for (const match of content.matchAll(regex)) {
21429
- const matchedText = match[1] ?? match[0];
21430
- const lineText = lineForMatch(content, match.index);
21431
- if (isPlaceholderValue(matchedText)) continue;
21432
- if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
21433
- if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
21434
- const line = content.slice(0, match.index).split("\n").length;
21435
- diagnostics.push({
21436
- filePath: relativePath,
21437
- engine: "security",
21438
- rule: "security/hardcoded-secret",
21439
- severity: "error",
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
- const AISLOP_MD_BODY = `# Raigal - agent instructions
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
- 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.
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
- `<!-- aislop:begin v1 hash=${hash} -->`,
22891
+ `<!-- raigal:begin v1 hash=${hash} -->`,
22694
22892
  body.trimEnd(),
22695
- "<!-- aislop:end v1 -->"
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 next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
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 (!host.includes(paths.marker)) {
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
- if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
22778
- else result.skipped.push(paths.rules);
22779
- if (paths.host && paths.marker) {
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?.includes(paths.marker)) {
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) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
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-aislop-rules.md",
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-aislop-rules.md");
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
- //#endregion
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
- [AISLOP_SENTINEL_KEY]: {
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
- [AISLOP_SENTINEL_KEY]: {
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
- [AISLOP_SENTINEL_KEY]: {
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 mdHash = sentinelHash(AISLOP_MD_BODY);
22936
- const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
22937
- applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
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 = "@AISLOP.md";
22940
- if (!existingClaudeMd.includes(marker)) {
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 @AISLOP.md reference");
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
- if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
22973
- else result.skipped.push(paths.aislopMd);
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) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
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/aislop.md");
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", "aislop.mdc")
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
- [AISLOP_SENTINEL_KEY]: {
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 rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23083
- applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
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") applyRemoval(result, opts, paths.rules, null);
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
- [AISLOP_SENTINEL_KEY]: {
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 fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23156
- applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
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 = "@AISLOP.md";
23159
- if (!existingGeminiMd.includes(marker)) {
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 @AISLOP.md reference");
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
- if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
23187
- else result.skipped.push(paths.aislopMd);
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) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
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/aislop-rules.md",
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/aislop-rules.md");
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" ? [p.hooks, p.rules] : [p.hooks];
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) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
23381
- kilocode: (opts) => [resolveKilocodePaths(opts).rules],
23382
- antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
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.5", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
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.5";
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.5",
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
- base ?? "HEAD"
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
- base ?? "HEAD"
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, Java). Nothing to score.";
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) return options.base ? {
26786
- kind: "changes",
26787
- base: options.base
26788
- } : { kind: "changes" };
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
- if (options.changes && options.base && !baseRefExists(resolvedDir, options.base)) return `Could not resolve base ref "${options.base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${options.base}\`).`;
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
- return `${header}${renderRail({
30214
+ const rail = renderRail({
29904
30215
  steps: input.steps,
29905
30216
  footer
29906
- }, deps)}\n${renderHintLine(`Try ${input.nextCommand}`, 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 promptForConfigChoices = async () => {
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 a GitHub Actions workflow to run Raigal on every push and PR?",
29988
- options: [{
29989
- value: "yes",
29990
- label: "Yes — writes .github/workflows/raigal.yml"
29991
- }, {
29992
- value: "no",
29993
- label: "No, I'll wire CI myself"
29994
- }],
29995
- initialValue: "yes"
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 === "yes"
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
- const workflowResult = writeGithubWorkflow(resolvedDir, choices.writeGithubWorkflow);
30106
- if (workflowResult.status === "written") steps.push({
30107
- status: "done",
30108
- label: `Wrote ${workflowResult.relativePath}`
30109
- });
30110
- else if (workflowResult.status === "skipped-exists") steps.push({
30111
- status: "skipped",
30112
- label: `${workflowResult.relativePath} already exists`
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)"],