Skip to content
Open
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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ vibe-rules load my-rule-name cursor -t ./my-project/.cursor-rules/custom-rule.md
Arguments:

- `<name>`: The name of the rule saved in the local store (`~/.vibe-rules/rules/`).
- `<editor>`: The target editor/tool type. Supported: `cursor`, `windsurf`, `claude-code`, `gemini`, `codex`, `clinerules`, `roo`, `vscode`.
- `<editor>`: The target editor/tool type. Supported: `cursor`, `windsurf`, `claude-code`, `gemini`, `codex`, `clinerules`, `roo`, `vscode`, `kiro`.

Options:

Expand All @@ -126,7 +126,7 @@ vibe-rules convert vscode unified .github/instructions --global

Arguments:

- `<sourceFormat>`: Source editor format (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode).
- `<sourceFormat>`: Source editor format (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode, kiro).
- `<targetFormat>`: Target editor format (same options as source).
- `<sourcePath>`: Path to source directory (like `.cursor`) or file (like `CLAUDE.md`).

Expand Down Expand Up @@ -163,7 +163,7 @@ vibe-rules convert cursor claude-code .cursor --target my-claude.md

**Supported conversions:**

- **Directory-based**: `cursor` ↔ `clinerules` ↔ `vscode` (individual files)
- **Directory-based**: `cursor` ↔ `clinerules` ↔ `vscode` ↔ `kiro` (individual files)
- **File-based**: `windsurf` ↔ `claude-code` ↔ `codex` ↔ `amp` ↔ `zed` ↔ `unified` (tagged blocks)
- **Cross-format**: Any format to any other format with automatic metadata preservation

Expand Down Expand Up @@ -193,7 +193,7 @@ Add the `--debug` global option to any `vibe-rules` command to enable detailed d

Arguments:

- `<editor>`: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `gemini`, `codex`, `clinerules`, `roo`, `vscode`.
- `<editor>`: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `gemini`, `codex`, `clinerules`, `roo`, `vscode`, `kiro`.
- `[packageName]` (Optional): The specific NPM package name to install rules from. If omitted, `vibe-rules` scans all dependencies and devDependencies in your project's `package.json`.

Options:
Expand Down Expand Up @@ -236,7 +236,7 @@ vibe-rules uninstall my-package_my-rule vscode
Arguments:

- `<name>`: The fully-qualified rule name as applied (typically `<package>_<rule>`)
- `<editor>`: Target editor type. Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `amp`, `clinerules`, `roo`, `zed`, `unified`, `vscode`.
- `<editor>`: Target editor type. Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `amp`, `clinerules`, `roo`, `zed`, `unified`, `vscode`, `kiro`.

Options:

Expand Down Expand Up @@ -272,6 +272,9 @@ Options:
- Supports metadata formatting for `alwaysApply` and `globs` configurations.
- **Cline/Roo (`clinerules`, `roo`)**:
- Creates/updates individual `.md` files within `./.clinerules/` (local) or a target directory specified by `-t`. Global (`-g`) is not typically used.
- **Kiro (`kiro`)**:
- Creates/updates individual `.md` files in `./.kiro/steering/` (local) or `~/.kiro/steering/` (global).
- Uses plain markdown with no frontmatter or metadata formatting.
- **ZED (`zed`)**:
- Manages rules within a single `.rules` file in the project root using XML-like tagged blocks.
- Each rule is encapsulated in tags like `<rule-name>...</rule-name>` without requiring wrapper blocks.
Expand Down
106 changes: 106 additions & 0 deletions examples/end-user-cjs-package/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,112 @@ test("install should create 8 rules files in .github/instructions", async () =>
await $`rm -rf .github`.quiet();
});

test("install should create 8 rules files in .kiro/steering", async () => {
// Import the llms modules from our dependencies
const cjsRules = require("cjs-package/llms");
const esmRules = await import("esm-package/llms");

// Clean up any existing .kiro directory
await $`rm -rf .kiro`.quiet();

// Run npm install
console.log("Running npm install...");
await $`npm install`;

// Run vibe-rules install kiro command
console.log("Running vibe-rules install kiro...");
await $`npm run vibe-rules install kiro`;

// Check that .kiro/steering directory exists
const kiroSteeringPath = join(process.cwd(), ".kiro", "steering");
const kiroSteeringStat = await stat(kiroSteeringPath);
expect(kiroSteeringStat.isDirectory()).toBe(true);

// Read the files in .kiro/steering directory
const rulesFiles = await readdir(kiroSteeringPath);

// Filter only .md files
const mdFiles = rulesFiles.filter((file) => file.endsWith(".md"));

console.log(`Found ${mdFiles.length} rules files:`, mdFiles);

// Expect 8 rules files (4 from cjs-package + 4 from esm-package)
expect(mdFiles.length).toBe(cjsRules.length + esmRules.default.length);

// Get expected rule names from the imported modules
const cjsRuleNames = cjsRules.map((r) => r.name);
const esmRuleNames = esmRules.default.map((r) => r.name);
const expectedRules = [...new Set([...cjsRuleNames, ...esmRuleNames])];

// Check that we have rules from both packages
for (const rule of expectedRules) {
const cjsRuleExists = mdFiles.some((file) => file.includes("cjs") && file.includes(rule));
const esmRuleExists = mdFiles.some((file) => file.includes("esm") && file.includes(rule));

expect(cjsRuleExists).toBe(true);
expect(esmRuleExists).toBe(true);
}

// Validate imported rules structure
console.log("Validating imported rules structure...");

expect(Array.isArray(cjsRules)).toBe(true);
expect(cjsRules.length).toBe(4);

const esmRulesArray = esmRules.default;
expect(Array.isArray(esmRulesArray)).toBe(true);
expect(esmRulesArray.length).toBe(4);

const allRules = [...cjsRules, ...esmRulesArray];
for (const rule of allRules) {
expect(rule).toHaveProperty("name");
expect(rule).toHaveProperty("rule");
expect(rule).toHaveProperty("alwaysApply");

expect(typeof rule.name).toBe("string");
expect(typeof rule.rule).toBe("string");
expect(typeof rule.alwaysApply).toBe("boolean");

if (rule.description !== undefined) {
expect(typeof rule.description).toBe("string");
}
if (rule.globs !== undefined) {
expect(Array.isArray(rule.globs) || typeof rule.globs === "string").toBe(true);
}
}

// Read and validate actual file contents match the imported rules
console.log("Validating file content matches imported rules...");

for (const rule of cjsRules) {
const fileName = `cjs-package_${rule.name}.md`;
expect(mdFiles).toContain(fileName);

const filePath = join(kiroSteeringPath, fileName);
const fileContent = await readFile(filePath, "utf-8");

// Kiro uses plain markdown — content should appear directly without tags or frontmatter
expect(fileContent).toContain(rule.rule);
}

for (const rule of esmRulesArray) {
const fileName = `esm-package_${rule.name}.md`;
expect(mdFiles).toContain(fileName);

const filePath = join(kiroSteeringPath, fileName);
const fileContent = await readFile(filePath, "utf-8");

expect(fileContent).toContain(rule.rule);
}

console.log(
"✅ All Kiro assertions passed! Rules properly installed and match source modules."
);

// Clean up .kiro directory at the end of the test
await $`rm -rf .kiro`.quiet();
});

test("should convert rules from cursor to claude-code format", async () => {
// First, install cursor rules to have something to convert
await $`rm -rf .cursor CLAUDE.md`;
Expand Down
9 changes: 9 additions & 0 deletions examples/end-user-cjs-package/uninstall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,14 @@ export function registerUninstallTests() {
await $`npm run vibe-rules uninstall ${fullName} clinerules`;
expect(await pathExists(`.clinerules/${fullName}.md`)).toBe(false);
await $`rm -rf .clinerules`.quiet();

// Kiro (multi-file .md in .kiro/steering/)
await $`rm -rf .kiro`.quiet();
await $`npm install`;
await $`npm run vibe-rules install kiro`;
expect(await pathExists(`.kiro/steering/${fullName}.md`)).toBe(true);
await $`npm run vibe-rules uninstall ${fullName} kiro`;
expect(await pathExists(`.kiro/steering/${fullName}.md`)).toBe(false);
await $`rm -rf .kiro`.quiet();
}, 60000);
}
18 changes: 9 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ program
.argument("<n>", "Name of the rule to apply")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode)"
"Target editor type (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode, kiro)"
)
.option(
"-g, --global",
"Apply to global config path if supported (claude-code, gemini, codex)",
"Apply to global config path if supported (claude-code, gemini, codex, kiro)",
false
)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
Expand All @@ -73,12 +73,12 @@ program
)
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode)"
"Target editor type (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode, kiro)"
)
.argument("[packageName]", "Optional NPM package name to install rules from")
.option(
"-g, --global",
"Apply to global config path if supported (claude-code, gemini, codex)",
"Apply to global config path if supported (claude-code, gemini, codex, kiro)",
false
)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
Expand All @@ -89,16 +89,16 @@ program
.description("Convert rules from one format to another (directory or file-based)")
.argument(
"<sourceFormat>",
"Source format (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode)"
"Source format (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode, kiro)"
)
.argument(
"<targetFormat>",
"Target format (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode)"
"Target format (cursor, windsurf, claude-code, gemini, codex, amp, clinerules, roo, zed, unified, vscode, kiro)"
)
.argument("<sourcePath>", "Source path (directory like .cursor or file like CLAUDE.md)")
.option(
"-g, --global",
"Apply to global config path if supported (claude-code, gemini, codex)",
"Apply to global config path if supported (claude-code, gemini, codex, kiro)",
false
)
.option("-t, --target <path>", "Custom target path (overrides default path)")
Expand All @@ -110,9 +110,9 @@ program
.argument("<name>", "Name of the rule to remove")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode, kiro)"
)
.option("-g, --global", "Remove from global config path if supported (claude-code, codex)", false)
.option("-g, --global", "Remove from global config path if supported (claude-code, codex, kiro)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(uninstallCommandAction);

Expand Down
30 changes: 30 additions & 0 deletions src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ async function extractRulesFromDirectory(
case RuleType.VSCODE:
return await extractFromVSCodeDirectory(dirPath);

case RuleType.KIRO:
return await extractFromKiroDirectory(dirPath);

default:
throw new Error(`Directory extraction not supported for ${sourceType}`);
}
Expand Down Expand Up @@ -327,6 +330,31 @@ async function extractFromVSCodeFile(filePath: string): Promise<StoredRuleConfig
];
}

/**
* Extract rules from Kiro .md files in a .kiro/steering directory
*/
async function extractFromKiroDirectory(dirPath: string): Promise<StoredRuleConfig[]> {
const rules: StoredRuleConfig[] = [];
const steeringDir = dirPath.endsWith("steering") ? dirPath : path.join(dirPath, "steering");

if (!(await fsExtra.pathExists(steeringDir))) {
throw new Error(`Kiro steering directory not found: ${steeringDir}`);
}

const files = await fs.readdir(steeringDir);
const mdFiles = files.filter((file) => file.endsWith(".md"));

for (const file of mdFiles) {
const filePath = path.join(steeringDir, file);
const content = await fs.readFile(filePath, "utf-8");
const ruleName = path.basename(file, ".md");

rules.push({ name: ruleName, content });
}

return rules;
}

/**
* Extract rules from Windsurf .windsurfrules file using tagged blocks
*/
Expand Down Expand Up @@ -487,6 +515,7 @@ function validateAndGetRuleType(format: string, type: "source" | "target"): Rule
zed: RuleType.ZED,
unified: RuleType.UNIFIED,
vscode: RuleType.VSCODE,
kiro: RuleType.KIRO,
};

const ruleType = formatMap[normalizedFormat];
Expand All @@ -510,6 +539,7 @@ function isMultiFileProvider(ruleType: RuleType): boolean {
case RuleType.CLINERULES:
case RuleType.ROO:
case RuleType.VSCODE:
case RuleType.KIRO:
return true;
default:
return false;
Expand Down
3 changes: 2 additions & 1 deletion src/commands/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function uninstallCommandAction(
console.error(chalk.red(`Unsupported editor: ${editor}`));
console.log(
chalk.gray(
"Supported editors: cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode"
"Supported editors: cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode, kiro"
)
);
process.exit(1);
Expand Down Expand Up @@ -59,6 +59,7 @@ function getRuleTypeFromString(editor: string): RuleType | null {
zed: RuleType.ZED,
unified: RuleType.UNIFIED,
vscode: RuleType.VSCODE,
kiro: RuleType.KIRO,
};

return editorMap[editor.toLowerCase()] || null;
Expand Down
3 changes: 3 additions & 0 deletions src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GeminiRuleProvider } from "./gemini-provider.js";
import { CodexRuleProvider } from "./codex-provider.js";
import { AmpRuleProvider } from "./amp-provider.js";
import { ClinerulesRuleProvider } from "./clinerules-provider.js";
import { KiroRuleProvider } from "./kiro-provider.js";
import { ZedRuleProvider } from "./zed-provider.js";
import { UnifiedRuleProvider } from "./unified-provider.js";
import { VSCodeRuleProvider } from "./vscode-provider.js";
Expand All @@ -30,6 +31,8 @@ export function getRuleProvider(ruleType: RuleType): RuleProvider {
case RuleType.CLINERULES:
case RuleType.ROO:
return new ClinerulesRuleProvider();
case RuleType.KIRO:
return new KiroRuleProvider();
case RuleType.ZED:
return new ZedRuleProvider();
case RuleType.UNIFIED:
Expand Down
Loading