Zenn
Open3

「CursorのProject Rules運用のベストプラクティスを探る」を試してみる

すてぃおすてぃお

こんな感じのGitHub Actionsを作り、作ってもらうようにした

name: Update Cursor Rules

on:
  push:
    paths:
      - 'rules/**'
      - 'scripts/build_mdc.js'

jobs:
  update-rules:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm install

      - name: Generate MDC files
        run: npm run build:mdc

      - name: Check for changes
        id: git-check
        run: |
          git add ".cursor/rules/"*.mdc
          if git diff --staged --quiet; then
            echo "No changes detected"
            echo "changes=false" >> "$GITHUB_OUTPUT"
          else
            echo "Changes detected"
            echo "changes=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Create Pull Request
        if: steps.git-check.outputs.changes == 'true'
        uses: peter-evans/create-pull-request@v6
        with:
          commit-message: "chore: update cursor rules"
          title: "chore: update cursor rules from rules directory"
          body: |
            このPRは、rulesディレクトリの変更に基づいて.cursor/rulesディレクトリのmdcファイルを更新します。
            
            ## 変更内容
            - rulesディレクトリの変更を.cursor/rulesディレクトリに反映
            
            ## 自動生成
            このPRは、GitHub Actionsによって自動的に生成されました。
          branch: update-cursor-rules
          base: ${{ github.ref_name }}
          delete-branch: true

一旦これで運用予定

すてぃおすてぃお

JSはこんな感じで書いた

const fs = require('fs');
const path = require('path');
const { glob } = require('glob');

// mdcファイルとmdディレクトリの対応関係の定義
const mdcConfigurations = [
  {
    output: ".cursor/rules/00_basic.mdc",
    sourceDir: "rules/general",
    header: "---\ndescription: セッション開始時に読み込むこと\nglobs: \nalwaysApply: true\n---\n\n",
    filePattern: "*.md",
    sortBy: "name"
  },
  {
    output: ".cursor/rules/01_project.mdc",
    sourceDir: "rules/common",
    header: "---\ndescription: \nglobs: \nalwaysApply: false\n---\n",
    filePattern: "*.md",
    sortBy: "name"
  },
  {
    output: ".cursor/rules/02_memory.mdc",
    sourceDir: "rules/memory",
    header: "---\ndescription: 今のセッション情報を保存する際に使用\nglobs: \nalwaysApply: true\n---\n",
    filePattern: "*.md",
    sortBy: "name"
  },
  {
    output: ".cursor/rules/03_backend_development_checklist.mdc",
    sourceDir: "rules/backend",
    header: "---\ndescription: ドキュメント以外のファイルを修正した際\nglobs: \nalwaysApply: false\n---\n",
    filePattern: "*.md",
    sortBy: "name"
  },
  {
    output: ".cursor/rules/04_context_window.mdc",
    sourceDir: "rules/testing",
    header: "---\ndescription: *.jsonなどのファイルを読み込む時\nglobs: \nalwaysApply: false\n---\n",
    filePattern: "*.md",
    sortBy: "name"
  },
  {
    output: ".cursor/rules/05_git.mdc",
    sourceDir: "rules/general",
    header: "---\ndescription: \nglobs: \nalwaysApply: false\n---\n",
    filePattern: "*.md",
    sortBy: "name"
  }
];

// ファイル名から数字プレフィックスを抽出してソートするための関数
function extractNumberPrefix(filename) {
  const match = filename.match(/^(\d+)_/);
  return match ? parseInt(match[1], 10) : Infinity;
}

// mdファイルを検索して結合する関数
async function buildMdcFile(config) {
  // ルートディレクトリの取得(スクリプトの実行場所から相対パスで計算)
  const rootDir = path.resolve(process.cwd());
  
  // mdファイルのパターンを作成
  const pattern = path.join(rootDir, config.sourceDir, config.filePattern);
  
  // mdファイルを検索
  const files = await glob(pattern);
  
  // ファイルが見つからない場合は処理をスキップ
  if (files.length === 0) {
    console.log(`No files found in ${config.sourceDir}, skipping...`);
    return;
  }
  
  // ファイル名でソート
  files.sort((a, b) => {
    const numA = extractNumberPrefix(path.basename(a));
    const numB = extractNumberPrefix(path.basename(b));
    return numA - numB;
  });
  
  // コンテンツの初期化
  let content = '';
  
  // ヘッダー情報を追加
  content += config.header;
  
  // 各mdファイルの内容を結合
  for (const file of files) {
    console.log(`Processing file: ${file}`);
    const fileContent = await fs.promises.readFile(file, 'utf8');
    content += fileContent + '\n\n';
  }
  
  // mdcファイルを出力
  const outputPath = path.join(rootDir, config.output);
  
  // 出力ディレクトリが存在することを確認
  const outputDir = path.dirname(outputPath);
  try {
    await fs.promises.mkdir(outputDir, { recursive: true });
  } catch (error) {
    // ディレクトリが既に存在する場合は無視
  }
  
  // ファイルに書き込み
  await fs.promises.writeFile(outputPath, content);
  
  console.log(`Generated ${config.output} from ${files.length} files in ${config.sourceDir}`);
}

// 既存のMDCファイルの中身を空にする関数
async function cleanMdcFiles() {
  const rootDir = path.resolve(process.cwd());
  
  // .cursor/rules ディレクトリの存在確認
  const rulesDir = path.join(rootDir, '.cursor/rules');
  try {
    await fs.promises.access(rulesDir);
  } catch (error) {
    // ディレクトリが存在しない場合は作成
    await fs.promises.mkdir(rulesDir, { recursive: true });
    return;
  }
  
  // .mdc ファイルを検索して中身を空にする
  const mdcFiles = await glob(path.join(rulesDir, '*.mdc'));
  for (const file of mdcFiles) {
    console.log(`Clearing content of MDC file: ${file}`);
    await fs.promises.writeFile(file, ''); // ファイルの中身を空にする
  }
}

// メイン処理
async function main() {
  try {
    // 既存のMDCファイルを削除
    await cleanMdcFiles();
    
    // 各設定に対してmdcファイルを生成
    for (const config of mdcConfigurations) {
      await buildMdcFile(config);
    }
    console.log('All mdc files have been successfully generated!');
  } catch (error) {
    console.error('Error generating mdc files:', error);
    process.exit(1);
  }
}

// スクリプトの実行
main();
ログインするとコメントできます