TypeScript CLI で Markdown 1 ファイルから Cursor/Cline ルールを自動生成する方法
こんにちは!アルダグラムでエンジニアをしている柴田です
はじめに
近年、AI エージェントツールの発展が著しく、開発現場でも Cursor や Cline のような自律エージェントを活用してコード生成やレビューを行うケースが増えていると思います。
弊社でも業務効率化を目的に複数の AI ツールを試験的に導入しており、特定のAIツールに依存せず用途に応じて併用しています。
これらのツールでは、プロジェクト固有のガイドラインやインデックス除外設定を記述したルールファイルを設定することができますが、Cursor と Cline の両方でほぼ同一のルールファイルを手動管理していると、更新タイミングのずれや除外パスの齟齬で想定外の差分事故が起こりがちです。
本記事では、大元のマスターのルールとルール除外設定ファイルをMarkdownで作成し、作成したマスタールールを基に、TypeScript 製の CLI を使って Cursor 向けの cursorrules.mdc
・cursorignore.mdc
と Cline 向けの .clinerules
・.clineignore
の計4ファイルを自動生成する方法をご紹介します。
この仕組みを導入すれば、ルールの修正は Markdown を1か所直すだけで済み、二重メンテや差分漏れを防げます。
本記事のゴール
-
rules/rules.md
に 共通ルール+ツール別(cursor/cline)ルール をまとめる -
rules/rulesignore.md
に 共通除外パス+ツール別除外パス をまとめる - CLI 実行で以下を自動生成
- Cursor 向け:
.cursor/rules/cursorrules.mdc
&.cursor/rules/cursorignore.mdc
- Cline 向け: プロジェクト直下の
.clinerules
&.clineignore
- Cursor 向け:
- 生成後はコミットするだけで完了。必要に応じて CI や Git Hooks も追加可能
ディレクトリ構成
ディレクトリ構成は以下の構成で進めます。
root/
├─ .cursor/
│ └─ rules/
│ ├─ cursorrules.mdc # Cursor が読み込むルール定義
│ └─ cursorignore.mdc # Cursor のインデックス除外設定
├─ .clinerules # Cline が読み込むルール定義
├─ .clineignore # Cline の除外設定
├─ rules/
│ ├─ rules.md # マスター Markdown: ルール定義
│ └─ rulesignore.md # マスター Markdown: 除外設定
├─ bin/
│ └─ generate.ts # TypeScript CLI スクリプト
├─ package.json
└─ tsconfig.json
cursorrules.mdc
/ cursorignore.mdc
とは
ファイル | 役割 |
---|---|
cursorrules.mdc |
Cursor が最初に読み込むシステムプロンプト。 技術スタック・命名規約・ディレクトリ構成などプロジェクト共通ガイドラインを記述。 |
cursorignore.mdc |
Cursor の検索/インデックス対象から除外するパスを定義。 例: node_modules , dist , .env など |
*.cursorrules(ルート直下)は非推奨となっているため、本記事では .cursor/rules/ 配下に配置します。
.clinerules
/.clineignore
とは
- Cline はプロジェクトルートで読み込むルールファイルです。
Cursor 同様に Markdown 形式で記述可能です。
1. マスタールールの作成
ディレクトリ構成にある通り、rules/rules.md
にマスタールールを定義します。
ディレクトリ構成にある通り、rules/rules.md
には全ツール共通のルールと、## cursor
/ ## cline
別のルールをまとめて記述します。
例:
## 作業開始準備
1. `git status` でクリーン状態を確認
2. 無関係変更が多ければ「別タスク化」を提案
3. “無視して続行” 指示があればそのまま進める
## プロジェクト概要
Next.js + TypeScript + GraphQL
UI: Material-UI / Test: Jest・Storybook
## ベストプラクティス
- ルーティング: `pages/`
- 動的ページ → SSR / 静的ページ → SSG
- TypeScript: 関数型優先・`any` 禁止
- コンポーネントはPascalCase / フックはcamelCase
## セキュリティ & i18n
- `.env` や API キーは **コミット禁止**
...(略)...
同様に、rules/rulesignore.md
には全ツール共通の除外パスと、## cursor
/ ## cline
別の除外設定を記述します。
例:
node_modules
dist
.env
build/**
coverage/**
vendor/**
temp/**
2. 実装
以下の 3 つのステップで CLI を構築します。
2.1 依存ライブラリのインストール
必要なPackageを各種インストールします。
npm install -D typescript @types/node @types/commander tsx unified remark-parse commander
- unified / remark-parse: Markdown パーサー
- commander: CLI オプション解析
- tsx: TypeScript を「ビルド無し」で実行できるランタイム
2.2 TypeScript CLI を実装
TypeScript CLI を実装していきます。
作成した bin/generate.ts
を 4 つの要所に分けて解説します。
2.2.1 CLI のオプション定義
-
commander が CLI の引数をパースし、
program.opts()
で取得します - ルート配下以外にファイルを置いた場合でも
-
-rules
/-ignore
/-cursor-dir
/-root
で柔軟に上書きできます
-
program
.option('--rules <file>', 'master rules markdown', DEFAULT_RULES)
.option('--ignore <file>', 'master ignore markdown', DEFAULT_IGNORE)
.option(
'--cursor-dir <dir>',
'output directory for Cursor',
DEFAULT_CURSOR_DIR
)
.option('--root <dir>', 'project root for cline files', DEFAULT_ROOT)
.parse()
2.2.2 Markdown を 3 セクションに分割
- 見出し**
## cursor
/## cline
** を境にcurrent
を切替えます - 見出し前はすべて
common
→ 共通ルール として後段で再利用します
const buf: Record<Section,string[]> = { common: [], cursor: [], cline: [] }
let current: Section = 'common'
for (const node of tree.children) {
// h2 見出しで current を切替え
if (node.type === 'heading' && node.depth === 2) {
const h2 = String(node.children?.[0]?.value ?? '').trim().toLowerCase()
current = h2 === 'cursor' ? 'cursor'
: h2 === 'cline' ? 'cline'
: 'common'
}
// 元 Markdown から文字列 slice → バッファに push
const { start, end } = node.position ?? {}
if (typeof start?.offset === 'number' && typeof end?.offset === 'number') {
buf[current].push(markdown.slice(start.offset, end.offset))
}
}
2.2.3 ファイル生成 & Front-Matter を付与して出力
-
ensureBlankBeforeHeading()
で#
/##
の前に必ず空行を挿入します→ Markdown ビューワでも読みやすく、後編集もしやすくするためです
-
.cursor/rules/
に置くことで Project Rules 形式 を遵守します -
Cursor 用 2 ファイルのみ Prefix(YAML Front-Matter) を先頭に追加します
// 見出しの前に必ず空行を 1 行入れるヘルパー
const ensureBlankBeforeHeading = (s: string) => s.replace(/\n(#+\s)/g, '\n\n$1')
// セクション結合+整形
const cursorRules = ensureBlankBeforeHeading([...rules.common, ...rules.cursor].join('\n'))
const cursorIgnore = ensureBlankBeforeHeading([...ign.common, ...ign.cursor ].join('\n'))
const clineRules = ensureBlankBeforeHeading([...rules.common, ...rules.cline ].join('\n'))
const clineIgnore = ensureBlankBeforeHeading([...ign.common, ...ign.cline ].join('\n'))
// Cursor は YAML Front-Matter を先頭に付与
await writeFile(`${cursorDir}/cursorrules.mdc`, CURSOR_FRONT_MATTER + cursorRules)
await writeFile(`${cursorDir}/cursorignore.mdc`, CURSOR_FRONT_MATTER + cursorIgnore)
// Cline 側はそのまま
await writeFile('.clinerules', clineRules)
await writeFile('.clineignore', clineIgnore)
2.2.4 エラー時は、即失敗させる
- 必須ファイルが見つからない場合は 即
exit(1)
します - CI・CD に組み込めば “マスタールールのコミットし忘れ” を確実に検知可能できます
try {
markdown = await fs.readFile(abs, 'utf-8');
} catch {
console.error('❌ File not found');
process.exit(1); // CI でジョブを失敗させる
}
2.2. 5 TypeScript CLI 全体像
作成したbin/generate.ts のコードの全体像は以下のとおりです。
bin/generate.ts
#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'
import { program } from 'commander'
import remarkParse from 'remark-parse'
import { unified } from 'unified'
const DEFAULT_RULES = 'rules/rules.md'
const DEFAULT_IGNORE = 'rules/rulesignore.md'
const DEFAULT_CURSOR_DIR = '.cursor/rules'
const DEFAULT_ROOT = '.'
/** Cursor ファイルの先頭に付与する YAML Front-Matter */
const CURSOR_FRONT_MATTER = `---\ndescription:\nglobs:\nalwaysApply: true\n---\n\n`
// セクション分類
type Section = 'common' | 'cursor' | 'cline'
// CLI オプション定義
program
.option('--rules <file>', 'master rules markdown', DEFAULT_RULES)
.option('--ignore <file>', 'master ignore markdown', DEFAULT_IGNORE)
.option(
'--cursor-dir <dir>',
'output directory for Cursor',
DEFAULT_CURSOR_DIR
)
.option('--root <dir>', 'project root for cline files', DEFAULT_ROOT)
.parse()
const opts = program.opts<{
rules: string
ignore: string
cursorDir: string
root: string
}>()
// Markdown を 3 セクションに分割する関数
async function parseBySection(
file: string
): Promise<Record<Section, string[]>> {
const abs = path.resolve(file)
let markdown: string
try {
markdown = await fs.readFile(abs, 'utf-8')
} catch {
console.error(`❌ File not found: ${abs}`) // ファイル欠損は即失敗
process.exit(1)
}
const tree = unified().use(remarkParse).parse(markdown) as any
const buf: Record<Section, string[]> = { common: [], cursor: [], cline: [] }
let current: Section = 'common'
for (const node of tree.children) {
// h2 見出しでセクション切替
if (node.type === 'heading' && node.depth === 2) {
const h2 = String(node.children?.[0]?.value ?? '')
.trim()
.toLowerCase()
current = h2 === 'cursor' ? 'cursor' : h2 === 'cline' ? 'cline' : 'common'
// run continues to push slice below as usual
}
// 各セクションに slice して格納
const { start, end } = node.position ?? {}
if (typeof start?.offset === 'number' && typeof end?.offset === 'number') {
buf[current].push(markdown.slice(start.offset, end.offset))
}
}
return buf
}
async function ensureDir(dir: string) {
await fs.mkdir(dir, { recursive: true })
}
async function writeFile(target: string, content: string) {
await ensureDir(path.dirname(target))
await fs.writeFile(target, content.trimEnd() + '\n') // 必ず末尾改行
}
// 見出しの前に必ず空行を入れるヘルパー
function ensureBlankBeforeHeading(src: string): string {
// 1行目が見出しの場合はそのまま、それ以外は # or ## の前に空行を保証
return src.replace(/\n(#+\s)/g, '\n\n$1')
}
;(async () => {
const rulesBuf = await parseBySection(opts.rules)
const ignoreBuf = await parseBySection(opts.ignore)
// セクション結合
const cursorRules = ensureBlankBeforeHeading(
[...rulesBuf.common, ...rulesBuf.cursor].join('\n')
)
const cursorIgnore = ensureBlankBeforeHeading(
[...ignoreBuf.common, ...ignoreBuf.cursor].join('\n')
)
const clineRules = ensureBlankBeforeHeading(
[...rulesBuf.common, ...rulesBuf.cline].join('\n')
)
const clineIgnore = ensureBlankBeforeHeading(
[...ignoreBuf.common, ...ignoreBuf.cline].join('\n')
)
// 出力先パス
const cursorOutDir = path.resolve(opts.cursorDir)
const rootDir = path.resolve(opts.root)
// ファイル書き出し
// - Cursorは Front-Matter を先頭に追加
await writeFile(
path.join(cursorOutDir, 'cursorrules.mdc'),
CURSOR_FRONT_MATTER + cursorRules
)
await writeFile(
path.join(cursorOutDir, 'cursorignore.mdc'),
CURSOR_FRONT_MATTER + cursorIgnore
)
// - Clineはそのまま
await writeFile(path.join(rootDir, '.clinerules'), clineRules)
await writeFile(path.join(rootDir, '.clineignore'), clineIgnore)
console.log(
`✅ Generated files:\n - ${path.relative(rootDir, path.join(cursorOutDir, 'cursorrules.mdc'))}\n - ${path.relative(rootDir, path.join(cursorOutDir, 'cursorignore.mdc'))}\n - .clinerules\n - .clineignore`
)
})()
2.3 package.json へスクリプト登録
ルールを設定するためのカスタムコマンドをpackage.json
に追記します。
{
"scripts": {
"generate:rules": " tsx bin/generate.ts"
},
}
3. 動作確認
CLIを実行し、生成されたファイルが所定の場所に出力されているか確認します。
$ npm run generate:rules
確認ポイント
-
.cursor/rules/cursorrules.mdc
が更新されていること -
.cursor/rules/cursorignore.mdc
が更新されていること - プロジェクト直下の
.clinerules
/.clineignore
が更新されていること
以下のように、正常にファイルが生成されたら動作OKです!
最後に
ここまでご覧いただきありがとうございました!
今回はTypeScript CLI で Cursor/Cline ルールを自動生成する方法をご紹介しました
ルールを 1 つの Markdown に集約すれば、二重メンテを排除し、差分漏れの不安から解放されます。Cursor や Cline を存分に活用して開発を加速しましょう!
もっとアルダグラムエンジニア組織を知りたい人、ぜひ下記の情報をチェックしてみてください!

株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion