diff --git a/.gitignore b/.gitignore index 9a1eeb8c9..d7c5c268e 100644 --- a/.gitignore +++ b/.gitignore @@ -172,14 +172,17 @@ pnpm-lock.yaml **/.github/instructions/ **/.cursor/rules/ **/.clinerules/ -**/CLAUDE.md **/.roo/rules/ +**/CLAUDE.md **/.claude/memories/ +**/GEMINI.md +**/.gemini/memories/ **/.rulesync/copilot__* **/.rulesync/cursor__* **/.rulesync/cline__* **/.rulesync/roo__* **/.rulesync/claudecode__* +**/.rulesync/geminicli__* /tmp diff --git a/.rulesync/rules-specification-claude.md b/.rulesync/rules-specification-claudecode.md similarity index 98% rename from .rulesync/rules-specification-claude.md rename to .rulesync/rules-specification-claudecode.md index 471bf066d..6d854ea97 100644 --- a/.rulesync/rules-specification-claude.md +++ b/.rulesync/rules-specification-claudecode.md @@ -1,6 +1,6 @@ --- root: false -targets: ['claude'] +targets: ['*'] description: "Claude Code memories specification" globs: [] --- diff --git a/.rulesync/rules-specification-cline.md b/.rulesync/rules-specification-cline.md index 11b41a5a6..a4353a4be 100644 --- a/.rulesync/rules-specification-cline.md +++ b/.rulesync/rules-specification-cline.md @@ -1,6 +1,6 @@ --- root: false -targets: ['cline'] +targets: ['*'] description: "Cline rules specification" globs: [] --- diff --git a/.rulesync/rules-specification-copilot.md b/.rulesync/rules-specification-copilot.md index 9cddbd85d..ed22696ad 100644 --- a/.rulesync/rules-specification-copilot.md +++ b/.rulesync/rules-specification-copilot.md @@ -1,6 +1,6 @@ --- root: false -targets: ['copilot'] +targets: ['*'] description: "GitHub Copilot custom instructions specification" globs: [] --- diff --git a/.rulesync/rules-specification-cursor.md b/.rulesync/rules-specification-cursor.md index 8d6e61f9d..759fcadab 100644 --- a/.rulesync/rules-specification-cursor.md +++ b/.rulesync/rules-specification-cursor.md @@ -1,6 +1,6 @@ --- root: false -targets: ['cursor'] +targets: ['*'] description: "Cursor project rules specification" globs: [] --- diff --git a/.rulesync/rules-specification-roo.md b/.rulesync/rules-specification-roo.md index 9be1e8bb2..4e6a305e9 100644 --- a/.rulesync/rules-specification-roo.md +++ b/.rulesync/rules-specification-roo.md @@ -1,6 +1,6 @@ --- root: false -targets: ['roo'] +targets: ['*'] description: "Roo Code rules specification" globs: [] --- diff --git a/.vscode/settings.json b/.vscode/settings.json index 4adfcecff..e139bb1b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,7 @@ "clinerules", "cursorrules", "dyoshikawa", + "geminicli", "Nonoctal", "roorules", "rulesync", diff --git a/CONTRIBUTING.ja.md b/CONTRIBUTING.ja.md index 446fba546..a32863ac7 100644 --- a/CONTRIBUTING.ja.md +++ b/CONTRIBUTING.ja.md @@ -89,12 +89,14 @@ rulesync/ │ │ ├── cursor.ts # Cursor Project Rules (MDCフォーマット) │ │ ├── cline.ts # Cline Rules │ │ ├── claudecode.ts # Claude Code Memory (CLAUDE.md + memories) +│ │ ├── geminicli.ts # Gemini CLI設定 (GEMINI.md + memories) │ │ └── roo.ts # Roo Code Rules │ ├── parsers/ # インポート機能用ツール固有パーサー │ │ ├── copilot.ts # GitHub Copilot設定のパース (.github/copilot-instructions.md) │ │ ├── cursor.ts # Cursor設定のパース (.cursorrules, .cursor/rules/*.mdc) │ │ ├── cline.ts # Cline設定のパース (.cline/instructions.md) │ │ ├── claudecode.ts # Claude Code設定のパース (CLAUDE.md, .claude/memories/*.md) +│ │ ├── geminicli.ts # Gemini CLI設定のパース (GEMINI.md, .gemini/memories/*.md) │ │ └── roo.ts # Roo Code設定のパース (.roo/instructions.md) │ ├── types/ # TypeScript型定義 │ │ ├── config.ts # 設定型 @@ -249,7 +251,7 @@ pnpm test src/parsers/ # すべてのパーサーのテス ## 新しいAIツールの追加 -新しいAIツールのサポートを追加するには: +新しいAIツールのサポートを追加するには(最近追加された`geminicli`を参考として): 1. **ジェネレーターを作成**: `src/generators/newtool.ts`を追加 2. **パーサーを作成**: インポート機能用に`src/parsers/newtool.ts`を追加 @@ -257,8 +259,9 @@ pnpm test src/parsers/ # すべてのパーサーのテス 4. **コアに追加**: `src/core/generator.ts`と`src/core/importer.ts`を更新 5. **CLIオプションを追加**: generateとimportコマンドの両方で`src/cli/index.ts`を更新 6. **型を更新**: `src/types/rules.ts`の`ToolTarget`に追加 -7. **テストを追加**: `src/generators/newtool.test.ts`と`src/parsers/newtool.test.ts`を作成 -8. **ドキュメントを更新**: README.mdとREADME.ja.mdに追加 +7. **設定を更新**: `src/utils/config.ts`で出力パスを追加 +8. **テストを追加**: `src/generators/newtool.test.ts`と`src/parsers/newtool.test.ts`を作成 +9. **ドキュメントを更新**: README.mdとREADME.ja.mdに追加 ### ジェネレーターインターフェースパターン diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77de74a1e..f50144512 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,12 +89,14 @@ rulesync/ │ │ ├── cursor.ts # Cursor Project Rules (MDC format) │ │ ├── cline.ts # Cline Rules │ │ ├── claudecode.ts # Claude Code Memory (CLAUDE.md + memories) +│ │ ├── geminicli.ts # Gemini CLI configuration (GEMINI.md + memories) │ │ └── roo.ts # Roo Code Rules │ ├── parsers/ # Tool-specific parsers for import functionality │ │ ├── copilot.ts # Parse GitHub Copilot configurations (.github/copilot-instructions.md) │ │ ├── cursor.ts # Parse Cursor configurations (.cursorrules, .cursor/rules/*.mdc) │ │ ├── cline.ts # Parse Cline configurations (.cline/instructions.md) │ │ ├── claudecode.ts # Parse Claude Code configurations (CLAUDE.md, .claude/memories/*.md) +│ │ ├── geminicli.ts # Parse Gemini CLI configurations (GEMINI.md, .gemini/memories/*.md) │ │ └── roo.ts # Parse Roo Code configurations (.roo/instructions.md) │ ├── types/ # TypeScript type definitions │ │ ├── config.ts # Configuration types @@ -249,7 +251,7 @@ Note: Coverage temporarily reduced due to new import functionality requiring tes ## Adding New AI Tools -To add support for a new AI tool: +To add support for a new AI tool (see the recent addition of `geminicli` as a reference): 1. **Create generator**: Add `src/generators/newtool.ts` 2. **Create parser**: Add `src/parsers/newtool.ts` for import functionality @@ -257,8 +259,9 @@ To add support for a new AI tool: 4. **Add to core**: Update `src/core/generator.ts` and `src/core/importer.ts` 5. **Add CLI options**: Update `src/cli/index.ts` for both generate and import commands 6. **Update types**: Add to `ToolTarget` in `src/types/rules.ts` -7. **Add tests**: Create `src/generators/newtool.test.ts` and `src/parsers/newtool.test.ts` -8. **Update docs**: Add to README.md and README.ja.md +7. **Update config**: Add output path in `src/utils/config.ts` +8. **Add tests**: Create `src/generators/newtool.test.ts` and `src/parsers/newtool.test.ts` +9. **Update docs**: Add to README.md and README.ja.md ### Generator Interface Pattern diff --git a/README.ja.md b/README.ja.md index bc3ee6f1a..804dd1a13 100644 --- a/README.ja.md +++ b/README.ja.md @@ -16,6 +16,7 @@ rulesyncは以下のAI開発ツールの**生成**と**インポート**の両 - **Cline Rules** (`.clinerules/*.md` + `.cline/instructions.md`) - **Claude Code Memory** (`./CLAUDE.md` + `.claude/memories/*.md`) - **Roo Code Rules** (`.roo/rules/*.md` + `.roo/instructions.md`) +- **Gemini CLI** (`GEMINI.md` + `.gemini/memories/*.md`) ## インストール @@ -68,6 +69,7 @@ yarn global add rulesync npx rulesync import --copilot # .github/copilot-instructions.mdから npx rulesync import --cline # .cline/instructions.mdから npx rulesync import --roo # .roo/instructions.mdから + npx rulesync import --geminicli # GEMINI.mdと.gemini/memories/*.mdから ``` 2. **`.rulesync/`ディレクトリのインポートされたルールを確認・編集** @@ -93,9 +95,10 @@ AI開発ツールは新しいツールが頻繁に登場し、急速に進化し - Cursor:リファクタリング - Claude Code:アーキテクチャ設計 - Cline:デバッグ支援 +- Gemini CLI:知的コード解析 ### 🔓 **ベンダーロックインなし** -ベンダーロックインを完全に回避できます。rulesyncの使用を停止することを決定した場合でも、生成されたルールファイル(`.github/instructions/`、`.cursor/rules/`、`.clinerules/`、`CLAUDE.md`など)をそのまま使い続けることができます。 +ベンダーロックインを完全に回避できます。rulesyncの使用を停止することを決定した場合でも、生成されたルールファイル(`.github/instructions/`、`.cursor/rules/`、`.clinerules/`、`CLAUDE.md`、`GEMINI.md`など)をそのまま使い続けることができます。 ### 🎯 **ツール間の一貫性** すべてのAIツールに一貫したルールを適用し、チーム全体のコード品質と開発体験を向上させます。 @@ -171,6 +174,7 @@ rulesyncは2レベルのルールシステムを使用します: | **GitHub Copilot** | 標準フォーマット | 標準フォーマット | すべてのルールがフロントマター付きの同じフォーマットを使用 | | **Cline** | 標準フォーマット | 標準フォーマット | すべてのルールがプレーンMarkdownフォーマットを使用 | | **Roo Code** | 標準フォーマット | 標準フォーマット | すべてのルールが説明ヘッダー付きのプレーンMarkdownフォーマットを使用 | +| **Gemini CLI** | `GEMINI.md` | `.gemini/memories/*.md` | GEMINI.mdがメモリファイルへの`@filename`参照を含む | ### 3. 設定ファイルの生成 @@ -184,6 +188,7 @@ npx rulesync generate --cursor npx rulesync generate --cline npx rulesync generate --claudecode npx rulesync generate --roo +npx rulesync generate --geminicli # クリーンビルド(既存ファイルを最初に削除) npx rulesync generate --delete @@ -205,7 +210,7 @@ npx rulesync generate --base-dir ./apps/web,./apps/api,./packages/shared - `--delete`: 新しいファイルを作成する前に既存の生成済みファイルをすべて削除 - `--verbose`: 生成プロセス中に詳細出力を表示 -- `--copilot`, `--cursor`, `--cline`, `--claudecode`, `--roo`: 指定されたツールのみ生成 +- `--copilot`, `--cursor`, `--cline`, `--claudecode`, `--roo`, `--geminicli`: 指定されたツールのみ生成 - `--base-dir `: 指定されたベースディレクトリに設定ファイルを生成(複数パスの場合はカンマ区切り)。異なるプロジェクトディレクトリにツール固有の設定を生成したいmonorepoセットアップに便利。 ### 4. 既存設定のインポート @@ -219,6 +224,7 @@ npx rulesync import --cursor # .cursorrulesと.cursor/rules/*.mdからイン npx rulesync import --copilot # .github/copilot-instructions.mdと.github/instructions/*.instructions.mdからインポート npx rulesync import --cline # .cline/instructions.mdからインポート npx rulesync import --roo # .roo/instructions.mdからインポート +npx rulesync import --geminicli # GEMINI.mdと.gemini/memories/*.mdからインポート # 複数のツールからインポート npx rulesync import --claudecode --cursor --copilot @@ -326,6 +332,7 @@ globs: "**/*.ts,**/*.tsx" | **Cline** | `.clinerules/*.md` | プレーンMarkdown | 両レベルとも同じフォーマットを使用 | | **Claude Code** | `./CLAUDE.md` (ルート)
`.claude/memories/*.md` (非ルート) | プレーンMarkdown | ルートはCLAUDE.mdに移動
非ルートは別メモリファイルに移動
CLAUDE.mdは`@filename`参照を含む | | **Roo Code** | `.roo/rules/*.md` | プレーンMarkdown | 両レベルとも説明ヘッダー付きの同じフォーマットを使用 | +| **Gemini CLI** | `GEMINI.md` (ルート)
`.gemini/memories/*.md` (非ルート) | プレーンMarkdown | ルートはGEMINI.mdに移動
非ルートは別メモリファイルに移動
GEMINI.mdは`@filename`参照を含む | ## バリデーション diff --git a/README.md b/README.md index 61e336309..006c221bf 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ rulesync supports both **generation** and **import** for the following AI develo - **Cline Rules** (`.clinerules/*.md` + `.cline/instructions.md`) - **Claude Code Memory** (`./CLAUDE.md` + `.claude/memories/*.md`) - **Roo Code Rules** (`.roo/rules/*.md` + `.roo/instructions.md`) +- **Gemini CLI** (`GEMINI.md` + `.gemini/memories/*.md`) ## Installation @@ -68,6 +69,7 @@ If you already have AI tool configurations, you can import them into rulesync fo npx rulesync import --copilot # From .github/copilot-instructions.md npx rulesync import --cline # From .cline/instructions.md npx rulesync import --roo # From .roo/instructions.md + npx rulesync import --geminicli # From GEMINI.md and .gemini/memories/*.md ``` 2. **Review and edit** the imported rules in `.rulesync/` directory @@ -93,9 +95,10 @@ Enable hybrid development workflows combining multiple AI tools: - Cursor for refactoring - Claude Code for architecture design - Cline for debugging assistance +- Gemini CLI for intelligent code analysis ### 🔓 **No Vendor Lock-in** -Avoid vendor lock-in completely. If you decide to stop using rulesync, you can continue using the generated rule files (`.github/instructions/`, `.cursor/rules/`, `.clinerules/`, `CLAUDE.md`, etc.) as-is. +Avoid vendor lock-in completely. If you decide to stop using rulesync, you can continue using the generated rule files (`.github/instructions/`, `.cursor/rules/`, `.clinerules/`, `CLAUDE.md`, `GEMINI.md`, etc.) as-is. ### 🎯 **Consistency Across Tools** Apply consistent rules across all AI tools, improving code quality and development experience for the entire team. @@ -171,6 +174,7 @@ Each AI tool handles rule levels differently: | **GitHub Copilot** | Standard format | Standard format | All rules use same format with frontmatter | | **Cline** | Standard format | Standard format | All rules use plain Markdown format | | **Roo Code** | Standard format | Standard format | All rules use plain Markdown format with description header | +| **Gemini CLI** | `GEMINI.md` | `.gemini/memories/*.md` | GEMINI.md includes `@filename` references to memory files | ### 3. Generate Configuration Files @@ -184,6 +188,7 @@ npx rulesync generate --cursor npx rulesync generate --cline npx rulesync generate --claudecode npx rulesync generate --roo +npx rulesync generate --geminicli # Clean build (delete existing files first) npx rulesync generate --delete @@ -205,7 +210,7 @@ npx rulesync generate --base-dir ./apps/web,./apps/api,./packages/shared - `--delete`: Remove all existing generated files before creating new ones - `--verbose`: Show detailed output during generation process -- `--copilot`, `--cursor`, `--cline`, `--claudecode`, `--roo`: Generate only for specified tools +- `--copilot`, `--cursor`, `--cline`, `--claudecode`, `--roo`, `--geminicli`: Generate only for specified tools - `--base-dir `: Generate configuration files in specified base directories (comma-separated for multiple paths). Useful for monorepo setups where you want to generate tool-specific configurations in different project directories. ### 4. Import Existing Configurations @@ -219,6 +224,7 @@ npx rulesync import --cursor # Import from .cursorrules and .cursor/rules/*. npx rulesync import --copilot # Import from .github/copilot-instructions.md and .github/instructions/*.instructions.md npx rulesync import --cline # Import from .cline/instructions.md npx rulesync import --roo # Import from .roo/instructions.md +npx rulesync import --geminicli # Import from GEMINI.md and .gemini/memories/*.md # Import from multiple tools npx rulesync import --claudecode --cursor --copilot @@ -326,6 +332,7 @@ globs: "**/*.ts,**/*.tsx" | **Cline** | `.clinerules/*.md` | Plain Markdown | Both levels use same format | | **Claude Code** | `./CLAUDE.md` (root)
`.claude/memories/*.md` (non-root) | Plain Markdown | Root goes to CLAUDE.md
Non-root go to separate memory files
CLAUDE.md includes `@filename` references | | **Roo Code** | `.roo/rules/*.md` | Plain Markdown | Both levels use same format with description header | +| **Gemini CLI** | `GEMINI.md` (root)
`.gemini/memories/*.md` (non-root) | Plain Markdown | Root goes to GEMINI.md
Non-root go to separate memory files
GEMINI.md includes `@filename` references | ## Validation diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index 7ebf6e43a..b49cd50cc 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -71,6 +71,9 @@ export async function generateCommand(options: GenerateOptions = {}): Promise { expect.stringContaining("# Generated by rulesync") ); expect(console.log).toHaveBeenCalledWith( - expect.stringContaining("✅ .gitignoreに8個のルールを追加しました:") + expect.stringContaining("✅ .gitignoreに10個のルールを追加しました:") ); }); @@ -60,6 +60,8 @@ describe("gitignoreCommand", () => { CLAUDE.md .claude/memories/ .roo/rules/ +GEMINI.md +.gemini/memories/ # Support for --base-dir option (any depth) **/.github/copilot-instructions.md **/.github/instructions/ @@ -68,6 +70,8 @@ CLAUDE.md **/CLAUDE.md **/.claude/memories/ **/.roo/rules/ +**/GEMINI.md +**/.gemini/memories/ `; mockExistsSync.mockReturnValue(true); @@ -98,7 +102,7 @@ CLAUDE.md expect.stringContaining(".clinerules/") ); expect(console.log).toHaveBeenCalledWith( - expect.stringContaining("✅ .gitignoreに7個のルールを追加しました:") + expect.stringContaining("✅ .gitignoreに9個のルールを追加しました:") ); }); }); diff --git a/src/cli/commands/gitignore.ts b/src/cli/commands/gitignore.ts index 275a56cb6..b2a0e58df 100644 --- a/src/cli/commands/gitignore.ts +++ b/src/cli/commands/gitignore.ts @@ -13,6 +13,8 @@ export const gitignoreCommand = async (): Promise => { "**/CLAUDE.md", "**/.claude/memories/", "**/.roo/rules/", + "**/GEMINI.md", + "**/.gemini/memories/", ]; let gitignoreContent = ""; diff --git a/src/cli/commands/import.ts b/src/cli/commands/import.ts index 42d5f6b1b..980824a12 100644 --- a/src/cli/commands/import.ts +++ b/src/cli/commands/import.ts @@ -7,6 +7,7 @@ export interface ImportOptions { copilot?: boolean; cline?: boolean; roo?: boolean; + geminicli?: boolean; verbose?: boolean; } @@ -19,11 +20,12 @@ export async function importCommand(options: ImportOptions = {}): Promise if (options.copilot) tools.push("copilot"); if (options.cline) tools.push("cline"); if (options.roo) tools.push("roo"); + if (options.geminicli) tools.push("geminicli"); // Validate that at least one tool is selected if (tools.length === 0) { console.error( - "❌ Please specify at least one tool to import from (--claudecode, --cursor, --copilot, --cline, --roo)" + "❌ Please specify at least one tool to import from (--claudecode, --cursor, --copilot, --cline, --roo, --geminicli)" ); process.exit(1); } diff --git a/src/cli/index.ts b/src/cli/index.ts index f582fdfb0..ca2ef255c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -34,6 +34,7 @@ program .option("--copilot", "Import from GitHub Copilot (.github/copilot-instructions.md)") .option("--cline", "Import from Cline (.cline/instructions.md)") .option("--roo", "Import from Roo Code (.roo/instructions.md)") + .option("--geminicli", "Import from Gemini CLI (GEMINI.md)") .option("-v, --verbose", "Verbose output") .action(importCommand); @@ -45,6 +46,7 @@ program .option("--cline", "Generate only for Cline") .option("--claudecode", "Generate only for Claude Code") .option("--roo", "Generate only for Roo Code") + .option("--geminicli", "Generate only for Gemini CLI") .option("--delete", "Delete all existing files in output directories before generating") .option( "-b, --base-dir ", @@ -58,6 +60,7 @@ program if (options.cline) tools.push("cline"); if (options.claudecode) tools.push("claudecode"); if (options.roo) tools.push("roo"); + if (options.geminicli) tools.push("geminicli"); const generateOptions: { verbose?: boolean; diff --git a/src/core/generator.ts b/src/core/generator.ts index 6bb18d838..193d27364 100644 --- a/src/core/generator.ts +++ b/src/core/generator.ts @@ -2,6 +2,7 @@ import { generateClaudecodeConfig } from "../generators/claudecode.js"; import { generateClineConfig } from "../generators/cline.js"; import { generateCopilotConfig } from "../generators/copilot.js"; import { generateCursorConfig } from "../generators/cursor.js"; +import { generateGeminiConfig } from "../generators/geminicli.js"; import { generateRooConfig } from "../generators/roo.js"; import type { Config, GeneratedOutput, ParsedRule, ToolTarget } from "../types/index.js"; import { resolveTargets } from "../utils/index.js"; @@ -64,6 +65,8 @@ async function generateForTool( return await generateClaudecodeConfig(rules, config, baseDir); case "roo": return generateRooConfig(rules, config, baseDir); + case "geminicli": + return generateGeminiConfig(rules, config, baseDir); default: console.warn(`Unknown tool: ${tool}`); return null; diff --git a/src/core/parser.ts b/src/core/parser.ts index 8bfb69f4c..18921c18b 100644 --- a/src/core/parser.ts +++ b/src/core/parser.ts @@ -93,7 +93,7 @@ function validateFrontmatter(data: unknown, filepath: string): void { ); } - const validTargets = ["copilot", "cursor", "cline", "claudecode", "roo", "*"]; + const validTargets = ["copilot", "cursor", "cline", "claudecode", "roo", "geminicli", "*"]; for (const target of obj.targets) { if (typeof target !== "string" || !validTargets.includes(target)) { throw new Error( diff --git a/src/generators/geminicli.test.ts b/src/generators/geminicli.test.ts new file mode 100644 index 000000000..d45f2b0d8 --- /dev/null +++ b/src/generators/geminicli.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from "vitest"; +import type { Config, ParsedRule } from "../types/index.js"; +import { generateGeminiConfig } from "./geminicli.js"; + +const mockConfig: Config = { + aiRulesDir: ".rulesync", + outputPaths: { + copilot: ".github/instructions", + cursor: ".cursor/rules", + cline: ".clinerules", + claudecode: ".", + roo: ".roo/rules", + geminicli: ".gemini/memories", + }, + defaultTargets: ["copilot", "cursor", "cline", "claudecode", "roo", "geminicli"], + watchEnabled: false, +}; + +const mockRules: ParsedRule[] = [ + { + filename: "overview-rule", + filepath: "/path/to/overview-rule.md", + frontmatter: { + targets: ["*"], + root: true, + description: "Overview rule", + globs: ["**/*.ts", "**/*.js"], + }, + content: "This is an overview rule content", + }, + { + filename: "detail-rule", + filepath: "/path/to/detail-rule.md", + frontmatter: { + targets: ["geminicli"], + root: false, + description: "Detail rule", + globs: ["**/*.md"], + }, + content: "This is a detail rule content", + }, +]; + +describe("generateGeminiConfig", () => { + it("should generate GEMINI.md root file and memory files", async () => { + const results = await generateGeminiConfig(mockRules, mockConfig); + + expect(results).toHaveLength(2); + + // Check memory file + expect(results[0].tool).toBe("geminicli"); + expect(results[0].filepath).toBe(".gemini/memories/detail-rule.md"); + expect(results[0].content).not.toContain("# Detail rule"); + expect(results[0].content).toContain("This is a detail rule content"); + + // Check root file + expect(results[1].tool).toBe("geminicli"); + expect(results[1].filepath).toBe("GEMINI.md"); + expect(results[1].content).toContain( + "Please also reference the following documents as needed:" + ); + expect(results[1].content).toContain("| Document | Description | File Patterns |"); + expect(results[1].content).toContain("@.gemini/memories/detail-rule.md"); + expect(results[1].content).toContain("This is an overview rule content"); + }); + + it("should generate table of memory files in root file", async () => { + const results = await generateGeminiConfig(mockRules, mockConfig); + const rootFile = results.find((r) => r.filepath === "GEMINI.md"); + + expect(rootFile?.content).toContain("| Document | Description | File Patterns |"); + expect(rootFile?.content).toContain( + "| @.gemini/memories/detail-rule.md | Detail rule | **/*.md |" + ); + }); + + it("should handle only root rule", async () => { + const rootOnlyRules: ParsedRule[] = [ + { + filename: "root-only", + filepath: "/path/to/root-only.md", + frontmatter: { + targets: ["geminicli"], + root: true, + description: "Root only rule", + globs: ["**/*.ts"], + }, + content: "This is root only content", + }, + ]; + + const results = await generateGeminiConfig(rootOnlyRules, mockConfig); + + expect(results).toHaveLength(1); + expect(results[0].filepath).toBe("GEMINI.md"); + expect(results[0].content).toContain("This is root only content"); + expect(results[0].content).not.toContain( + "Please also reference the following documents as needed:" + ); + }); + + it("should handle only memory rules", async () => { + const memoryOnlyRules: ParsedRule[] = [ + { + filename: "memory-rule", + filepath: "/path/to/memory-rule.md", + frontmatter: { + targets: ["geminicli"], + root: false, + description: "Memory rule", + globs: ["**/*.ts"], + }, + content: "This is memory rule content", + }, + ]; + + const results = await generateGeminiConfig(memoryOnlyRules, mockConfig); + + expect(results).toHaveLength(2); + + // Memory file + expect(results[0].filepath).toBe(".gemini/memories/memory-rule.md"); + expect(results[0].content).not.toContain("# Memory rule"); + + // Root file + expect(results[1].filepath).toBe("GEMINI.md"); + expect(results[1].content).toContain( + "Please also reference the following documents as needed:" + ); + expect(results[1].content).toContain("| Document | Description | File Patterns |"); + expect(results[1].content).not.toContain("This is memory rule content"); + }); + + it("should handle empty rules array", async () => { + const results = await generateGeminiConfig([], mockConfig); + + expect(results).toHaveLength(1); + expect(results[0].filepath).toBe("GEMINI.md"); + expect(results[0].content).toContain("No configuration rules have been defined yet"); + }); + + it("should use baseDir when provided", async () => { + const results = await generateGeminiConfig(mockRules, mockConfig, "/custom/base"); + + expect(results).toHaveLength(2); + expect(results[0].filepath).toBe("/custom/base/.gemini/memories/detail-rule.md"); + expect(results[1].filepath).toBe("/custom/base/GEMINI.md"); + }); +}); diff --git a/src/generators/geminicli.ts b/src/generators/geminicli.ts new file mode 100644 index 000000000..b629b0e93 --- /dev/null +++ b/src/generators/geminicli.ts @@ -0,0 +1,83 @@ +import { join } from "node:path"; +import type { Config, GeneratedOutput, ParsedRule } from "../types/index.js"; + +export async function generateGeminiConfig( + rules: ParsedRule[], + config: Config, + baseDir?: string +): Promise { + const outputs: GeneratedOutput[] = []; + + // Find root and non-root rules + const rootRule = rules.find((rule) => rule.frontmatter.root === true); + const memoryRules = rules.filter((rule) => rule.frontmatter.root === false); + + // Generate memory files first (root=false files) + for (const rule of memoryRules) { + const content = generateGeminiMemoryMarkdown(rule); + const outputDir = baseDir + ? join(baseDir, config.outputPaths.geminicli) + : config.outputPaths.geminicli; + const filepath = join(outputDir, `${rule.filename}.md`); + + outputs.push({ + tool: "geminicli", + filepath, + content, + }); + } + + // Generate root GEMINI.md file + const rootContent = generateGeminiRootMarkdown(rootRule, memoryRules, baseDir); + const rootFilepath = baseDir ? join(baseDir, "GEMINI.md") : "GEMINI.md"; + + outputs.push({ + tool: "geminicli", + filepath: rootFilepath, + content: rootContent, + }); + + return outputs; +} + +function generateGeminiMemoryMarkdown(rule: ParsedRule): string { + // Just return the content without description header and trim leading whitespace + return rule.content.trim(); +} + +function generateGeminiRootMarkdown( + rootRule: ParsedRule | undefined, + memoryRules: ParsedRule[], + _baseDir?: string +): string { + const lines: string[] = []; + + // Start with CLAUDE.md style introduction if memory files exist + if (memoryRules.length > 0) { + lines.push("Please also reference the following documents as needed:"); + lines.push(""); + lines.push("| Document | Description | File Patterns |"); + lines.push("|----------|-------------|---------------|"); + + for (const rule of memoryRules) { + const relativePath = `@.gemini/memories/${rule.filename}.md`; + const filePatterns = + rule.frontmatter.globs.length > 0 ? rule.frontmatter.globs.join(", ") : "-"; + lines.push(`| ${relativePath} | ${rule.frontmatter.description} | ${filePatterns} |`); + } + lines.push(""); + lines.push(""); + } + + // Add root rule content if available + if (rootRule) { + lines.push(rootRule.content.trim()); + } else if (memoryRules.length === 0) { + // Fallback if no rules are provided + lines.push("# Gemini CLI Configuration"); + lines.push(""); + lines.push("No configuration rules have been defined yet."); + } + + return lines.join("\n"); +} diff --git a/src/generators/index.ts b/src/generators/index.ts index 7025afd10..1e13c0007 100644 --- a/src/generators/index.ts +++ b/src/generators/index.ts @@ -2,4 +2,5 @@ export * from "./claudecode.js"; export * from "./cline.js"; export * from "./copilot.js"; export * from "./cursor.js"; +export * from "./geminicli.js"; export * from "./roo.js"; diff --git a/src/types/rules.ts b/src/types/rules.ts index b8bf74e5f..c1bad42ce 100644 --- a/src/types/rules.ts +++ b/src/types/rules.ts @@ -1,4 +1,4 @@ -export type ToolTarget = "copilot" | "cursor" | "cline" | "claudecode" | "roo"; +export type ToolTarget = "copilot" | "cursor" | "cline" | "claudecode" | "roo" | "geminicli"; export type RuleFrontmatter = { root: boolean; diff --git a/src/utils/config.test.ts b/src/utils/config.test.ts index eafe57c2c..2de45db61 100644 --- a/src/utils/config.test.ts +++ b/src/utils/config.test.ts @@ -11,7 +11,15 @@ describe("config utils", () => { expect(config.outputPaths.cursor).toBe(".cursor/rules"); expect(config.outputPaths.cline).toBe(".clinerules"); expect(config.outputPaths.claudecode).toBe("."); - expect(config.defaultTargets).toEqual(["copilot", "cursor", "cline", "claudecode", "roo"]); + expect(config.outputPaths.geminicli).toBe(".gemini/memories"); + expect(config.defaultTargets).toEqual([ + "copilot", + "cursor", + "cline", + "claudecode", + "roo", + "geminicli", + ]); expect(config.watchEnabled).toBe(false); }); }); @@ -21,7 +29,7 @@ describe("config utils", () => { it("should resolve * to all default targets", () => { const targets = resolveTargets(["*"], config); - expect(targets).toEqual(["copilot", "cursor", "cline", "claudecode", "roo"]); + expect(targets).toEqual(["copilot", "cursor", "cline", "claudecode", "roo", "geminicli"]); }); it("should return specific targets as-is", () => { diff --git a/src/utils/config.ts b/src/utils/config.ts index 9d01a9c8d..e4bd25974 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -9,9 +9,10 @@ export function getDefaultConfig(): Config { cline: ".clinerules", claudecode: ".", roo: ".roo/rules", + geminicli: ".gemini/memories", }, watchEnabled: false, - defaultTargets: ["copilot", "cursor", "cline", "claudecode", "roo"], + defaultTargets: ["copilot", "cursor", "cline", "claudecode", "roo", "geminicli"], }; }