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