@methiu/raigal 0.2.4 → 0.2.6

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-CukuIVX4.js";
1
+ import { t as APP_VERSION } from "./version-DtOeK-2T.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.
@@ -2431,12 +2474,13 @@ const baseRefExists = (cwd, ref) => {
2431
2474
  return !result.error && result.status === 0;
2432
2475
  };
2433
2476
  const getChangedFiles = (cwd, base) => {
2477
+ const trimmedBase = base?.trim();
2434
2478
  const diff = spawnSync("git", [
2435
2479
  "diff",
2436
2480
  "--no-color",
2437
2481
  "--name-only",
2438
2482
  "--diff-filter=ACMR",
2439
- base ?? "HEAD"
2483
+ trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
2440
2484
  ], {
2441
2485
  cwd,
2442
2486
  encoding: "utf-8",
@@ -2488,13 +2532,14 @@ const listUntrackedFiles = (cwd) => {
2488
2532
  return untracked.stdout.split("\n").filter((line) => line.length > 0);
2489
2533
  };
2490
2534
  const getChangedLineMap = (cwd, base) => {
2535
+ const trimmedBase = base?.trim();
2491
2536
  const diff = spawnSync("git", [
2492
2537
  "diff",
2493
2538
  "--no-color",
2494
2539
  "--no-ext-diff",
2495
2540
  "-U0",
2496
2541
  "--diff-filter=ACMR",
2497
- base ?? "HEAD"
2542
+ trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
2498
2543
  ], {
2499
2544
  cwd,
2500
2545
  encoding: "utf-8",
@@ -16323,6 +16368,8 @@ const requestApi = async (endpoint, options = {}) => {
16323
16368
  };
16324
16369
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
16325
16370
  if (options.oidcToken) headers["X-GitHub-OIDC"] = options.oidcToken;
16371
+ const orgId = options.orgId || process.env.RAIGAL_ORG_ID;
16372
+ if (orgId) headers["X-Raigal-Org-Id"] = orgId;
16326
16373
  let body;
16327
16374
  if (options.body !== void 0) {
16328
16375
  headers["Content-Type"] = "application/json";
@@ -16354,11 +16401,12 @@ const requestApi = async (endpoint, options = {}) => {
16354
16401
  }
16355
16402
  return json;
16356
16403
  };
16357
- const fetchEntitlementFromApi = async (token, oidcToken) => {
16404
+ const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
16358
16405
  const data = await requestApi("/v1/entitlement", {
16359
16406
  method: "POST",
16360
16407
  token: token || void 0,
16361
- oidcToken
16408
+ oidcToken,
16409
+ orgId
16362
16410
  });
16363
16411
  return EntitlementResponse.parse(data);
16364
16412
  };
@@ -16440,7 +16488,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
16440
16488
  const claims = parsed.data;
16441
16489
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
16442
16490
  if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
16443
- if (!isVersionAtLeast("0.2.4", 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");
16491
+ if (!isVersionAtLeast("0.2.6", 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
16492
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
16445
16493
  const grace = options.allowGraceSeconds ?? 0;
16446
16494
  if (now > claims.exp + grace) {
@@ -16599,7 +16647,9 @@ const getEntitlement = async () => {
16599
16647
  }
16600
16648
  } else oidcToken = await fetchGitHubOidcToken();
16601
16649
  try {
16602
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
16650
+ const tokenToPass = activeToken?.token ?? "";
16651
+ const orgId = process.env.RAIGAL_ORG_ID;
16652
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken, orgId);
16603
16653
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
16604
16654
  saveCachedEntitlement(response.entitlement, response.refresh_after);
16605
16655
  return {
@@ -16613,7 +16663,7 @@ const getEntitlement = async () => {
16613
16663
  };
16614
16664
  //#endregion
16615
16665
  //#region src/cloud/owner.ts
16616
- const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
16666
+ const GENERIC_GIT_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)[^:/]+(?::(?:\d+\/)?|[:/])|https?:\/\/(?:[^@]+@)?[^/]+\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
16617
16667
  const parseGitRemote = (rawUrl) => {
16618
16668
  if (!rawUrl) return {
16619
16669
  owner: null,
@@ -16621,18 +16671,19 @@ const parseGitRemote = (rawUrl) => {
16621
16671
  hasRemote: false,
16622
16672
  isGitHubRemote: false
16623
16673
  };
16624
- const match = rawUrl.match(GITHUB_REMOTE_RE);
16674
+ const match = rawUrl.match(GENERIC_GIT_REMOTE_RE);
16625
16675
  if (!match || !match[1] || !match[2]) return {
16626
16676
  owner: null,
16627
16677
  repo: null,
16628
16678
  hasRemote: true,
16629
16679
  isGitHubRemote: false
16630
16680
  };
16681
+ const isGitHub = rawUrl.includes("github.com");
16631
16682
  return {
16632
16683
  owner: match[1].toLowerCase(),
16633
16684
  repo: match[2].toLowerCase(),
16634
16685
  hasRemote: true,
16635
- isGitHubRemote: true
16686
+ isGitHubRemote: isGitHub
16636
16687
  };
16637
16688
  };
16638
16689
  const readGitConfigRemote = (dir) => {
@@ -16697,6 +16748,27 @@ const detectLocalRepoOwner = (directory) => {
16697
16748
  isGitHubRemote: true
16698
16749
  };
16699
16750
  }
16751
+ const gitlabRepo = process.env.CI_PROJECT_PATH?.trim();
16752
+ if (gitlabRepo && gitlabRepo.includes("/")) {
16753
+ const parts = gitlabRepo.split("/");
16754
+ const repo = parts.pop() ?? "";
16755
+ return {
16756
+ owner: (parts[0] ?? "").toLowerCase(),
16757
+ repo: repo.toLowerCase(),
16758
+ hasRemote: true,
16759
+ isGitHubRemote: false
16760
+ };
16761
+ }
16762
+ const bitbucketRepo = (process.env.BITBUCKET_REPO_FULL_NAME || process.env.BITBUCKET_REPO_SLUG)?.trim();
16763
+ if (bitbucketRepo) {
16764
+ const [owner = process.env.BITBUCKET_WORKSPACE || "", repo = ""] = bitbucketRepo.includes("/") ? bitbucketRepo.split("/") : [process.env.BITBUCKET_WORKSPACE || "", bitbucketRepo];
16765
+ return {
16766
+ owner: owner.toLowerCase(),
16767
+ repo: repo.toLowerCase(),
16768
+ hasRemote: true,
16769
+ isGitHubRemote: false
16770
+ };
16771
+ }
16700
16772
  const gitRoot = findGitRoot(directory);
16701
16773
  if (!gitRoot) return {
16702
16774
  owner: null,
@@ -16708,26 +16780,17 @@ const detectLocalRepoOwner = (directory) => {
16708
16780
  if (rawUrl !== null) return parseGitRemote(rawUrl);
16709
16781
  return parseGitRemote(spawnGitRemote(gitRoot));
16710
16782
  };
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
16783
  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
16784
  if (!detected.hasRemote) {
16722
16785
  if (tokenKind === "session") return { allowed: true };
16723
16786
  return {
16724
16787
  allowed: false,
16725
- reason: "Repository has no GitHub remote. CI and API key runs require an origin remote matching an allowed organization."
16788
+ reason: "Repository has no origin remote. CI and API key runs require an origin remote matching an allowed organization."
16726
16789
  };
16727
16790
  }
16728
- if (!detected.isGitHubRemote || !detected.owner) return {
16791
+ if (!detected.owner) return {
16729
16792
  allowed: false,
16730
- reason: "Repository remote is not hosted on GitHub or could not be determined."
16793
+ reason: "Repository remote owner could not be determined."
16731
16794
  };
16732
16795
  const lowerOwner = detected.owner.toLowerCase();
16733
16796
  if (!allowedOwners.some((allowed) => allowed.login.toLowerCase() === lowerOwner)) {
@@ -16735,7 +16798,7 @@ const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
16735
16798
  return {
16736
16799
  allowed: false,
16737
16800
  owner: lowerOwner,
16738
- reason: `GitHub owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
16801
+ reason: `Repository owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
16739
16802
  };
16740
16803
  }
16741
16804
  return {
@@ -17990,10 +18053,13 @@ const isHistoryComparableScan = (options) => !options.staged && !options.changes
17990
18053
  const isFullProjectScan = (options) => isHistoryComparableScan(options) && !options.include?.length && !options.exclude?.length;
17991
18054
  const resolveScanScopeMode = (options) => {
17992
18055
  if (options.staged) return { kind: "staged" };
17993
- if (options.changes) return options.base ? {
17994
- kind: "changes",
17995
- base: options.base
17996
- } : { kind: "changes" };
18056
+ if (options.changes) {
18057
+ const base = options.base?.trim();
18058
+ return base ? {
18059
+ kind: "changes",
18060
+ base
18061
+ } : { kind: "changes" };
18062
+ }
17997
18063
  return { kind: "full" };
17998
18064
  };
17999
18065
  //#endregion
@@ -18001,7 +18067,8 @@ const resolveScanScopeMode = (options) => {
18001
18067
  const scanTargetError = (resolvedDir, options) => {
18002
18068
  if (!fs.existsSync(resolvedDir)) return `Path does not exist: ${resolvedDir}`;
18003
18069
  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}\`).`;
18070
+ const base = options.base?.trim();
18071
+ 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
18072
  return null;
18006
18073
  };
18007
18074
  //#endregion
@@ -18656,13 +18723,21 @@ const recordFullScanActivity = (history, allowPilotInvitation, telemetryConfig)
18656
18723
  appendHistory(history);
18657
18724
  return allowPilotInvitation && !isHistoryDisabled() && !isTelemetryDisabled(telemetryConfig) && recordCompletedFullScan();
18658
18725
  };
18659
- const AISLOP_MD_BODY = `# Raigal - agent instructions
18726
+ //#endregion
18727
+ //#region src/hooks/assets.ts
18728
+ const buildRaigalMdBody = (agent) => {
18729
+ let onEditSection;
18730
+ 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.`;
18731
+ 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.`;
18732
+ 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.`;
18733
+ 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.`;
18734
+ return `# Raigal - agent instructions
18660
18735
 
18661
18736
  [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
18737
 
18663
18738
  ## On every edit
18664
18739
 
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.
18740
+ ${onEditSection}
18666
18741
 
18667
18742
  ## Severity ladder
18668
18743
 
@@ -18682,6 +18757,8 @@ A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or Multi
18682
18757
  - If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
18683
18758
  - The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
18684
18759
  `;
18760
+ };
18761
+ buildRaigalMdBody("claude");
18685
18762
  //#endregion
18686
18763
  //#region src/hooks/io/atomic-write.ts
18687
18764
  const atomicWrite = (targetPath, content) => {
@@ -18712,12 +18789,12 @@ const readIfExists = (targetPath) => {
18712
18789
  //#endregion
18713
18790
  //#region src/hooks/io/sentinel.ts
18714
18791
  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*-->/;
18792
+ const BEGIN_RE = /<!--\s*(?:raigal|aislop):begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
18793
+ const END_RE = /<!--\s*(?:raigal|aislop):end\s+v\d+\s*-->/;
18717
18794
  const renderFence = (body, hash) => [
18718
- `<!-- aislop:begin v1 hash=${hash} -->`,
18795
+ `<!-- raigal:begin v1 hash=${hash} -->`,
18719
18796
  body.trimEnd(),
18720
- "<!-- aislop:end v1 -->"
18797
+ "<!-- raigal:end v1 -->"
18721
18798
  ].join("\n");
18722
18799
  const upsertMarkdownFence = (existing, body, hash) => {
18723
18800
  const fenced = renderFence(body, hash);
@@ -18782,11 +18859,20 @@ const applyRemoval = (result, opts, target, nextContent) => {
18782
18859
  //#region src/hooks/install/rules-only.ts
18783
18860
  const installRulesOnly = (opts, paths, summary) => {
18784
18861
  const result = emptyResult();
18785
- const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
18862
+ const body = paths.body ?? buildRaigalMdBody();
18863
+ const next = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
18786
18864
  applyContent(result, opts, paths.rules, next, summary);
18865
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
18866
+ if (!opts.dryRun) try {
18867
+ fs.rmSync(paths.legacyRules, { force: true });
18868
+ } catch {}
18869
+ }
18787
18870
  if (paths.host && paths.marker) {
18788
18871
  const host = readIfExists(paths.host) ?? "";
18789
- if (!host.includes(paths.marker)) {
18872
+ if (paths.legacyMarker && host.includes(paths.legacyMarker)) {
18873
+ const migrated = host.split("\n").map((l) => l.trim() === paths.legacyMarker ? paths.marker : l).join("\n");
18874
+ applyContent(result, opts, paths.host, migrated, `update ${paths.marker} reference`);
18875
+ } else if (!host.includes(paths.marker)) {
18790
18876
  const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
18791
18877
  const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
18792
18878
  applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
@@ -18799,30 +18885,41 @@ const uninstallRulesOnly = (opts, paths) => {
18799
18885
  removed: [],
18800
18886
  skipped: []
18801
18887
  };
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) {
18888
+ let removedRule = false;
18889
+ if (readIfExists(paths.rules) != null) {
18890
+ applyRemoval(result, opts, paths.rules, null);
18891
+ removedRule = true;
18892
+ }
18893
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
18894
+ applyRemoval(result, opts, paths.legacyRules, null);
18895
+ removedRule = true;
18896
+ }
18897
+ if (!removedRule) result.skipped.push(paths.rules);
18898
+ if (paths.host && (paths.marker || paths.legacyMarker)) {
18805
18899
  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();
18900
+ if (host && (paths.marker && host.includes(paths.marker) || paths.legacyMarker && host.includes(paths.legacyMarker))) {
18901
+ 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
18902
  applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
18809
- } else result.skipped.push(paths.host);
18903
+ } else if (paths.host) result.skipped.push(paths.host);
18810
18904
  }
18811
18905
  return result;
18812
18906
  };
18813
18907
  //#endregion
18814
18908
  //#region src/hooks/install/antigravity.ts
18815
- const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
18909
+ const resolveAntigravityPaths = (opts) => ({
18910
+ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-raigal-rules.md"),
18911
+ legacyRules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md")
18912
+ });
18816
18913
  const installAntigravity = (opts) => {
18817
18914
  if (opts.scope !== "project") return {
18818
18915
  wrote: [],
18819
18916
  skipped: [],
18820
18917
  planned: [{
18821
- path: ".agents/rules/antigravity-aislop-rules.md",
18918
+ path: ".agents/rules/antigravity-raigal-rules.md",
18822
18919
  summary: "Antigravity is project-scope only; pass --project"
18823
18920
  }]
18824
18921
  };
18825
- return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
18922
+ return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-raigal-rules.md");
18826
18923
  };
18827
18924
  const uninstallAntigravity = (opts) => {
18828
18925
  if (opts.scope !== "project") return {
@@ -18831,9 +18928,7 @@ const uninstallAntigravity = (opts) => {
18831
18928
  };
18832
18929
  return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
18833
18930
  };
18834
- //#endregion
18835
- //#region src/hooks/io/json-patch.ts
18836
- const AISLOP_SENTINEL_KEY = "__aislop";
18931
+ const RAIGAL_SENTINEL_KEY = "__raigal";
18837
18932
  const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
18838
18933
  const groupIsAislop = (group) => {
18839
18934
  if (typeof group !== "object" || group === null) return false;
@@ -18883,6 +18978,7 @@ const resolveClaudePaths = (opts) => {
18883
18978
  const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
18884
18979
  return {
18885
18980
  settings: path.join(root, "settings.json"),
18981
+ raigalMd: path.join(root, "RAIGAL.md"),
18886
18982
  aislopMd: path.join(root, "AISLOP.md"),
18887
18983
  claudeMd: path.join(root, "CLAUDE.md")
18888
18984
  };
@@ -18897,7 +18993,7 @@ const buildHookGroup$1 = () => {
18897
18993
  hooks: [{
18898
18994
  type: "command",
18899
18995
  command: "raigal hook claude",
18900
- [AISLOP_SENTINEL_KEY]: {
18996
+ [RAIGAL_SENTINEL_KEY]: {
18901
18997
  v: 1,
18902
18998
  managed: true,
18903
18999
  hash: sentinelHash(hashBody)
@@ -18912,7 +19008,7 @@ const buildStopHookGroup = () => {
18912
19008
  hooks: [{
18913
19009
  type: "command",
18914
19010
  command: "raigal hook claude --stop",
18915
- [AISLOP_SENTINEL_KEY]: {
19011
+ [RAIGAL_SENTINEL_KEY]: {
18916
19012
  v: 1,
18917
19013
  managed: true,
18918
19014
  hash: sentinelHash(hashBody)
@@ -18931,7 +19027,7 @@ const buildFileChangedHookGroup = () => {
18931
19027
  hooks: [{
18932
19028
  type: "command",
18933
19029
  command: "raigal hook claude --on-file-changed",
18934
- [AISLOP_SENTINEL_KEY]: {
19030
+ [RAIGAL_SENTINEL_KEY]: {
18935
19031
  v: 1,
18936
19032
  managed: true,
18937
19033
  hash: sentinelHash(hashBody)
@@ -18957,15 +19053,25 @@ const installClaude = (opts) => {
18957
19053
  const result = emptyResult();
18958
19054
  const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
18959
19055
  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");
19056
+ const body = buildRaigalMdBody("claude");
19057
+ const mdHash = sentinelHash(body);
19058
+ const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, mdHash);
19059
+ applyContent(result, opts, paths.raigalMd, fenced.nextContent, "write RAIGAL.md rules");
19060
+ if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
19061
+ if (!opts.dryRun) try {
19062
+ fs.rmSync(paths.aislopMd, { force: true });
19063
+ } catch {}
19064
+ }
18963
19065
  const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
18964
- const marker = "@AISLOP.md";
18965
- if (!existingClaudeMd.includes(marker)) {
19066
+ const marker = "@RAIGAL.md";
19067
+ const legacyMarker = "@AISLOP.md";
19068
+ if (existingClaudeMd.includes(legacyMarker)) {
19069
+ const migrated = existingClaudeMd.split("\n").map((line) => line.trim() === legacyMarker ? marker : line).join("\n");
19070
+ applyContent(result, opts, paths.claudeMd, migrated, "update @RAIGAL.md reference");
19071
+ } else if (!existingClaudeMd.includes(marker)) {
18966
19072
  const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
18967
19073
  const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
18968
- applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
19074
+ applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
18969
19075
  } else result.skipped.push(paths.claudeMd);
18970
19076
  return result;
18971
19077
  };
@@ -18994,11 +19100,19 @@ const uninstallClaude = (opts) => {
18994
19100
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
18995
19101
  else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
18996
19102
  } 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);
19103
+ let removedMd = false;
19104
+ if (readIfExists(paths.raigalMd) != null) {
19105
+ applyRemoval(result, opts, paths.raigalMd, null);
19106
+ removedMd = true;
19107
+ }
19108
+ if (readIfExists(paths.aislopMd) != null) {
19109
+ applyRemoval(result, opts, paths.aislopMd, null);
19110
+ removedMd = true;
19111
+ }
19112
+ if (!removedMd) result.skipped.push(paths.raigalMd);
18999
19113
  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();
19114
+ if (claudeMd?.includes("@RAIGAL.md") || claudeMd?.includes("@AISLOP.md")) {
19115
+ 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
19116
  applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
19003
19117
  } else result.skipped.push(paths.claudeMd);
19004
19118
  return result;
@@ -19006,7 +19120,10 @@ const uninstallClaude = (opts) => {
19006
19120
  //#endregion
19007
19121
  //#region src/hooks/install/cline.ts
19008
19122
  const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
19009
- const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
19123
+ const resolveRooPaths = (opts) => ({
19124
+ rules: path.join(opts.cwd, ".roo", "rules", "raigal.md"),
19125
+ legacyRules: path.join(opts.cwd, ".roo", "rules", "aislop.md")
19126
+ });
19010
19127
  const installCline = (opts) => {
19011
19128
  if (opts.scope !== "project") return {
19012
19129
  wrote: [],
@@ -19017,7 +19134,7 @@ const installCline = (opts) => {
19017
19134
  }]
19018
19135
  };
19019
19136
  const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
19020
- const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
19137
+ const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/raigal.md");
19021
19138
  return {
19022
19139
  wrote: [...cline.wrote, ...roo.wrote],
19023
19140
  skipped: [...cline.skipped, ...roo.skipped],
@@ -19068,7 +19185,8 @@ const resolveCursorPaths = (opts) => {
19068
19185
  const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
19069
19186
  return {
19070
19187
  hooks: path.join(root, "hooks.json"),
19071
- rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
19188
+ rules: path.join(opts.cwd, ".cursor", "rules", "raigal.mdc"),
19189
+ legacyRules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
19072
19190
  };
19073
19191
  };
19074
19192
  const buildHookEntry = () => {
@@ -19080,7 +19198,7 @@ const buildHookEntry = () => {
19080
19198
  command: "raigal hook cursor",
19081
19199
  type: "command",
19082
19200
  timeout: 5e3,
19083
- [AISLOP_SENTINEL_KEY]: {
19201
+ [RAIGAL_SENTINEL_KEY]: {
19084
19202
  v: 1,
19085
19203
  managed: true,
19086
19204
  hash: sentinelHash(hashBody)
@@ -19104,8 +19222,14 @@ const installCursor = (opts) => {
19104
19222
  const nextHooks = renderHooksJson(readIfExists(paths.hooks));
19105
19223
  applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
19106
19224
  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");
19225
+ const body = buildRaigalMdBody("cursor");
19226
+ const rules = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
19227
+ applyContent(result, opts, paths.rules, rules, "write .cursor/rules/raigal.mdc");
19228
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
19229
+ if (!opts.dryRun) try {
19230
+ fs.rmSync(paths.legacyRules, { force: true });
19231
+ } catch {}
19232
+ }
19109
19233
  }
19110
19234
  return result;
19111
19235
  };
@@ -19129,7 +19253,18 @@ const uninstallCursor = (opts) => {
19129
19253
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
19130
19254
  else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
19131
19255
  } else result.skipped.push(paths.hooks);
19132
- if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
19256
+ if (opts.scope === "project") {
19257
+ let removedRule = false;
19258
+ if (readIfExists(paths.rules) != null) {
19259
+ applyRemoval(result, opts, paths.rules, null);
19260
+ removedRule = true;
19261
+ }
19262
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
19263
+ applyRemoval(result, opts, paths.legacyRules, null);
19264
+ removedRule = true;
19265
+ }
19266
+ if (!removedRule) result.skipped.push(paths.rules);
19267
+ }
19133
19268
  return result;
19134
19269
  };
19135
19270
  //#endregion
@@ -19138,6 +19273,7 @@ const resolveGeminiPaths = (opts) => {
19138
19273
  const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
19139
19274
  return {
19140
19275
  settings: path.join(root, "settings.json"),
19276
+ raigalMd: path.join(root, "RAIGAL.md"),
19141
19277
  aislopMd: path.join(root, "AISLOP.md"),
19142
19278
  geminiMd: path.join(root, "GEMINI.md")
19143
19279
  };
@@ -19154,7 +19290,7 @@ const buildHookGroup = () => {
19154
19290
  type: "command",
19155
19291
  command: "raigal hook gemini",
19156
19292
  timeout: 5e3,
19157
- [AISLOP_SENTINEL_KEY]: {
19293
+ [RAIGAL_SENTINEL_KEY]: {
19158
19294
  v: 1,
19159
19295
  managed: true,
19160
19296
  hash: sentinelHash(hashBody)
@@ -19177,14 +19313,25 @@ const installGemini = (opts) => {
19177
19313
  const result = emptyResult();
19178
19314
  const next = renderSettings(readIfExists(paths.settings));
19179
19315
  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");
19316
+ const body = buildRaigalMdBody("gemini");
19317
+ const hash = sentinelHash(body);
19318
+ const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, hash).nextContent;
19319
+ applyContent(result, opts, paths.raigalMd, fenced, "write RAIGAL.md rules");
19320
+ if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
19321
+ if (!opts.dryRun) try {
19322
+ fs.rmSync(paths.aislopMd, { force: true });
19323
+ } catch {}
19324
+ }
19182
19325
  const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
19183
- const marker = "@AISLOP.md";
19184
- if (!existingGeminiMd.includes(marker)) {
19326
+ const marker = "@RAIGAL.md";
19327
+ const legacyMarker = "@AISLOP.md";
19328
+ if (existingGeminiMd.includes(legacyMarker)) {
19329
+ const migrated = existingGeminiMd.split("\n").map((l) => l.trim() === legacyMarker ? marker : l).join("\n");
19330
+ applyContent(result, opts, paths.geminiMd, migrated, "update @RAIGAL.md reference");
19331
+ } else if (!existingGeminiMd.includes(marker)) {
19185
19332
  const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
19186
19333
  const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
19187
- applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
19334
+ applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
19188
19335
  } else result.skipped.push(paths.geminiMd);
19189
19336
  return result;
19190
19337
  };
@@ -19208,28 +19355,39 @@ const uninstallGemini = (opts) => {
19208
19355
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
19209
19356
  else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
19210
19357
  } 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);
19358
+ let removedMd = false;
19359
+ if (readIfExists(paths.raigalMd) != null) {
19360
+ applyRemoval(result, opts, paths.raigalMd, null);
19361
+ removedMd = true;
19362
+ }
19363
+ if (readIfExists(paths.aislopMd) != null) {
19364
+ applyRemoval(result, opts, paths.aislopMd, null);
19365
+ removedMd = true;
19366
+ }
19367
+ if (!removedMd) result.skipped.push(paths.raigalMd);
19213
19368
  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();
19369
+ if (geminiMd?.includes("@RAIGAL.md") || geminiMd?.includes("@AISLOP.md")) {
19370
+ 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
19371
  applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
19217
19372
  } else result.skipped.push(paths.geminiMd);
19218
19373
  return result;
19219
19374
  };
19220
19375
  //#endregion
19221
19376
  //#region src/hooks/install/kilocode.ts
19222
- const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
19377
+ const resolveKilocodePaths = (opts) => ({
19378
+ rules: path.join(opts.cwd, ".kilocode", "rules", "raigal-rules.md"),
19379
+ legacyRules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md")
19380
+ });
19223
19381
  const installKilocode = (opts) => {
19224
19382
  if (opts.scope !== "project") return {
19225
19383
  wrote: [],
19226
19384
  skipped: [],
19227
19385
  planned: [{
19228
- path: ".kilocode/rules/aislop-rules.md",
19386
+ path: ".kilocode/rules/raigal-rules.md",
19229
19387
  summary: "Kilo Code is project-scope only; pass --project"
19230
19388
  }]
19231
19389
  };
19232
- return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
19390
+ return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/raigal-rules.md");
19233
19391
  };
19234
19392
  const uninstallKilocode = (opts) => {
19235
19393
  if (opts.scope !== "project") return {
@@ -19383,18 +19541,24 @@ const paths = {
19383
19541
  const p = resolveClaudePaths(opts);
19384
19542
  return [
19385
19543
  p.settings,
19544
+ p.raigalMd,
19386
19545
  p.aislopMd,
19387
19546
  p.claudeMd
19388
19547
  ];
19389
19548
  },
19390
19549
  cursor: (opts) => {
19391
19550
  const p = resolveCursorPaths(opts);
19392
- return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
19551
+ return opts.scope === "project" ? [
19552
+ p.hooks,
19553
+ p.rules,
19554
+ ...p.legacyRules ? [p.legacyRules] : []
19555
+ ] : [p.hooks];
19393
19556
  },
19394
19557
  gemini: (opts) => {
19395
19558
  const p = resolveGeminiPaths(opts);
19396
19559
  return [
19397
19560
  p.settings,
19561
+ p.raigalMd,
19398
19562
  p.aislopMd,
19399
19563
  p.geminiMd
19400
19564
  ];
@@ -19402,9 +19566,22 @@ const paths = {
19402
19566
  pi: (opts) => [resolvePiPaths(opts).extension],
19403
19567
  codex: (opts) => [resolveCodexPaths(opts).rules],
19404
19568
  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],
19569
+ cline: (opts) => {
19570
+ const roo = resolveRooPaths(opts);
19571
+ return [
19572
+ resolveClinePaths(opts).rules,
19573
+ roo.rules,
19574
+ ...roo.legacyRules ? [roo.legacyRules] : []
19575
+ ];
19576
+ },
19577
+ kilocode: (opts) => {
19578
+ const k = resolveKilocodePaths(opts);
19579
+ return [k.rules, ...k.legacyRules ? [k.legacyRules] : []];
19580
+ },
19581
+ antigravity: (opts) => {
19582
+ const a = resolveAntigravityPaths(opts);
19583
+ return [a.rules, ...a.legacyRules ? [a.legacyRules] : []];
19584
+ },
19408
19585
  copilot: (opts) => [resolveCopilotPaths(opts).rules]
19409
19586
  };
19410
19587
  const REGISTRY = {
@@ -20486,12 +20663,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
20486
20663
  scoreFileCount
20487
20664
  });
20488
20665
  if (options.sarif) {
20489
- const { buildSarifLog } = await import("./sarif-Lt31Ld5t.js");
20666
+ const { buildSarifLog } = await import("./sarif-Cu7Rm1Ma.js");
20490
20667
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
20491
20668
  return completion;
20492
20669
  }
20493
20670
  if (options.json) {
20494
- const { buildJsonOutput } = await import("./json-BBZKvm0E.js");
20671
+ const { buildJsonOutput } = await import("./json-DxIA7CCt.js");
20495
20672
  const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
20496
20673
  console.log(JSON.stringify(jsonOut, null, 2));
20497
20674
  return completion;
@@ -20810,10 +20987,12 @@ const buildInitSuccessRender = (input) => {
20810
20987
  }, deps);
20811
20988
  const writtenCount = input.steps.filter((s) => s.status === "done").length;
20812
20989
  const footer = writtenCount === 0 ? "Nothing to write" : `Done · wrote ${writtenCount} file${writtenCount === 1 ? "" : "s"}`;
20813
- return `${header}${renderRail({
20990
+ const rail = renderRail({
20814
20991
  steps: input.steps,
20815
20992
  footer
20816
- }, deps)}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}`;
20993
+ }, deps);
20994
+ const extraHint = input.hint ? `\n${renderHintLine(input.hint, deps)}` : "";
20995
+ return `${header}${rail}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}${extraHint}`;
20817
20996
  };
20818
20997
  const ENGINE_CHOICES = [
20819
20998
  {
@@ -20864,7 +21043,45 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
20864
21043
  relativePath
20865
21044
  };
20866
21045
  };
20867
- const promptForConfigChoices = async () => {
21046
+ const writeGitlabWorkflow = (rootDirectory, enabled) => {
21047
+ const relativePath = DEFAULT_GITLAB_CI_FILE;
21048
+ if (!enabled) return { status: "declined" };
21049
+ const workflowPath = path.join(rootDirectory, DEFAULT_GITLAB_CI_FILE);
21050
+ if (fs.existsSync(workflowPath)) return {
21051
+ status: "skipped-exists",
21052
+ relativePath
21053
+ };
21054
+ fs.writeFileSync(workflowPath, DEFAULT_GITLAB_CI_YAML);
21055
+ return {
21056
+ status: "written",
21057
+ relativePath
21058
+ };
21059
+ };
21060
+ const writeBitbucketWorkflow = (rootDirectory, enabled) => {
21061
+ const relativePath = DEFAULT_BITBUCKET_FILE;
21062
+ if (!enabled) return { status: "declined" };
21063
+ const workflowPath = path.join(rootDirectory, DEFAULT_BITBUCKET_FILE);
21064
+ if (fs.existsSync(workflowPath)) return {
21065
+ status: "skipped-exists",
21066
+ relativePath
21067
+ };
21068
+ fs.writeFileSync(workflowPath, DEFAULT_BITBUCKET_PIPELINES_YAML);
21069
+ return {
21070
+ status: "written",
21071
+ relativePath
21072
+ };
21073
+ };
21074
+ const detectDefaultCiProvider = (directory) => {
21075
+ if (fs.existsSync(path.join(directory, ".gitlab-ci.yml"))) return "gitlab";
21076
+ if (fs.existsSync(path.join(directory, "bitbucket-pipelines.yml"))) return "bitbucket";
21077
+ if (fs.existsSync(path.join(directory, ".github/workflows"))) return "github";
21078
+ const detectedOwner = detectLocalRepoOwner(directory);
21079
+ if (detectedOwner.hasRemote) {
21080
+ if (detectedOwner.isGitHubRemote) return "github";
21081
+ }
21082
+ return "github";
21083
+ };
21084
+ const promptForConfigChoices = async (directory) => {
20868
21085
  const enginesSelection = await runCancellable(() => multiselect({
20869
21086
  message: "Which engines should run?",
20870
21087
  options: ENGINE_CHOICES,
@@ -20894,15 +21111,26 @@ const promptForConfigChoices = async () => {
20894
21111
  });
20895
21112
  if (isCancel(telemetryChoice)) return null;
20896
21113
  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"
21114
+ message: "Add automated CI quality gate workflow?",
21115
+ options: [
21116
+ {
21117
+ value: "github",
21118
+ label: "GitHub Actions — writes .github/workflows/raigal.yml"
21119
+ },
21120
+ {
21121
+ value: "gitlab",
21122
+ label: "GitLab CI — writes .gitlab-ci.yml"
21123
+ },
21124
+ {
21125
+ value: "bitbucket",
21126
+ label: "Bitbucket Pipelines — writes bitbucket-pipelines.yml"
21127
+ },
21128
+ {
21129
+ value: "none",
21130
+ label: "No, I'll wire CI myself"
21131
+ }
21132
+ ],
21133
+ initialValue: detectDefaultCiProvider(directory)
20906
21134
  });
20907
21135
  if (isCancel(workflowChoice)) return null;
20908
21136
  return {
@@ -20910,7 +21138,8 @@ const promptForConfigChoices = async () => {
20910
21138
  failBelow: Number(failBelowRaw),
20911
21139
  typecheck: DEFAULT_CONFIG.lint.typecheck,
20912
21140
  telemetryEnabled: telemetryChoice === "enabled",
20913
- writeGithubWorkflow: workflowChoice === "yes"
21141
+ writeGithubWorkflow: workflowChoice === "github",
21142
+ ciProvider: workflowChoice
20914
21143
  };
20915
21144
  };
20916
21145
  const strictChoices = () => ({
@@ -20918,7 +21147,8 @@ const strictChoices = () => ({
20918
21147
  failBelow: 85,
20919
21148
  typecheck: true,
20920
21149
  telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
20921
- writeGithubWorkflow: true
21150
+ writeGithubWorkflow: true,
21151
+ ciProvider: "github"
20922
21152
  });
20923
21153
  const writeAislopConfig = (configDir, configPath, choices) => {
20924
21154
  const selected = new Set(choices.engines);
@@ -20995,7 +21225,7 @@ const initCommand = async (directory, options = {}) => {
20995
21225
  return;
20996
21226
  }
20997
21227
  }
20998
- const choices = options.strict ? strictChoices() : await promptForConfigChoices();
21228
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices(resolvedDir);
20999
21229
  if (!choices) return;
21000
21230
  writeAislopConfig(configDir, configPath, choices);
21001
21231
  const steps = [{
@@ -21012,19 +21242,49 @@ const initCommand = async (directory, options = {}) => {
21012
21242
  status: "skipped",
21013
21243
  label: `${CONFIG_DIR}/${RULES_FILE} already exists`
21014
21244
  });
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
- });
21245
+ let ciHint;
21246
+ if (choices.ciProvider === "gitlab") {
21247
+ const gitlabResult = writeGitlabWorkflow(resolvedDir, true);
21248
+ if (gitlabResult.status === "written") {
21249
+ steps.push({
21250
+ status: "done",
21251
+ label: `Wrote ${gitlabResult.relativePath}`
21252
+ });
21253
+ ciHint = "Set RAIGAL_TOKEN in GitLab CI/CD Variables (Settings > CI/CD > Variables).";
21254
+ } else if (gitlabResult.status === "skipped-exists") steps.push({
21255
+ status: "skipped",
21256
+ label: `${gitlabResult.relativePath} already exists`
21257
+ });
21258
+ } else if (choices.ciProvider === "bitbucket") {
21259
+ const bbResult = writeBitbucketWorkflow(resolvedDir, true);
21260
+ if (bbResult.status === "written") {
21261
+ steps.push({
21262
+ status: "done",
21263
+ label: `Wrote ${bbResult.relativePath}`
21264
+ });
21265
+ ciHint = "Set RAIGAL_TOKEN in Bitbucket Repository Variables (Repository settings > Repository variables).";
21266
+ } else if (bbResult.status === "skipped-exists") steps.push({
21267
+ status: "skipped",
21268
+ label: `${bbResult.relativePath} already exists`
21269
+ });
21270
+ } else if (choices.writeGithubWorkflow || choices.ciProvider === "github") {
21271
+ const workflowResult = writeGithubWorkflow(resolvedDir, true);
21272
+ if (workflowResult.status === "written") {
21273
+ steps.push({
21274
+ status: "done",
21275
+ label: `Wrote ${workflowResult.relativePath}`
21276
+ });
21277
+ ciHint = "GitHub Actions zero-token OIDC enabled (permissions: id-token: write).";
21278
+ } else if (workflowResult.status === "skipped-exists") steps.push({
21279
+ status: "skipped",
21280
+ label: `${workflowResult.relativePath} already exists`
21281
+ });
21282
+ }
21024
21283
  process.stdout.write(buildInitSuccessRender({
21025
21284
  steps,
21026
21285
  nextCommand: `${invocation} scan`,
21027
- includeHeader: false
21286
+ includeHeader: false,
21287
+ hint: ciHint
21028
21288
  }));
21029
21289
  };
21030
21290
  //#endregion