From 56a488cbec0fe5d4bc3af4884c831bf5699ca11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Jerci=C5=84ski?= Date: Mon, 6 Apr 2026 18:59:37 +0200 Subject: [PATCH] feat: add copilot support --- README.md | 15 +++++++++------ src/config/config.test.ts | 2 ++ src/config/loader.createSampleConfig.test.ts | 1 + src/config/loader.ts | 1 + src/core/harness-registry.ts | 8 +++++++- src/core/sync-global.test.ts | 3 +++ src/core/sync.test.ts | 3 +++ 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 63e8815..9a714cd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A CLI tool to synchronize AI coding assistant rule files between a central repos ## What is this tool for? -Many AI coding assistants (Claude Code, Gemini CLI, OpenCode, Codex CLI) read a local rules file to understand context, coding standards, and specific instructions. +Many AI coding assistants (Claude Code, Gemini CLI, OpenCode, Codex CLI, GitHub Copilot) read local instruction files to understand context, coding standards, and specific instructions. Managing these rules across numerous projects can be tedious. `sync-rules` lets you maintain a **single, centralized directory** of rules and automatically synchronize the relevant subsets into your projects using a single standard file. @@ -14,7 +14,7 @@ Managing these rules across numerous projects can be tedious. `sync-rules` lets - **Centralized Rule Management:** Keep all your AI guidelines in one place. - **Project-Specific Configuration:** Use flexible glob patterns to define exactly which rules apply to which projects. -- **Single Standard File:** Always generates `AGENTS.md` with all selected rules and writes a `CLAUDE.md` file containing `@AGENTS.md` for Claude Code. +- **Single Standard File:** Always generates `AGENTS.md` with all selected rules and writes a `CLAUDE.md` file containing `@AGENTS.md` for Claude Code. Copilot CLI reads the synced `AGENTS.md` directly. // Seamless integration via shell: chain with your tool, e.g. `sync-rules && claude --chat`. ## Installation @@ -63,7 +63,8 @@ Edit the `config.json` file to define your setup. "global": ["global-rules/*.md"], "globalOverrides": { "claude": ["claude-specific/*.md"], - "codex": ["codex-specific/*.md"] + "codex": ["codex-specific/*.md"], + "copilot": ["copilot-specific/*.md"] }, "projects": [ { @@ -79,8 +80,8 @@ Edit the `config.json` file to define your setup. ``` - `rulesSource`: The central directory where you store your rule files (e.g., Markdown files). If omitted, it defaults to the system's data directory. -- `global`: Optional POSIX globs for rules that are combined and written to built-in global target files for supported tools (e.g., `~/.claude/CLAUDE.md`, `~/.gemini/AGENTS.md`, `~/.config/opencode/AGENTS.md`, `~/.codex/AGENTS.md`). -- `globalOverrides`: Optional per-harness override globs. Each key is a harness name (`claude`, `gemini`, `opencode`, `codex`) with its own glob patterns. Override rules are appended after the shared `global` rules for that harness only. A rule file must not appear in both `global` and an override for the same harness. +- `global`: Optional POSIX globs for rules that are combined and written to built-in global target files for supported tools (e.g., `~/.claude/CLAUDE.md`, `~/.gemini/AGENTS.md`, `~/.config/opencode/AGENTS.md`, `~/.codex/AGENTS.md`, `~/.copilot/copilot-instructions.md`). +- `globalOverrides`: Optional per-harness override globs. Each key is a harness name (`claude`, `gemini`, `opencode`, `codex`, `copilot`) with its own glob patterns. Override rules are appended after the shared `global` rules for that harness only. A rule file must not appear in both `global` and an override for the same harness. - `projects`: Optional array defining each project. Can be omitted for a globals-only config. - `path`: The root directory of the project (supports `~` for home directory). - `rules`: POSIX-style glob patterns to select files from `rulesSource`. Supports negation (`!`). @@ -125,9 +126,11 @@ Tip: define a small shell function to forward args cleanly: ## Output Files -- `AGENTS.md`: Canonical rules file read by Codex CLI, Gemini CLI, and OpenCode. +- `AGENTS.md`: Canonical rules file read by Codex CLI, Gemini CLI, OpenCode, and GitHub Copilot CLI. - `CLAUDE.md`: A tiny include file with `@AGENTS.md` (Claude Code supported syntax). +> **Note:** The built-in global `copilot` target writes Copilot CLI's user-level instructions file at `~/.copilot/copilot-instructions.md`. + ## Pipeline Examples The CLI follows Unix philosophy—silent success, machine-readable output modes, and composability with standard tools. diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 48aba15..fe620ef 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -150,6 +150,7 @@ describe("config", () => { gemini: ["gemini/*.md"], opencode: ["opencode/*.md"], codex: ["codex/*.md"], + copilot: ["copilot/*.md"], }, }); const config = parseConfig(json); @@ -158,6 +159,7 @@ describe("config", () => { "gemini", "opencode", "codex", + "copilot", ]); }); }); diff --git a/src/config/loader.createSampleConfig.test.ts b/src/config/loader.createSampleConfig.test.ts index c47df3f..c8c314d 100644 --- a/src/config/loader.createSampleConfig.test.ts +++ b/src/config/loader.createSampleConfig.test.ts @@ -30,6 +30,7 @@ describe("createSampleConfig", () => { expect(content).toContain('"globalOverrides"'); expect(content).toContain('"claude"'); expect(content).toContain('"codex"'); + expect(content).toContain('"copilot"'); expect(content).toContain('"projects"'); }); diff --git a/src/config/loader.ts b/src/config/loader.ts index 57a2ae9..7a897df 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -21,6 +21,7 @@ const SAMPLE_CONFIG = { globalOverrides: { claude: ["claude-specific/*.md"], codex: ["codex-specific/*.md"], + copilot: ["copilot-specific/*.md"], }, projects: [ { diff --git a/src/core/harness-registry.ts b/src/core/harness-registry.ts index b6d0ae3..2e7d47a 100644 --- a/src/core/harness-registry.ts +++ b/src/core/harness-registry.ts @@ -5,7 +5,12 @@ * The registry is the single source of truth for valid harness names and target paths. */ -export type HarnessName = "claude" | "gemini" | "opencode" | "codex"; +export type HarnessName = + | "claude" + | "gemini" + | "opencode" + | "codex" + | "copilot"; type HarnessEntry = { readonly target: string; @@ -16,6 +21,7 @@ export const HARNESS_REGISTRY: Record = { gemini: { target: "~/.gemini/AGENTS.md" }, opencode: { target: "~/.config/opencode/AGENTS.md" }, codex: { target: "~/.codex/AGENTS.md" }, + copilot: { target: "~/.copilot/copilot-instructions.md" }, }; export const HARNESS_NAMES = Object.keys(HARNESS_REGISTRY) as HarnessName[]; diff --git a/src/core/sync-global.test.ts b/src/core/sync-global.test.ts index 3156e90..736045e 100644 --- a/src/core/sync-global.test.ts +++ b/src/core/sync-global.test.ts @@ -77,6 +77,9 @@ describe("sync-global", () => { const paths = actionsArgument.map((action) => action.path); expect(paths.some((p) => p.endsWith("/.claude/CLAUDE.md"))).toBe(true); expect(paths.some((p) => p.endsWith("/.codex/AGENTS.md"))).toBe(true); + expect( + paths.some((p) => p.endsWith("/.copilot/copilot-instructions.md")), + ).toBe(true); expect( actionsArgument.every( (action) => action.content === "# G1\nA\n\n# G2\nB", diff --git a/src/core/sync.test.ts b/src/core/sync.test.ts index 9b9f9cc..d5f9bc3 100644 --- a/src/core/sync.test.ts +++ b/src/core/sync.test.ts @@ -36,6 +36,9 @@ describe("sync", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(fsPromises.lstat).mockRejectedValue( + Object.assign(new Error("ENOENT"), { code: "ENOENT" }), + ); }); it("writes AGENTS.md with concatenated rules", async () => {