Cursor rulesの変更PRが作成されたら Roo Codeのrulesにも反映するPRを作成する

## はじめに

CursorやClineなどAI開発を前提としたツールが熱いですが、チーム内でCursor派、Cline派など複数の派閥があるとルールの管理が怠かったりします。（自分はRoo Codeを使うことが多いです。）

そこで今回はCursorのルールを管理する.mdcファイルを変更するPRが作成されたら.rooの配下に同じようなルールが作成されるPRを自動生成するGithub Actionsの設定を作ってみました。

設定すると以下の動きをします。

- 実装者が.cursor/rulesを変更するPRを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527942/3e34e961-4431-4a9e-87c8-81c12d97af78.png)

- GithubActionsで変更された.cursor/rulesから.rooのrulesを修正するPRを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527942/7f0f812b-8bdd-4b36-b741-4ba0226705e2.png)

## Github Actionsの設定

以下のスニペットのような.ymlを.github/workflowsに作成します。

- .cursor/**

```yml
name: Sync Cursor Rules to Roo

on:
  pull_request:
    paths:
      - '.cursor/**/*.mdc'
    types: [opened, synchronize]

jobs:
  sync-cursor-rules:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Setup GitHub CLI
        run: |
          type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
          curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
          && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
          && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
          && sudo apt update \
          && sudo apt install gh -y

      - name: Run sync script
        run: node scripts/sync-cursor-to-roo.js

      - name: Check for changes
        id: check-changes
        run: |
          if [ -n "$(git status --porcelain .roo)" ]; then
            echo "changes=true" >> $GITHUB_OUTPUT
            echo "changed_files<<EOF" >> $GITHUB_OUTPUT
            git status --porcelain .roo >> $GITHUB_OUTPUT
            echo "EOF" >> $GITHUB_OUTPUT
          else
            echo "changes=false" >> $GITHUB_OUTPUT
          fi

      - name: Create PR for .roo changes
        if: steps.check-changes.outputs.changes == 'true'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Get current PR info
          PR_NUMBER=${{ github.event.number }}
          PR_BRANCH=${{ github.head_ref }}
          BASE_BRANCH=${{ github.base_ref }}

          # Create a new branch for .roo changes with timestamp to ensure uniqueness
          TIMESTAMP=$(date +%Y%m%d-%H%M%S)
          ROO_BRANCH="sync-roo-from-pr-${PR_NUMBER}-${TIMESTAMP}"

          # Configure git
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"

          # Delete existing branch if it exists (both locally and remotely)
          git branch -D "sync-roo-from-pr-${PR_NUMBER}" 2>/dev/null || true
          git push origin --delete "sync-roo-from-pr-${PR_NUMBER}" 2>/dev/null || true

          # Create and switch to new branch
          git checkout -b "${ROO_BRANCH}"

          # Add and commit .roo changes
          git add .roo
          git commit -m "Auto-sync: Update .roo files from .cursor changes in PR #${PR_NUMBER}

          This PR automatically syncs .cursor rule changes to .roo directory.

          Source PR: #${PR_NUMBER}
          Changed files:
          ${{ steps.check-changes.outputs.changed_files }}"

          # Push the new branch (use force push to handle any conflicts)
          git push --force-with-lease origin "${ROO_BRANCH}"

          # Create PR using GitHub CLI
          gh pr create \
            --title "🔄 Auto-sync .roo from .cursor changes (PR #${PR_NUMBER})" \
            --body "This PR automatically syncs .cursor rule changes to .roo directory.

          ## Source
          - **Source PR**: #${PR_NUMBER}
          - **Source Branch**: \`${PR_BRANCH}\`

          ## Changes
          - Converted .mdc files to .md files
          - Removed frontmatter from rule files
          - Updated .roo directory structure

          ## Files Changed
          \`\`\`
          ${{ steps.check-changes.outputs.changed_files }}
          \`\`\`

          ## Auto-generated
          This PR was automatically created by GitHub Actions when .cursor files were modified." \
            --base "${PR_BRANCH}" \
            --head "${ROO_BRANCH}" \
            --label "auto-sync" \
            --label "roo-sync"

```

流れとしては以下のような感じです。

- トリガー
    - 対象ファイル：.cursor 配下の .mdc ファイル
    - 対象イベント：Pull Requestの作成 (opened) または更新 (synchronize)

- 環境の設定
    - Node.jsのセットアップ
    - 対象リポジトリをクローン
    - GitHub CLI のインストール

- RooCode用のPR作成
    - .cursor → .roo への同期スクリプト実行（詳細は後述）
    -  .roo ディレクトリに差分があるか確認
    -  差分があればPRを自動作成

## .cursor → .roo のルールファイル作成スクリプト

Github Actionsで実行されるコードは以下になります。

```js
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

function processFile(filePath) {
  console.log(`Processing: ${filePath}`);

  // Read the .mdc file
  const content = fs.readFileSync(filePath, 'utf8');

  // Remove frontmatter (from first --- to second ---)
  const lines = content.split('\n');
  let startIndex = -1;
  let endIndex = -1;

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].trim() === '---') {
      if (startIndex === -1) {
        startIndex = i;
      } else {
        endIndex = i;
        break;
      }
    }
  }

  let processedContent;
  if (startIndex !== -1 && endIndex !== -1) {
    // Remove frontmatter
    processedContent = lines.slice(endIndex + 1).join('\n');
  } else {
    // No frontmatter found, use original content
    processedContent = content;
  }

  // Determine output path
  const relativePath = path.relative('.cursor', filePath);

  // Convert 'rules' directory to 'rules-code' if the path starts with 'rules/'
  let outputRelativePath = relativePath;
  if (outputRelativePath.startsWith('rules/')) {
    outputRelativePath = 'rules-code/' + outputRelativePath.substring('rules/'.length);
  }

  const outputPath = path.join('.roo', outputRelativePath.replace(/\.mdc$/, '.md'));

  // Create output directory if it doesn't exist
  const outputDir = path.dirname(outputPath);
  fs.mkdirSync(outputDir, { recursive: true });

  // Write processed content
  fs.writeFileSync(outputPath, processedContent);
  console.log(`Created: ${outputPath}`);
}

function findMdcFiles(dir) {
  const files = [];
  const items = fs.readdirSync(dir);

  for (const item of items) {
    const fullPath = path.join(dir, item);
    const stat = fs.statSync(fullPath);

    if (stat.isDirectory()) {
      files.push(...findMdcFiles(fullPath));
    } else if (item.endsWith('.mdc')) {
      files.push(fullPath);
    }
  }

  return files;
}

// Main execution
console.log('Starting sync from .cursor to .roo...');

if (fs.existsSync('.cursor')) {
  const mdcFiles = findMdcFiles('.cursor');
  console.log(`Found ${mdcFiles.length} .mdc files`);

  for (const file of mdcFiles) {
    processFile(file);
  }

  console.log('Sync completed successfully');
} else {
  console.log('No .cursor directory found');
}

```

流れとしては以下のような感じです。

- .mdc → .md に変換
- frontmatter（YAML形式のメタデータ）を削除
- rules/ → rules-code/ にディレクトリ名を変換
- 変換後のファイルを .roo/ に保存


## おわりに

一旦作ってRoo Codeのルールファイルも自動でメンテナンスされるようになりました。
とはいえ、すべての変更をRoo CodeのCodeモードに対応しているrules-codeディレクトリに突っ込んでしまっているなど、コードの改善の余地はあるので、今後運用しながらCursorを使う人も、Cline(Roo Cline)を使う人も常に快適にAI活用できるようブラッシュアップしていきたいと思います。

また今回のyml, jsファイルはどちらも完全にAIが作成してくれていて、作成時間は30m未満でした。「自動化系の実装も速攻で実現できるようになって楽だな〜」と思ったので、フットワーク軽く改善~検証をしていきたいですね。