Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ package-lock.json

.pnpm-store

/.issue-*.md
/.pr-*.diff

.rulesync/rules/my-instructions.md

.cspellcache
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ See [Quick Start guide](https://dyoshikawa.github.io/rulesync/getting-started/qu
| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 |
| Goose | goose | ✅ 🌏 | ✅ | | | | | |
| GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ |
| GitHub Copilot CLI | copilotcli | | | ✅ 🌏 | | | | |
| GitHub Copilot CLI | copilotcli | ✅ 🌏 | | ✅ 🌏 | | | | |
| Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ |
| deepagents-cli | deepagents | ✅ | | ✅ 🌏 | | ✅ | ✅ | 🌏 |
| Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | 🎮 | 🎮 | 🎮 | ✅ 🌏 |
Expand Down
11 changes: 10 additions & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ Example:
"type": "stdio",
"command": "uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]
},
"github": {
"type": "http",
"url": "http://localhost:3000/mcp"
},
"local-dev": {
"type": "local",
"command": "node",
"args": ["scripts/start-local-mcp.js"]
}
}
}
Expand All @@ -74,7 +83,7 @@ This file is used by the GitHub Copilot CLI for MCP server configuration. Rulesy
- **Project mode:** `.copilot/mcp-config.json` (relative to project root)
- **Global mode:** `~/.copilot/mcp-config.json` (relative to home directory)

Rulesync ensures that each server entry contains the mandatory `"type": "stdio"` field required by the Copilot CLI.
Rulesync preserves explicit `type` values for `http`, `sse`, and `local` servers. For command-based servers that omit a transport type, Rulesync emits the mandatory `"type": "stdio"` field required by the Copilot CLI.

Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude-only, `opencode.hooks` for OpenCode-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time).

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Codex CLI | codexcli | ✅ 🌏 | | ✅ 🌏 🔧 | 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 |
| GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ |
| GitHub Copilot CLI | copilotcli | | | ✅ 🌏 | | | | |
| GitHub Copilot CLI | copilotcli | ✅ 🌏 | | ✅ 🌏 | | | | |
| Goose | goose | ✅ 🌏 | ✅ | | | | | |
| Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ |
| Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | 🎮 | 🎮 | 🎮 | ✅ 🌏 |
Expand Down
11 changes: 10 additions & 1 deletion skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ Example:
"type": "stdio",
"command": "uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]
},
"github": {
"type": "http",
"url": "http://localhost:3000/mcp"
},
"local-dev": {
"type": "local",
"command": "node",
"args": ["scripts/start-local-mcp.js"]
}
}
}
Expand All @@ -74,7 +83,7 @@ This file is used by the GitHub Copilot CLI for MCP server configuration. Rulesy
- **Project mode:** `.copilot/mcp-config.json` (relative to project root)
- **Global mode:** `~/.copilot/mcp-config.json` (relative to home directory)

Rulesync ensures that each server entry contains the mandatory `"type": "stdio"` field required by the Copilot CLI.
Rulesync preserves explicit `type` values for `http`, `sse`, and `local` servers. For command-based servers that omit a transport type, Rulesync emits the mandatory `"type": "stdio"` field required by the Copilot CLI.

Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude-only, `opencode.hooks` for OpenCode-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time).

Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Codex CLI | codexcli | ✅ 🌏 | | ✅ 🌏 🔧 | 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 |
| GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ |
| GitHub Copilot CLI | copilotcli | | | ✅ 🌏 | | | | |
| GitHub Copilot CLI | copilotcli | ✅ 🌏 | | ✅ 🌏 | | | | |
| Goose | goose | ✅ 🌏 | ✅ | | | | | |
| Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ |
| Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | 🎮 | 🎮 | 🎮 | ✅ 🌏 |
Expand Down
33 changes: 30 additions & 3 deletions src/cli/commands/gitignore-entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ describe("GITIGNORE_ENTRY_REGISTRY", () => {
});

it("should cover all tool targets except intentionally excluded ones", () => {
const registeredTargets = new Set(GITIGNORE_ENTRY_REGISTRY.map((tag) => tag.target));
const registeredTargets = new Set(
GITIGNORE_ENTRY_REGISTRY.flatMap((tag) =>
Array.isArray(tag.target) ? tag.target : [tag.target],
),
);
for (const target of ALL_TOOL_TARGETS) {
if (TARGETS_WITHOUT_GITIGNORE_ENTRIES.has(target)) {
expect(registeredTargets).not.toContain(target);
Expand Down Expand Up @@ -89,6 +93,15 @@ describe("filterGitignoreEntries", () => {
expect(result).not.toContain("**/.cursor/");
});

it("should include shared copilot rule entries for copilotcli target", () => {
const result = filterGitignoreEntries({ logger, targets: ["copilotcli"] });

expect(result).toContain("**/.github/copilot-instructions.md");
expect(result).toContain("**/.github/instructions/");
expect(result).toContain("**/.copilot/mcp-config.json");
expect(result).not.toContain("**/.github/prompts/");
});

it("should return all entries when target is wildcard", () => {
const result = filterGitignoreEntries({ logger, targets: ["*"] });
expect(result).toEqual([...ALL_GITIGNORE_ENTRIES]);
Expand Down Expand Up @@ -189,8 +202,9 @@ describe("filterGitignoreEntries", () => {
// copilot commands
expect(result).toContain("**/.github/prompts/");

// copilot has ["commands"] so copilot rules should NOT be included
expect(result).not.toContain("**/.github/copilot-instructions.md");
// Shared copilot/copilotcli rule entries stay included because copilotcli
// is not restricted in this per-target feature map.
expect(result).toContain("**/.github/copilot-instructions.md");

// claudecode commands should NOT be included
expect(result).not.toContain("**/.claude/commands/");
Expand Down Expand Up @@ -233,6 +247,19 @@ describe("filterGitignoreEntries", () => {
expect(result).not.toContain("**/.github/copilot-instructions.md");
expect(result).not.toContain("**/.cursor/");
});

it("should include shared entries when copilotcli enables matching features", () => {
const result = filterGitignoreEntries({
features: {
copilot: ["commands"],
copilotcli: ["rules"],
},
});

expect(result).toContain("**/.github/copilot-instructions.md");
expect(result).toContain("**/.github/instructions/");
expect(result).toContain("**/.github/prompts/");
});
});

describe("validation warnings", () => {
Expand Down
55 changes: 47 additions & 8 deletions src/cli/commands/gitignore-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ import {
import { ALL_TOOL_TARGETS_WITH_WILDCARD, type ToolTarget } from "../../types/tool-targets.js";
import type { Logger } from "../../utils/logger.js";

type GitignoreEntryTarget = ToolTarget | "common";

export type GitignoreEntryTag = {
readonly target: ToolTarget | "common";
readonly target: GitignoreEntryTarget | ReadonlyArray<GitignoreEntryTarget>;
readonly feature: Feature | "general";
readonly entry: string;
};

const normalizeGitignoreEntryTargets = (
target: GitignoreEntryTag["target"],
): ReadonlyArray<GitignoreEntryTarget> => {
return typeof target === "string" ? [target] : target;
};

export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [
// Common / general
{
Expand Down Expand Up @@ -126,11 +134,15 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [

// GitHub Copilot
{
target: "copilot",
target: ["copilot", "copilotcli"],
feature: "rules",
entry: "**/.github/copilot-instructions.md",
},
{ target: "copilot", feature: "rules", entry: "**/.github/instructions/" },
{
target: ["copilot", "copilotcli"],
feature: "rules",
entry: "**/.github/instructions/",
},
{ target: "copilot", feature: "commands", entry: "**/.github/prompts/" },
{ target: "copilot", feature: "subagents", entry: "**/.github/agents/" },
{ target: "copilot", feature: "skills", entry: "**/.github/skills/" },
Expand Down Expand Up @@ -216,16 +228,32 @@ type FilterGitignoreEntriesParams = {
};

const isTargetSelected = (
target: ToolTarget | "common",
target: GitignoreEntryTag["target"],
selectedTargets: ReadonlyArray<string> | undefined,
): boolean => {
if (target === "common") return true;
const targets = normalizeGitignoreEntryTargets(target);

if (targets.includes("common")) return true;
if (!selectedTargets || selectedTargets.length === 0) return true;
if (selectedTargets.includes("*")) return true;
return selectedTargets.includes(target);
return targets.some((candidate) => selectedTargets.includes(candidate));
};

const isFeatureSelected = (
const getSelectedGitignoreEntryTargets = (
target: GitignoreEntryTag["target"],
selectedTargets: ReadonlyArray<string> | undefined,
): ReadonlyArray<GitignoreEntryTarget> => {
const targets = normalizeGitignoreEntryTargets(target);

if (targets.includes("common")) return ["common"];
if (!selectedTargets || selectedTargets.length === 0 || selectedTargets.includes("*")) {
return targets;
}

return targets.filter((candidate) => selectedTargets.includes(candidate));
};

const isFeatureSelectedForTarget = (
feature: Feature | "general",
target: ToolTarget | "common",
features: RulesyncFeatures | undefined,
Expand All @@ -251,6 +279,16 @@ const isFeatureSelected = (
return targetFeatures.includes(feature);
};

const isFeatureSelected = (
feature: Feature | "general",
target: GitignoreEntryTag["target"],
features: RulesyncFeatures | undefined,
): boolean => {
return normalizeGitignoreEntryTargets(target).some((candidate) =>
isFeatureSelectedForTarget(feature, candidate, features),
);
};

const warnInvalidTargets = (targets: ReadonlyArray<string>, logger?: Logger): void => {
const validTargets = new Set<string>(ALL_TOOL_TARGETS_WITH_WILDCARD);
for (const target of targets) {
Expand Down Expand Up @@ -304,7 +342,8 @@ export const filterGitignoreEntries = (

for (const tag of GITIGNORE_ENTRY_REGISTRY) {
if (!isTargetSelected(tag.target, targets)) continue;
if (!isFeatureSelected(tag.feature, tag.target, features)) continue;
const selectedTagTargets = getSelectedGitignoreEntryTargets(tag.target, targets);
if (!isFeatureSelected(tag.feature, selectedTagTargets, features)) continue;
if (seen.has(tag.entry)) continue;
seen.add(tag.entry);
result.push(tag.entry);
Expand Down
Loading
Loading