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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The tables below show whether each tool supports a given feature (✅ = supporte
| Roo Code | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | |
| Rovodev (Atlassian) | ✅ | | ✅ | | ✅ | ✅ | | |
| Takt | ✅ | | | ✅ | ✅ | ✅ | | |
| Vibe Code | ✅ | ✅ | ✅ | | ✅ | ✅ | | ✅ |
| Qwen Code | ✅ | ✅ | | | | | | ✅ |
| Kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Google Antigravity IDE | ✅ | | ✅ | ✅ | | ✅ | ✅ | ✅ |
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"deepwiki",
"depd",
"devinignore",
"devstral",
"dnsutils",
"donotpresent",
"dotenv",
Expand Down Expand Up @@ -303,6 +304,7 @@
"USERPROFILE",
"uvicorn",
"venv",
"vibeignore",
"vite",
"vuepress",
"weakmap",
Expand Down
1 change: 1 addition & 0 deletions docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Roo Code | roo | ✅ | ✅ | ✅ | ✅ | 🎮 | ✅ 🌏 | | |
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
| Vibe Code | vibe | ✅ 🌏 | ✅ | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 |
| Qwen Code | qwencode | ✅ | ✅ | | | | | | ✅ 🌏 |
| Kiro | kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Google Antigravity IDE | antigravity-ide | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ |
Expand Down
1 change: 1 addition & 0 deletions skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Roo Code | roo | ✅ | ✅ | ✅ | ✅ | 🎮 | ✅ 🌏 | | |
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
| Vibe Code | vibe | ✅ 🌏 | ✅ | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 |
| Qwen Code | qwencode | ✅ | ✅ | | | | | | ✅ 🌏 |
| Kiro | kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Google Antigravity IDE | antigravity-ide | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ |
Expand Down
9 changes: 8 additions & 1 deletion src/cli/commands/gitignore-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [

// AGENTS.md
{
target: ["agentsmd", "amp", "antigravity-ide", "pi", "warp"],
target: ["agentsmd", "amp", "antigravity-ide", "pi", "vibe", "warp"],
feature: "rules",
entry: "**/AGENTS.md",
},
Expand Down Expand Up @@ -335,6 +335,13 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [
{ target: "devin", feature: "skills", entry: "**/.devin/skills/" },
{ target: "devin", feature: "skills", entry: "**/.codeium/windsurf/skills/" },

// Vibe
{ target: "vibe", feature: "ignore", entry: "**/.vibeignore" },
{ target: "vibe", feature: "skills", entry: "**/.vibe/skills/" },
{ target: "vibe", feature: "subagents", entry: "**/.vibe/agents/" },
{ target: "vibe", feature: "mcp", entry: "**/.vibe/config.toml" },
{ target: "vibe", feature: "permissions", entry: "**/.vibe/config.toml" },

// Warp
// `/init` now writes `AGENTS.md` (handled by the shared AGENTS.md entry above).
{ target: "warp", feature: "rules", entry: "**/.warp/" },
Expand Down
3 changes: 3 additions & 0 deletions src/e2e/e2e-ignore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("E2E: ignore", () => {
outputPath: join(".zed", "settings.json"),
format: "json" as const,
},
{ target: "vibe", outputPath: ".vibeignore", format: "plaintext" as const },
])("should generate $target ignore", async ({ target, outputPath, format }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -81,6 +82,7 @@ credentials/
{ target: "junie", orphanPath: ".aiignore" },
{ target: "augmentcode", orphanPath: ".augmentignore" },
{ target: "devin", orphanPath: ".devinignore" },
{ target: "vibe", orphanPath: ".vibeignore" },
// zed ignore uses .zed/settings.json which is not deletable by rulesync
])(
"should fail in check mode when delete would remove an orphan $target ignore file",
Expand Down Expand Up @@ -146,6 +148,7 @@ describe("E2E: ignore (import)", () => {
{ target: "junie", sourcePath: ".aiignore" },
{ target: "augmentcode", sourcePath: ".augmentignore" },
{ target: "devin", sourcePath: ".devinignore" },
{ target: "vibe", sourcePath: ".vibeignore" },
])("should import $target ignore", async ({ target, sourcePath }) => {
const testDir = getTestDir();

Expand Down
93 changes: 93 additions & 0 deletions src/e2e/e2e-mcp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
import { join } from "node:path";
import { setTimeout } from "node:timers/promises";

import * as smolToml from "smol-toml";
import { describe, expect, it } from "vitest";

import { RULESYNC_MCP_RELATIVE_FILE_PATH } from "../constants/rulesync-paths.js";
Expand Down Expand Up @@ -38,6 +39,7 @@ describe("E2E: mcp", () => {
{ target: "warp", outputPath: join(".warp", ".mcp.json") },
{ target: "zed", outputPath: join(".zed", "settings.json") },
{ target: "devin", outputPath: join(".windsurf", "mcp_config.json") },
{ target: "vibe", outputPath: join(".vibe", "config.toml") },
])("should generate $target mcp", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -141,6 +143,11 @@ describe("E2E: mcp", () => {
outputPath: "kilo.jsonc",
content: JSON.stringify({ theme: "dark", mcp: {} }, null, 2),
},
{
target: "vibe",
outputPath: join(".vibe", "config.toml"),
content: 'theme = "dark"\n',
},
])(
"should succeed in check mode when a $target mcp file is non-deletable",
async ({ target, outputPath, content }) => {
Expand Down Expand Up @@ -197,6 +204,54 @@ describe("E2E: mcp", () => {
// Verify that there were no actual errors (warnings are acceptable)
expect(hasError, `MCP daemon produced errors: ${stderrOutput}`).toBe(false);
});

it("should generate Vibe MCP and permissions into shared config.toml", async () => {
const testDir = getTestDir();

await writeFileContent(
join(testDir, RULESYNC_MCP_RELATIVE_FILE_PATH),
JSON.stringify(
{
mcpServers: {
"test-server": {
type: "stdio",
command: "echo",
args: ["hello"],
},
},
},
null,
2,
),
);
await writeFileContent(
join(testDir, ".rulesync", "permissions.json"),
JSON.stringify(
{
permission: {
bash: { "*": "ask", "git *": "allow" },
edit: { "*": "deny" },
},
},
null,
2,
),
);

await runGenerate({ target: "vibe", features: "mcp,permissions" });

const parsed = toTable(
smolToml.parse(await readFileContent(join(testDir, ".vibe", "config.toml"))),
);
const tools = toTable(parsed.tools);
const bash = toTable(tools.bash);
expect(toTableArray(parsed.mcp_servers)).toMatchObject([
{ name: "test-server", command: "echo" },
]);
expect(bash.permission).toBe("ask");
expect(bash.allow).toEqual(["git *"]);
expect(parsed.disabled_tools).toContain("write_file");
});
});

describe("E2E: mcp (import)", () => {
Expand Down Expand Up @@ -259,6 +314,29 @@ describe("E2E: mcp (import)", () => {
expect(importedContent).toContain("test-server");
});

it("should import vibe mcp from config.toml", async () => {
const testDir = getTestDir();

await writeFileContent(
join(testDir, ".vibe", "config.toml"),
[
'theme = "dark"',
"",
"[[mcp_servers]]",
'name = "test-server"',
'transport = "stdio"',
'command = "echo"',
'args = ["hello"]',
].join("\n"),
);

await runImport({ target: "vibe", features: "mcp" });

const importedContent = await readFileContent(join(testDir, RULESYNC_MCP_RELATIVE_FILE_PATH));
expect(importedContent).toContain("test-server");
expect(importedContent).toContain("hello");
});

// Zed stores MCP servers under `context_servers` (not `mcpServers`) inside a
// shared settings.json, so it needs a bespoke source rather than the generic
// `mcpServers`-seeded import case above.
Expand Down Expand Up @@ -320,6 +398,7 @@ describe("E2E: mcp (global mode)", () => {
target: "devin",
outputPath: join(".codeium", "windsurf", "mcp_config.json"),
},
{ target: "vibe", outputPath: join(".vibe", "config.toml") },
])("should generate $target mcp in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down Expand Up @@ -481,3 +560,17 @@ describe("E2E: mcp (global mode)", () => {
expect(generatedContent).not.toContain("non-root-server");
});
});

function toTable(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) {
return { ...value };
}
return {};
}

function toTableArray(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) {
return [];
}
return value.map(toTable);
}
111 changes: 111 additions & 0 deletions src/e2e/e2e-permissions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,46 @@ describe("E2E: permissions", () => {
expect(content.permissions.deny).toContain("Read(.env)");
});

it("should generate vibe permissions into .vibe/config.toml and preserve MCP config", async () => {
const testDir = getTestDir();

await writeFileContent(
join(testDir, ".vibe", "config.toml"),
["[[mcp_servers]]", 'name = "existing"', 'transport = "stdio"', 'command = "node"'].join(
"\n",
),
);
await writeFileContent(
join(testDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH),
JSON.stringify(
{
permission: {
bash: { "*": "ask", "git *": "allow", "rm *": "deny" },
read: { "*": "allow" },
edit: { "*": "deny" },
},
},
null,
2,
),
);

await runGenerate({ target: "vibe", features: "permissions" });

const parsed = toTable(
smolToml.parse(await readFileContent(join(testDir, ".vibe", "config.toml"))),
);
const tools = toTable(parsed.tools);
const bash = toTable(tools.bash);
const readFile = toTable(tools.read_file);
expect(toTableArray(parsed.mcp_servers)).toMatchObject([{ name: "existing", command: "node" }]);
expect(bash.permission).toBe("ask");
expect(bash.allow).toEqual(["git *"]);
expect(bash.deny).toEqual(["rm *"]);
expect(readFile.permission).toBe("always");
expect(parsed.disabled_tools).toContain("write_file");
});

it("should remove denied Kiro web tools from existing allowedTools", async () => {
const testDir = getTestDir();

Expand Down Expand Up @@ -788,6 +828,34 @@ enabled = true
expect(content.permission.read[".env"]).toBe("deny");
expect(content.permission.webfetch["*"]).toBe("allow");
});

it("should import vibe permissions into .rulesync/permissions.json", async () => {
const testDir = getTestDir();

await writeFileContent(
join(testDir, ".vibe", "config.toml"),
[
'enabled_tools = ["read_file"]',
'disabled_tools = ["write_file"]',
"",
"[tools.bash]",
'permission = "ask"',
'allow = ["git *"]',
'deny = ["rm *"]',
].join("\n"),
);

await runImport({ target: "vibe", features: "permissions" });

const content = JSON.parse(
await readFileContent(join(testDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH)),
);
expect(content.permission.read["*"]).toBe("allow");
expect(content.permission.edit["*"]).toBe("deny");
expect(content.permission.bash["*"]).toBe("ask");
expect(content.permission.bash["git *"]).toBe("allow");
expect(content.permission.bash["rm *"]).toBe("deny");
});
});

describe("E2E: permissions (global mode)", () => {
Expand Down Expand Up @@ -1247,6 +1315,42 @@ describe("E2E: permissions (global mode)", () => {
// Unrelated user settings preserved by the non-destructive merge.
expect(generated["amp.mcpServers"].my_server.command).toBe("x");
});

it("should generate vibe permissions in home directory with --global", async () => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();

await writeFileContent(
join(projectDir, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH),
JSON.stringify(
{
root: true,
permission: {
bash: { "*": "ask", "git status": "allow" },
edit: { "*": "deny" },
},
},
null,
2,
),
);

await runGenerate({
target: "vibe",
features: "permissions",
global: true,
env: { HOME_DIR: homeDir },
});

const parsed = toTable(
smolToml.parse(await readFileContent(join(homeDir, ".vibe", "config.toml"))),
);
const tools = toTable(parsed.tools);
const bash = toTable(tools.bash);
expect(bash.permission).toBe("ask");
expect(bash.allow).toEqual(["git status"]);
expect(parsed.disabled_tools).toContain("write_file");
});
});

type AugmentEntry = {
Expand Down Expand Up @@ -1289,3 +1393,10 @@ function toTable(value: unknown): Record<string, unknown> {
}
return {};
}

function toTableArray(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) {
return [];
}
return value.map(toTable);
}
3 changes: 3 additions & 0 deletions src/e2e/e2e-rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe("E2E: rules", () => {
{ target: "replit", outputPath: "replit.md" },
{ target: "pi", outputPath: "AGENTS.md" },
{ target: "zed", outputPath: ".rules" },
{ target: "vibe", outputPath: "AGENTS.md" },
])("should generate $target rules", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -256,6 +257,7 @@ describe("E2E: rules (import)", () => {
{ target: "warp", sourcePath: "AGENTS.md", importedFileName: "overview.md" },
{ target: "replit", sourcePath: "replit.md", importedFileName: "overview.md" },
{ target: "pi", sourcePath: "AGENTS.md", importedFileName: "overview.md" },
{ target: "vibe", sourcePath: "AGENTS.md", importedFileName: "overview.md" },
{
target: "cline",
sourcePath: join(".clinerules", "overview.md"),
Expand Down Expand Up @@ -336,6 +338,7 @@ describe("E2E: rules (global mode)", () => {
{ target: "takt", outputPath: join(".takt", "facets", "policies", "overview.md") },
{ target: "pi", outputPath: join(".pi", "agent", "AGENTS.md") },
{ target: "zed", outputPath: join(".config", "zed", "AGENTS.md") },
{ target: "vibe", outputPath: join(".vibe", "AGENTS.md") },
{ target: "augmentcode", outputPath: join(".augment", "rules", "overview.md") },
{
target: "devin",
Expand Down
Loading
Loading