```
├── .cursor/
├── rules/
├── architecture.mdc (omitted)
├── examples.mdc (omitted)
├── good-behaviour.mdc (omitted)
├── vibe-tools.mdc (omitted)
├── yo.mdc (omitted)
├── .cursorindexingignore (omitted)
├── .github/
├── instructions/
├── examples-test.instructions.md (omitted)
├── workflows/
├── ci.yml (omitted)
├── .gitignore (omitted)
├── .npmignore (omitted)
├── .prettierrc (omitted)
├── .repomixignore (omitted)
├── ARCHITECTURE.md (omitted)
├── LICENSE (omitted)
├── README.md (2k tokens)
├── TODO.md (omitted)
├── UNIFIED_RULES_CONVENTION.md (1100 tokens)
├── bun.lock (omitted)
├── context.json (omitted)
├── examples/
├── README.md (omitted)
├── end-user-cjs-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── metadata-integration.test.ts (omitted)
├── package.json (omitted)
├── end-user-esm-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── package.json (omitted)
├── library-cjs-package/
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── library-esm-package/
├── README.md (omitted)
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── package.json (omitted)
├── public/
├── unified.png (omitted)
├── reference/
├── README.md (omitted)
├── amp-rules-directory/
├── AGENT.md (omitted)
├── claude-code-directory/
├── CLAUDE.md (omitted)
├── cline-rules-directory/
├── .clinerules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── codex-rules-directory/
├── AGENTS.md (omitted)
├── cursor-rules-directory/
├── .cursor/
├── always-on.mdc (omitted)
├── glob.mdc (omitted)
├── manual.mdc (omitted)
├── model-decision.mdc (omitted)
├── windsurf-rules-directory/
├── .windsurf/
├── rules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── zed-rules-directory/
├── .rules (omitted)
├── scripts/
├── build-examples.ts (omitted)
├── test-examples.ts (omitted)
├── src/
├── cli.ts (500 tokens)
├── commands/
├── install.ts (omitted)
├── list.ts (omitted)
├── load.ts (omitted)
├── save.ts (omitted)
├── index.ts (omitted)
├── llms/
├── index.ts (omitted)
├── internal.ts (omitted)
├── llms.txt (omitted)
├── providers/
├── amp-provider.ts (700 tokens)
├── claude-code-provider.ts (1100 tokens)
├── clinerules-provider.ts (1000 tokens)
├── codex-provider.ts (700 tokens)
├── cursor-provider.ts (800 tokens)
├── index.ts (300 tokens)
├── metadata-application.test.ts (2.1k tokens)
├── unified-provider.ts (600 tokens)
├── vscode-provider.ts (800 tokens)
├── windsurf-provider.ts (600 tokens)
├── zed-provider.ts (400 tokens)
├── schemas.ts (500 tokens)
├── text.d.ts (omitted)
├── types.ts (400 tokens)
├── utils/
├── frontmatter.test.ts (600 tokens)
├── frontmatter.ts (700 tokens)
├── path.ts (1600 tokens)
├── rule-formatter.ts (400 tokens)
├── rule-storage.test.ts (1300 tokens)
├── rule-storage.ts (1400 tokens)
├── similarity.ts (500 tokens)
├── single-file-helpers.ts (1200 tokens)
├── tsconfig.json (omitted)
├── vibe-tools.config.json (omitted)
```
## /README.md
# vibe-rules
[](https://www.npmjs.com/package/vibe-rules)
A powerful CLI tool for managing and sharing AI rules (prompts, configurations) across different editors and tools.
✨ **Supercharge your workflow!** ✨
## Quick Context Guide
This guide highlights the essential parts of the codebase and how you can quickly start working with **vibe-rules**.
[](https://contextjson.com/futureexcited/vibe-rules)
# Guide
Quickly save your favorite prompts and apply them to any supported editor:
```bash
# 1. Save your prompt locally
vibe-rules save my-react-helper -f ./prompts/react-helper.md
# 2. Apply it to Cursor
vibe-rules load my-react-helper cursor
```
Or, automatically install shared prompts from your project's NPM packages:
```bash
# Find packages with 'llms' exports in node_modules and install for Cursor
vibe-rules install cursor
```
(See full command details below.)
## Installation
```bash
# Install globally (bun recommended, npm/yarn also work)
bun i -g vibe-rules
```
## Usage
`vibe-rules` helps you save, list, load, and install AI rules.
### Save a Rule Locally
Store a rule for later use in the common `vibe-rules` store (`~/.vibe-rules/rules/`).
```bash
# Save from a file (e.g., .mdc, .md, or plain text)
vibe-rules save my-rule-name -f ./path/to/rule-content.md
# Save directly from content string
vibe-rules save another-rule -c "This is the rule content."
# Add/update a description
vibe-rules save my-rule-name -d "A concise description of what this rule does."
```
Options:
- `-f, --file <file>`: Path to the file containing the rule content.
- `-c, --content <content>`: Rule content provided as a string. (One of `-f` or `-c` is required).
- `-d, --description <desc>`: An optional description for the rule.
### List Saved Rules
See all the rules you've saved to the common local store.
```bash
vibe-rules list
```
### Load (Apply) a Saved Rule to an Editor
Apply a rule _from your local store_ (`~/.vibe-rules/rules/`) to a specific editor's configuration file. `vibe-rules` handles the formatting.
```bash
# Load 'my-rule-name' for Cursor (creates/updates .cursor/rules/my-rule-name.mdc)
vibe-rules load my-rule-name cursor
# Alias: vibe-rules add my-rule-name cursor
# Load 'my-rule-name' for Claude Code IDE globally (updates ~/.claude/CLAUDE.md)
vibe-rules load my-rule-name claude-code --global
# Alias: vibe-rules add my-rule-name claude-code -g
# Load 'my-rule-name' for Windsurf (appends to ./.windsurfrules)
vibe-rules load my-rule-name windsurf
# Load into the project's unified .rules file
vibe-rules load unified my-unified-rule
# Load into a specific custom target path
vibe-rules load my-rule-name cursor -t ./my-project/.cursor-rules/custom-rule.mdc
```
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`, `codex`, `clinerules`, `roo`, `vscode`.
Options:
- `-g, --global`: Apply to the editor's global configuration path (if supported, e.g., `claude-code`, `codex`). Defaults to project-local.
- `-t, --target <path>`: Specify a custom target file path or directory, overriding default/global paths.
### Sharing and Installing Rules via NPM
In the evolving AI landscape, prompts and context are becoming increasingly crucial components of development workflows. Just like code libraries, reusable AI rules and prompts are emerging as shareable assets.
We anticipate more NPM packages will begin exporting standardized AI configurations, often via a `llms` entry point (e.g., `my-package/llms`). `vibe-rules` embraces this trend with the `install` command.
### Install Rules from NPM Packages
âš¡ **The easiest way to integrate shared rule sets!** âš¡
Install rules _directly from NPM packages_ into an editor's configuration. `vibe-rules` automatically scans your project's dependencies or a specified package for compatible rule exports.
```bash
# Most common: Install rules from ALL dependencies/devDependencies for Cursor
# Scans package.json, finds packages with 'llms' export, applies rules.
vibe-rules install cursor
# Install rules from a specific package for Cursor
# (Assumes 'my-rule-package' is in node_modules)
vibe-rules install cursor my-rule-package
# Install rules from a specific package into a custom target dir for Roo/Cline
vibe-rules install roo my-rule-package -t ./custom-ruleset/
# Install rules into the project's unified .rules file
vibe-rules install unified my-awesome-prompts
```
Add the `--debug` global option to any `vibe-rules` command to enable detailed debug logging during execution. This can be helpful for troubleshooting installation issues.
Arguments:
- `<editor>`: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`, `vscode`.
- `[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:
- `-g, --global`: Apply to the editor's global configuration path (if supported).
- `-t, --target <path>`: Specify a custom target file path or directory.
**How `install` finds rules:**
1. Looks for installed NPM packages (either the specified one or all dependencies based on whether `[packageName]` is provided).
2. Attempts to dynamically import a module named `llms` from the package root (e.g., `require('my-rule-package/llms')` or `import('my-rule-package/llms')`). Handles both CommonJS and ESM.
3. Examines the `default export` of the `llms` module:
- **If it's a string:** Treats it as a single rule's content.
- **If it's an array:** Expects an array of rule strings or rule objects.
- Rule Object Shape: `{ name: string, rule: string, description?: string, alwaysApply?: boolean, globs?: string | string[] }` (Validated using Zod). Note the use of `rule` for content.
4. Uses the appropriate editor provider (`cursor`, `windsurf`, etc.) to format and apply each found rule to the correct target path (respecting `-g` and `-t` options). Metadata like `alwaysApply` and `globs` is passed to the provider if present in a rule object.
5. **Important:** Rules installed this way are applied _directly_ to the editor configuration; they are **not** saved to the common local store (`~/.vibe-rules/rules/`). Use `vibe-rules save` for that.
## Supported Editors & Formats
`vibe-rules` automatically handles formatting for:
- **Cursor (`cursor`)**:
- Creates/updates individual `.mdc` files in `./.cursor/rules/` (local) or `~/.cursor/rules/` (global, if supported via `-t`).
- Uses frontmatter for metadata (`description`, `alwaysApply`, `globs`).
- **Windsurf (`windsurf`)**:
- Appends rules wrapped in `<rule-name>` tags to `./.windsurfrules` (local) or a target file specified by `-t`. Global (`-g`) is not typically used.
- **Claude Code (`claude-code`)**:
- Appends/updates rules within XML-like tagged blocks in a `<!-- vibe-rules Integration -->` section in `./CLAUDE.md` (local) or `~/.claude/CLAUDE.md` (global).
- Each rule is encapsulated in tags like `<rule-name>...</rule-name>` within the single markdown file.
- Supports metadata formatting for `alwaysApply` and `globs` configurations.
- **Codex (`codex`)**:
- Appends/updates rules within XML-like tagged blocks in a `<!-- vibe-rules Integration -->` section in `./AGENTS.md` (local) or `~/.codex/AGENTS.md` (global).
- Each rule is encapsulated in tags like `<rule-name>...</rule-name>` within the single markdown file.
- Supports metadata formatting for `alwaysApply` and `globs` configurations.
- **Codex File Hierarchy**: Codex looks for `AGENTS.md` files in this order: `~/.codex/AGENTS.md` (global), `AGENTS.md` at repo root (default), and `AGENTS.md` in current working directory (use `--target` for subdirectories).
- **Amp (`amp`)**:
- Manages rules within a single `AGENT.md` 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 (similar to ZED).
- **Local only**: Does not support global files or subdirectory configurations.
- 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.
- **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.
- Follows the unified .rules convention and supports metadata formatting for `alwaysApply` and `globs` configurations.
- **VSCode (`vscode`)**:
- Creates/updates individual `.instructions.md` files within `./.github/instructions/` (project-local).
- Uses YAML frontmatter with `applyTo` field for file targeting.
- Rule name and description are included in the markdown content, not frontmatter.
- **Note:** Due to VSCode's limitations with multiple globs in the `applyTo` field, all rules use `applyTo: "**"` for universal application and better reliability.
- **Unified (`unified`)**:
- Manages rules within a single `.rules` file in the project root.
- Ideal for project-specific, centralized rule management.
- See the [Unified .rules File Convention](./UNIFIED_RULES_CONVENTION.md) for detailed format and usage.
## Development
```bash
# Clone the repo
git clone https://github.com/your-username/vibe-rules.git
cd vibe-rules
# Install dependencies
bun install
# Run tests (if any)
bun test
# Build the project
bun run build
# Link for local development testing
bun link
# Now you can use 'vibe-rules' command locally
```
## License
MIT
## /UNIFIED_RULES_CONVENTION.md
# Unified .rules File Convention
This document outlines the convention for the unified `.rules` file, a single file designed to store all your AI assistant/editor rules for a project in a centralized and portable manner.
## Purpose
The `.rules` file aims to:
1. **Simplify Rule Management**: Instead of scattering rules across various editor-specific files (e.g., `.cursor/rules/`, `.windsurfrules`, `CLAUDE.md`), you can keep them all in one place.
2. **Improve Portability**: Easily share and version control your project-specific rules along with your codebase.
3. **Standardize Format**: Provide a consistent way to define rules, including common metadata.
### The Challenge: Rule File Proliferation
Many development tools and AI assistants rely on specific configuration files for custom instructions or rules. While this allows for tailored behavior, it can lead to a multitude of rule files scattered across a project or system, each with its own format. This is the problem the `unified` provider and the `.rules` convention aim to simplify.

_Image concept from a [tweet by @aidenybai](https://x.com/aidenybai/status/1923781810820968857/photo/1), illustrating the proliferation of rule files._
## File Location
The `.rules` file should be located in the root directory of your project.
```
my-project/
├── .rules <-- Your unified rules file
├── src/
└── package.json
```
## File Format
The `.rules` file uses an XML-like tagged format to define individual rules. Each rule is enclosed in a tag named after the rule itself. This format is human-readable and easy to parse.
### Structure of a Rule Block
```xml
<your-rule-name>
<!-- Optional: Human-readable metadata -->
<!-- Description: A brief explanation of what the rule does. -->
<!-- AlwaysApply: true/false (Cursor-specific, can be included for compatibility) -->
<!-- Globs: pattern1,pattern2 (Cursor-specific, can be included for compatibility) -->
Your actual rule content goes here.
This can be multi-line plain text, a prompt, or any specific syntax required by the target AI model or tool.
</your-rule-name>
```
**Explanation:**
- **`<your-rule-name>`**: This is the opening tag for the rule. The name should be unique within the `.rules` file and is used to identify the rule (e.g., by the `vibe-rules load unified <your-rule-name>` command).
- **Metadata Comments (Optional)**:
- `<!-- Description: ... -->`: A description of the rule.
- `<!-- AlwaysApply: true/false -->`: Corresponds to Cursor's `alwaysApply` metadata. Useful if you intend to use these rules with Cursor or a system that understands this flag.
- `<!-- Globs: pattern1,pattern2 -->`: Corresponds to Cursor's `globs` metadata. Specify comma-separated glob patterns.
- **Rule Content**: The actual text or prompt for the rule.
- **`</your-rule-name>`**: The closing tag for the rule.
### Example `.rules` File
```xml
<explain-code-snippet>
<!-- Description: Explains the selected code snippet. -->
Explain the following code snippet:
```
{{code}}
```
</explain-code-snippet>
<generate-test-cases>
<!-- Description: Generates test cases for the given function. -->
<!-- Globs: *.test.js,*.spec.ts -->
Given the following function, please generate a comprehensive set of unit test cases in Jest format:
```
{{code}}
```
</generate-test-cases>
<my-custom-task-for-pkg>
This is a rule installed from 'my-package'.
It helps with a specific task related to this package.
</my-custom-task-for-pkg>
```
## Using with `vibe-rules`
### Saving Rules
You can save rules to your local common store and then load them into the `.rules` file, or install them directly from packages.
1. **Save a rule to the local store (optional first step):**
```bash
vibe-rules save my-new-rule -c "This is the content of my new rule."
```
2. **Load a rule into the project's `.rules` file:**
```bash
vibe-rules load unified my-new-rule
# This will create/update .rules in the current directory with the 'my-new-rule' block.
```
### Installing Rules from Packages
When you install rules from an NPM package using the `unified` editor type, `vibe-rules` will add them to your project's `.rules` file. Rule names will be prefixed with the package name.
```bash
# Install rules from 'some-npm-package' into .rules
vibe-rules install unified some-npm-package
# Install rules from all dependencies into .rules
vibe-rules install unified
```
This would result in entries like `<some-npm-package-rulename>...</some-npm-package-rulename>` in your `.rules` file.
### Listing Rules
To see rules specifically defined for the unified provider in your local store:
```bash
vibe-rules list --type unified
```
(Note: `list` command currently lists from the global store, not directly from a project's `.rules` file. To see rules in a project, open the `.rules` file.)
## Benefits of the `.rules` Convention
- **Centralization**: All project-specific AI prompts and rules in one predictable location.
- **Version Control**: Easily track changes to your rules alongside your code.
- **Collaboration**: Share rule configurations seamlessly with your team.
- **Editor Agnostic (Conceptual)**: While `vibe-rules` helps manage this file, the raw `.rules` file can be understood or parsed by other tools or future IDE integrations that adopt this convention.
## Future Considerations
- **Global `.rules`**: Support for a global `~/.rules` file for user-wide rules.
- **Enhanced Tooling**: IDE extensions or features that directly consume and suggest rules from the `.rules` file.
## /src/cli.ts
```ts path="/src/cli.ts"
#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";
import { installCommandAction } from "./commands/install.js";
import { saveCommandAction } from "./commands/save.js";
import { loadCommandAction } from "./commands/load.js";
import { listCommandAction } from "./commands/list.js";
// Simple debug logger
export let isDebugEnabled = false;
export const debugLog = (message: string, ...optionalParams: any[]) => {
if (isDebugEnabled) {
console.log(chalk.dim(`[Debug] ${message}`), ...optionalParams);
}
};
const program = new Command();
program
.name("vibe-rules")
.description(
"A utility for managing Cursor rules, Windsurf rules, Amp rules, and other AI prompts"
)
.version("0.1.0")
.option("--debug", "Enable debug logging", false);
program.on("option:debug", () => {
isDebugEnabled = program.opts().debug;
debugLog("Debug logging enabled.");
});
program
.command("save")
.description("Save a rule to the local store")
.argument("<n>", "Name of the rule")
.option("-c, --content <content>", "Rule content")
.option("-f, --file <file>", "Load rule content from file")
.option("-d, --description <desc>", "Rule description")
.action(saveCommandAction);
program
.command("list")
.description("List all saved rules from the common store")
.action(listCommandAction);
program
.command("load")
.alias("add")
.description("Apply a saved rule to an editor configuration")
.argument("<n>", "Name of the rule to apply")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(loadCommandAction);
program
.command("install")
.description(
"Install rules from an NPM package or all dependencies directly into an editor configuration"
)
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.argument("[packageName]", "Optional NPM package name to install rules from")
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(installCommandAction);
program.parse(process.argv);
if (process.argv.length <= 2) {
program.help();
}
```
## /src/providers/amp-provider.ts
```ts path="/src/providers/amp-provider.ts"
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class AmpRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.AMP;
/**
* Generates formatted content for Amp including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Appends a rule loaded from internal storage to the target Amp file.
* @param name - The name of the rule in internal storage.
* @param targetPath - Optional explicit target file path.
* @param isGlobal - Not supported for Amp (always local project files).
* @param options - Additional generation options.
* @returns True on success, false on failure.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal storage.`);
return false;
}
// Amp only supports local project files, ignore isGlobal
const actualTargetPath = targetPath ?? getRulePath(this.ruleType, "", false);
return this.appendFormattedRule(ruleConfig, actualTargetPath, false, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
* @param config - The rule configuration to apply.
* @param targetPath - The target file path (e.g., ./AGENT.md).
* @param isGlobal - Not supported for Amp (always false).
* @param options - Additional options like description, alwaysApply, globs.
* @returns True on success, false on failure.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean | undefined,
options?: RuleGeneratorOptions | undefined
): Promise<boolean> {
// Amp uses a simpler approach without vibe-rules integration block
return appendOrUpdateTaggedBlock(
targetPath,
config,
options,
false // No special integration block needed for Amp
);
}
}
```
## /src/providers/claude-code-provider.ts
```ts path="/src/providers/claude-code-provider.ts"
import * as fs from "fs-extra/esm";
import { readFile, writeFile } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata, createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
import chalk from "chalk";
export class ClaudeCodeRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CLAUDE_CODE;
/**
* Generates formatted content for Claude Code including metadata.
* This content is intended to be placed within the <!-- vibe-rules Integration --> block.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// Format the content with metadata
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.CLAUDE_CODE, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.CLAUDE_CODE, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.CLAUDE_CODE);
}
/**
* Applies a rule by updating the <vibe-rules> section in the target CLAUDE.md.
* If targetPath is omitted, it determines local vs global based on isGlobal option.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${this.ruleType}.`);
return false;
}
const destinationPath = targetPath || getRulePath(this.ruleType, name, isGlobal); // name might not be needed by getRulePath here
return this.appendFormattedRule(rule, destinationPath, isGlobal, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const destinationPath = targetPath;
// Ensure the parent directory exists
fs.ensureDirSync(path.dirname(destinationPath));
const newBlock = createTaggedRuleBlock(config, options);
let fileContent = "";
if (await fs.pathExists(destinationPath)) {
fileContent = await readFile(destinationPath, "utf-8");
}
// Escape rule name for regex
const ruleNameRegex = config.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`^<${ruleNameRegex}>[\\s\\S]*?</${ruleNameRegex}>`, "m");
let updatedContent: string;
const match = fileContent.match(regex);
if (match) {
// Rule exists, replace its content
console.log(
chalk.blue(`Updating existing rule block for "${config.name}" in ${destinationPath}...`)
);
updatedContent = fileContent.replace(regex, newBlock);
} else {
// Rule doesn't exist, append it
// Attempt to append within <!-- vibe-rules Integration --> if possible
const integrationStartTag = "<!-- vibe-rules Integration -->";
const integrationEndTag = "<!-- /vibe-rules Integration -->";
const startIndex = fileContent.indexOf(integrationStartTag);
const endIndex = fileContent.indexOf(integrationEndTag);
console.log(
chalk.blue(`Appending new rule block for "${config.name}" to ${destinationPath}...`)
);
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
// Insert before the end tag of the integration block
const insertionPoint = endIndex;
const before = fileContent.slice(0, insertionPoint);
const after = fileContent.slice(insertionPoint);
updatedContent = `${before.trimEnd()}\n\n${newBlock}\n\n${after.trimStart()}`;
} else {
// Create the vibe-rules Integration block if it doesn't exist
if (fileContent.trim().length > 0) {
// File has content but no integration block, wrap everything
updatedContent = `<!-- vibe-rules Integration -->\n\n${fileContent.trim()}\n\n${newBlock}\n\n<!-- /vibe-rules Integration -->`;
} else {
// New/empty file, create integration block with the new rule
updatedContent = `<!-- vibe-rules Integration -->\n\n${newBlock}\n\n<!-- /vibe-rules Integration -->`;
}
}
}
try {
await writeFile(destinationPath, updatedContent.trim() + "\n");
return true;
} catch (error) {
console.error(chalk.red(`Error writing updated rules to ${destinationPath}: ${error}`));
return false;
}
}
}
```
## /src/providers/clinerules-provider.ts
```ts path="/src/providers/clinerules-provider.ts"
import * as fs from "fs-extra/esm";
import { writeFile } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import {
getRulePath, // Returns the .clinerules directory path
ensureDirectoryExists,
} from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import chalk from "chalk";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
// Helper function specifically for clinerules/roo setup
// Focuses on the directory structure: .clinerules/vibe-rules.md
async function setupClinerulesDirectory(
clinerulesDirPath: string,
rulesContent: string
): Promise<void> {
await fs.ensureDir(clinerulesDirPath); // Ensure the .clinerules directory exists
const vibeRulesRulePath = path.join(clinerulesDirPath, "vibe-rules.md");
const rulesTemplate = rulesContent.replace(/\r\n/g, "\n").trim();
// Wrap content with <!-- vibe-rules --> tags if not already present
const startTag = "<!-- vibe-rules Integration -->";
const endTag = "<!-- /vibe-rules Integration -->";
let contentToWrite = rulesTemplate;
if (!contentToWrite.includes(startTag)) {
contentToWrite = `${startTag}\n${rulesTemplate}\n${endTag}`;
}
await writeFile(vibeRulesRulePath, contentToWrite + "\n");
}
export class ClinerulesRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CLINERULES;
/**
* Generates formatted content for Clinerules/Roo including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Applies a rule by setting up the .clinerules/vibe-rules.md structure.
* Always targets the project-local .clinerules directory.
*/
async appendRule(
name: string,
targetPath?: string, // If provided, should be the .clinerules directory path
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${RuleType.CLINERULES}/${RuleType.ROO}.`);
return false;
}
// getRulePath for CLINERULES/ROO returns the directory path
const destinationDir = targetPath || getRulePath(RuleType.CLINERULES, name); // name is ignored here
try {
const contentToAppend = this.generateRuleContent(rule, options);
await setupClinerulesDirectory(destinationDir, contentToAppend);
console.log(
chalk.green(
`Successfully set up ${RuleType.CLINERULES}/${RuleType.ROO} rules in: ${destinationDir}`
)
);
return true;
} catch (error) {
console.error(
chalk.red(
`Error setting up ${RuleType.CLINERULES}/${RuleType.ROO} rules in ${destinationDir}:`
),
error
);
return false;
}
}
/**
* Formats and applies a rule directly from a RuleConfig object.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string, // Should now receive the correct .../.clinerules/slugified-name.md path
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
// Ensure the parent .clinerules directory exists
const parentDir = path.dirname(targetPath);
ensureDirectoryExists(parentDir);
// Generate the content
const content = this.generateRuleContent(config, options);
// Log metadata inclusion (optional, but kept from previous state)
if (options?.alwaysApply !== undefined || options?.globs) {
console.log(
chalk.blue(
` Including metadata in rule content: alwaysApply=${options.alwaysApply}, globs=${JSON.stringify(options.globs)}`
)
);
}
try {
// Write directly to the target file path
await writeFile(targetPath, content, "utf-8");
console.log(chalk.green(`Successfully applied rule "${config.name}" to ${targetPath}`));
return true;
} catch (error) {
console.error(chalk.red(`Error applying rule "${config.name}" to ${targetPath}: ${error}`));
return false;
}
}
}
```
## /src/providers/codex-provider.ts
```ts path="/src/providers/codex-provider.ts"
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class CodexRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CODEX;
/**
* Generates formatted content for Codex including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Appends a rule loaded from internal storage to the target Codex file.
* @param name - The name of the rule in internal storage.
* @param targetPath - Optional explicit target file path.
* @param isGlobal - Hint for global context (uses ~/.codex/instructions.md).
* @param options - Additional generation options.
* @returns True on success, false on failure.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal storage.`);
return false;
}
const actualTargetPath = targetPath ?? getRulePath(this.ruleType, "", isGlobal);
return this.appendFormattedRule(ruleConfig, actualTargetPath, isGlobal, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
* @param config - The rule configuration to apply.
* @param targetPath - The target file path (e.g., ~/.codex/AGENTS.md or ./AGENTS.md).
* @param isGlobal - Unused by this method but kept for interface compatibility.
* @param options - Additional options like description, alwaysApply, globs.
* @returns True on success, false on failure.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean | undefined,
options?: RuleGeneratorOptions | undefined
): Promise<boolean> {
// Delegate to the shared helper, ensuring insertion within <vibe-rules>
return appendOrUpdateTaggedBlock(
targetPath,
config,
options,
true // Append inside <vibe-rules Integration>
);
}
}
```
## /src/providers/cursor-provider.ts
```ts path="/src/providers/cursor-provider.ts"
import * as path from "path";
import { writeFile } from "fs/promises";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath, ensureDirectoryExists } from "../utils/path.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
// Custom function to format frontmatter simply
const formatFrontmatter = (fm: Record<string, any>): string => {
let result = "";
if (fm.description) {
result += `description: ${fm.description}\n`;
}
if (fm.globs) {
if (fm.debug) {
console.log(`[Debug] Formatting globs: ${JSON.stringify(fm.globs)}`);
}
const globsString = Array.isArray(fm.globs) ? fm.globs.join(",") : fm.globs;
if (globsString) {
result += `globs: ${globsString}\n`;
}
}
if (fm.alwaysApply === false) {
result += `alwaysApply: false\n`;
} else if (fm.alwaysApply === true) {
result += `alwaysApply: true\n`;
}
return result.trim();
};
export class CursorRuleProvider implements RuleProvider {
/**
* Generate cursor rule content with frontmatter
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
const frontmatter: Record<string, any> = {};
if (options?.description ?? config.description) {
frontmatter.description = options?.description ?? config.description;
}
if (options?.globs) {
frontmatter.globs = options.globs;
}
if (options?.alwaysApply !== undefined) {
frontmatter.alwaysApply = options.alwaysApply;
}
if (options?.debug) {
frontmatter.debug = options.debug;
}
const frontmatterString =
Object.keys(frontmatter).length > 0 ? `---\n${formatFrontmatter(frontmatter)}\n---\n` : "";
return `${frontmatterString}${config.content}`;
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
// Use the utility function to save to internal storage
return saveInternalRule(RuleType.CURSOR, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
// Use the utility function to load from internal storage
return loadInternalRule(RuleType.CURSOR, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
// Use the utility function to list rules from internal storage
return listInternalRules(RuleType.CURSOR);
}
/**
* Append a cursor rule to a target file
*/
async appendRule(name: string, targetPath?: string, isGlobal?: boolean): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal Cursor storage.`);
return false;
}
const finalTargetPath = targetPath || getRulePath(RuleType.CURSOR, name, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal);
}
/**
* Formats and applies a rule directly from a RuleConfig object.
* Creates the .cursor/rules directory if it doesn't exist.
* Uses slugified rule name for the filename.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const fullPath = targetPath;
const dir = path.dirname(fullPath);
try {
await ensureDirectoryExists(dir); // Ensure the PARENT directory exists
const formattedContent = this.generateRuleContent(config, options); // Pass options
await writeFile(fullPath, formattedContent, "utf-8");
return true;
} catch (error) {
console.error(`Error applying Cursor rule "${config.name}" to ${fullPath}:`, error);
return false;
}
}
}
```
## /src/providers/index.ts
```ts path="/src/providers/index.ts"
import { RuleProvider, RuleType } from "../types.js";
import { CursorRuleProvider } from "./cursor-provider.js";
import { WindsurfRuleProvider } from "./windsurf-provider.js";
import { ClaudeCodeRuleProvider } from "./claude-code-provider.js";
import { CodexRuleProvider } from "./codex-provider.js";
import { AmpRuleProvider } from "./amp-provider.js";
import { ClinerulesRuleProvider } from "./clinerules-provider.js";
import { ZedRuleProvider } from "./zed-provider.js";
import { UnifiedRuleProvider } from "./unified-provider.js";
import { VSCodeRuleProvider } from "./vscode-provider.js";
/**
* Factory function to get the appropriate rule provider based on rule type
*/
export function getRuleProvider(ruleType: RuleType): RuleProvider {
switch (ruleType) {
case RuleType.CURSOR:
return new CursorRuleProvider();
case RuleType.WINDSURF:
return new WindsurfRuleProvider();
case RuleType.CLAUDE_CODE:
return new ClaudeCodeRuleProvider();
case RuleType.CODEX:
return new CodexRuleProvider();
case RuleType.AMP:
return new AmpRuleProvider();
case RuleType.CLINERULES:
case RuleType.ROO:
return new ClinerulesRuleProvider();
case RuleType.ZED:
return new ZedRuleProvider();
case RuleType.UNIFIED:
return new UnifiedRuleProvider();
case RuleType.VSCODE:
return new VSCodeRuleProvider();
case RuleType.CUSTOM:
default:
throw new Error(`Unsupported rule type: ${ruleType}`);
}
}
```
## /src/providers/metadata-application.test.ts
```ts path="/src/providers/metadata-application.test.ts"
import { test, expect, describe } from "bun:test";
// Test data that would be used by providers
const testRule = {
name: "test-rule",
content: "# Test Rule\n\nThis is test content for metadata application.",
description: "Test rule for validating metadata application",
};
const testMetadata = {
alwaysApply: false,
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx"],
description: "API and routing patterns",
};
const alwaysApplyMetadata = {
alwaysApply: true,
description: "Universal guidelines",
};
describe("Metadata Application Patterns", () => {
describe("Cursor Provider Format", () => {
test("should generate YAML frontmatter pattern", () => {
// Simulate what Cursor provider generateRuleContent should produce
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
if (metadata?.globs) {
if (Array.isArray(metadata.globs)) {
result += "globs:\n";
metadata.globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
} else {
result += `globs: ${metadata.globs}\n`;
}
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, testMetadata);
expect(result).toContain("---");
expect(result).toContain("description: API and routing patterns");
expect(result).toContain("alwaysApply: false");
expect(result).toContain("globs:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("---");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should handle alwaysApply: true", () => {
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, alwaysApplyMetadata);
expect(result).toContain("alwaysApply: true");
expect(result).not.toContain("globs:");
});
test("should handle single glob string", () => {
const singleGlobMetadata = {
...testMetadata,
globs: "src/**/*.ts",
};
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
if (metadata?.globs) {
if (Array.isArray(metadata.globs)) {
result += "globs:\n";
metadata.globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
} else {
result += `globs: ${metadata.globs}\n`;
}
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, singleGlobMetadata);
expect(result).toContain("globs: src/**/*.ts");
});
});
describe("Windsurf Provider Format", () => {
test("should generate human-readable metadata", () => {
// Simulate what Windsurf provider should produce
const generateWindsurfFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateWindsurfFormat(testRule, testMetadata);
expect(result).toContain("Always Apply: false");
expect(result).toContain("Applies to files matching:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should handle alwaysApply: true with no globs", () => {
const generateWindsurfFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateWindsurfFormat(testRule, alwaysApplyMetadata);
expect(result).toContain("Always Apply: true");
expect(result).not.toContain("Applies to files matching:");
});
});
describe("VSCode Provider Format", () => {
test("should generate .instructions.md with YAML frontmatter", () => {
// Simulate what VSCode provider should produce
const generateVSCodeFormat = (rule: any, _metadata: any) => {
let result = "---\n";
// VSCode limitation: always use universal glob
result += `applyTo: "**"\n`;
result += "---\n\n";
result += `# ${rule.name}\n\n`;
if (rule.description) {
result += `**Description:** ${rule.description}\n\n`;
}
result += rule.content;
return result;
};
const result = generateVSCodeFormat(testRule, testMetadata);
expect(result).toContain("---");
expect(result).toContain('applyTo: "**"'); // VSCode limitation workaround
expect(result).toContain("---");
expect(result).toContain("# test-rule");
expect(result).toContain("**Description:** Test rule for validating metadata application");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should always use universal glob due to VSCode bug", () => {
const generateVSCodeFormat = (rule: any, _metadata: any) => {
let result = "---\n";
// VSCode limitation: always use universal glob
result += `applyTo: "**"\n`;
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateVSCodeFormat(testRule, testMetadata);
// Should always use "**" regardless of original globs
expect(result).toContain('applyTo: "**"');
expect(result).not.toContain("src/api");
expect(result).not.toContain("src/routes");
});
});
describe("Single-File Provider Patterns", () => {
test("should generate content with metadata lines for Zed/Claude/Codex", () => {
// Simulate what single-file providers should produce
const generateSingleFileFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateSingleFileFormat(testRule, testMetadata);
expect(result).toContain("Always Apply: false");
expect(result).toContain("Applies to files matching:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("# Test Rule");
});
});
describe("Cross-Provider Consistency", () => {
test("all provider formats should preserve rule content", () => {
const formats = [
// Cursor format
(rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) result += `description: ${metadata.description}\n`;
result += "---\n\n" + rule.content;
return result;
},
// Windsurf format
(rule: any, _metadata: any) => {
return rule.content;
},
// VSCode format
(rule: any, _metadata: any) => {
return `---\napplyTo: "**"\n---\n\n${rule.content}`;
},
// Single-file format
(rule: any, _metadata: any) => {
return rule.content;
},
];
formats.forEach((format) => {
const result = format(testRule, testMetadata);
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
});
test("all provider formats should handle rules without metadata", () => {
const plainRule = {
name: "plain-rule",
content: "# Plain Rule\n\nNo metadata here.",
description: "A simple rule",
};
const formats = [
// Cursor format (minimal frontmatter)
(rule: any) => {
return `---\n---\n\n${rule.content}`;
},
// Others just return content
(rule: any) => rule.content,
(rule: any) => rule.content,
(rule: any) => rule.content,
];
formats.forEach((format) => {
const result = format(plainRule);
expect(result).toContain("# Plain Rule");
expect(result).toContain("No metadata here.");
// Should not crash or add invalid metadata
});
});
});
});
```
## /src/providers/unified-provider.ts
```ts path="/src/providers/unified-provider.ts"
import path from "path";
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import {
saveInternalRule as saveRuleToInternalStorage,
loadInternalRule as loadRuleFromInternalStorage,
listInternalRules as listRulesFromInternalStorage,
} from "../utils/rule-storage.js";
const UNIFIED_RULE_FILENAME = ".rules";
export class UnifiedRuleProvider implements RuleProvider {
private getRuleFilePath(
ruleName: string,
isGlobal: boolean = false,
projectRoot: string = process.cwd()
): string {
// For unified provider, ruleName is not used in the path, always '.rules'
// isGlobal might determine if it's in projectRoot or user's global .rules, but for now, always project root.
if (isGlobal) {
// Potentially handle a global ~/.rules file in the future
// For now, global unified rules are not distinct from project unified rules
// console.warn('Global unified rules are not yet uniquely supported, using project .rules');
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
async saveRule(config: RuleConfig, _options?: RuleGeneratorOptions): Promise<string> {
return saveRuleToInternalStorage(RuleType.UNIFIED, config);
}
async loadRule(name: string): Promise<RuleConfig | null> {
return loadRuleFromInternalStorage(RuleType.UNIFIED, name);
}
async listRules(): Promise<string[]> {
return listRulesFromInternalStorage(RuleType.UNIFIED);
}
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// For .rules, we use the tagged block format directly
return createTaggedRuleBlock(config, options);
}
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in local unified store.`);
return false;
}
const filePath = targetPath || this.getRuleFilePath(name, isGlobal);
return this.appendFormattedRule(ruleConfig, filePath, isGlobal, options);
}
async appendFormattedRule(
config: RuleConfig,
targetPath: string, // This will be the path to the .rules file
_isGlobal?: boolean, // isGlobal might be less relevant here if .rules is always project-based
options?: RuleGeneratorOptions
): Promise<boolean> {
// The 'targetPath' for unified provider should directly be the path to the '.rules' file.
// The 'config.name' is used for the tag name within the '.rules' file.
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/providers/vscode-provider.ts
```ts path="/src/providers/vscode-provider.ts"
import * as path from "path";
import { writeFile } from "fs/promises";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath, ensureDirectoryExists } from "../utils/path.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
//vs code bugged for 2+ globs...
export class VSCodeRuleProvider implements RuleProvider {
/**
* Generate VSCode instruction content with frontmatter
*
* NOTE: VSCode has a bug where multiple globs in applyTo don't work properly,
* so we always use "**" to apply rules universally for better reliability.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// VSCode Bug Workaround: Always use "**" because VSCode's applyTo field
// doesn't properly handle multiple globs or complex glob patterns.
// This ensures rules work consistently across all files.
const applyToValue = "**";
// Always include frontmatter with applyTo (required for VSCode)
const frontmatterString = `---\napplyTo: "${applyToValue}"\n---\n`;
// Start with the original rule content
let content = config.content;
// Add name and description in content if available
let contentPrefix = "";
// Extract original rule name from prefixed config.name (remove package prefix)
let displayName = config.name;
if (displayName.includes("_")) {
displayName = displayName.split("_").slice(1).join("_");
}
// Add name as heading if not already present in content
if (displayName && !content.includes(`# ${displayName}`)) {
contentPrefix += `# ${displayName}\n`;
}
// Add description if provided
if (options?.description ?? config.description) {
contentPrefix += `## ${options?.description ?? config.description}\n\n`;
} else if (contentPrefix) {
contentPrefix += "\n";
}
return `${frontmatterString}${contentPrefix}${content}`;
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.VSCODE, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.VSCODE, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.VSCODE);
}
/**
* Append a VSCode instruction rule to a target file
*/
async appendRule(name: string, targetPath?: string, isGlobal?: boolean): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal VSCode storage.`);
return false;
}
const finalTargetPath = targetPath || getRulePath(RuleType.VSCODE, name, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal);
}
/**
* Formats and applies a rule directly from a RuleConfig object.
* Creates the .github/instructions directory if it doesn't exist.
* Uses slugified rule name for the filename with .instructions.md extension.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const fullPath = targetPath;
const dir = path.dirname(fullPath);
try {
await ensureDirectoryExists(dir);
const formattedContent = this.generateRuleContent(config, options);
await writeFile(fullPath, formattedContent, "utf-8");
return true;
} catch (error) {
console.error(`Error applying VSCode rule "${config.name}" to ${fullPath}:`, error);
return false;
}
}
}
```
## /src/providers/windsurf-provider.ts
```ts path="/src/providers/windsurf-provider.ts"
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { getDefaultTargetPath } from "../utils/path.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class WindsurfRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.WINDSURF;
/**
* Format rule content with XML tags
*/
private formatRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return createTaggedRuleBlock(config, options);
}
/**
* Generates formatted rule content with Windsurf XML tags
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return this.formatRuleContent(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.WINDSURF, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.WINDSURF, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.WINDSURF);
}
/**
* Append a windsurf rule to a target file
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${this.ruleType}.`);
return false;
}
// Windsurf typically doesn't use global paths or specific rule name paths for the target
// It uses a single default file.
const destinationPath = targetPath || getDefaultTargetPath(this.ruleType);
return this.appendFormattedRule(rule, destinationPath, false, options);
}
/**
* Format and append a rule directly from a RuleConfig object.
* If a rule with the same name (tag) already exists, its content is updated.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
// Delegate to the shared helper
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/providers/zed-provider.ts
```ts path="/src/providers/zed-provider.ts"
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { getDefaultTargetPath } from "../utils/path.js";
export class ZedRuleProvider implements RuleProvider {
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.ZED, config);
}
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.ZED, name);
}
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.ZED);
}
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// Zed .rules files are expected to be plain text or use a format
// compatible with simple tagged blocks if we are managing multiple rules within it.
// For consistency with Windsurf and other single-file providers, we use tagged blocks.
return createTaggedRuleBlock(config, options);
}
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean, // isGlobal is not typically used for Zed's .rules
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal ZED storage.`);
return false;
}
const finalTargetPath = targetPath || getDefaultTargetPath(RuleType.ZED, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal, options);
}
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean, // isGlobal is not typically used for Zed's .rules
options?: RuleGeneratorOptions
): Promise<boolean> {
// Zed .rules files are at the root of the worktree, so isGlobal is likely false.
// We don't append inside a specific <vibe-rules Integration> block by default for .rules
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/schemas.ts
```ts path="/src/schemas.ts"
import { z } from "zod";
export const RuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
// Add other fields from RuleConfig if they exist and need validation
});
// Schema for metadata in stored rules
export const RuleGeneratorOptionsSchema = z
.object({
description: z.string().optional(),
isGlobal: z.boolean().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(),
debug: z.boolean().optional(),
})
.optional();
// Schema for the enhanced local storage format with metadata
export const StoredRuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
metadata: RuleGeneratorOptionsSchema,
});
// Original schema for reference (might still be used elsewhere, e.g., save command)
export const VibeRulesSchema = z.array(RuleConfigSchema);
// --- Schemas for Package Exports (`<packageName>/llms`) ---
// Schema for the flexible rule object format within packages
export const PackageRuleObjectSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
rule: z.string().min(1, "Rule content cannot be empty"), // Renamed from content
description: z.string().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(), // Allow string or array
});
// Schema for a single item in the package export (either a string or the object)
export const PackageRuleItemSchema = z.union([
z.string().min(1, "Rule string cannot be empty"),
PackageRuleObjectSchema, // Defined above now
]);
// Schema for the default export of package/llms (array of strings or objects)
export const VibePackageRulesSchema = z.array(PackageRuleItemSchema);
// --- Type Helpers ---
// Basic RuleConfig type
export type RuleConfig = z.infer<typeof RuleConfigSchema>;
// Type for the enhanced local storage format
export type StoredRuleConfig = z.infer<typeof StoredRuleConfigSchema>;
// Type for rule generator options
export type RuleGeneratorOptions = z.infer<typeof RuleGeneratorOptionsSchema>;
// Type for the flexible package rule object
export type PackageRuleObject = z.infer<typeof PackageRuleObjectSchema>;
// Type for a single item in the package export array
export type PackageRuleItem = z.infer<typeof PackageRuleItemSchema>;
```
## /src/types.ts
```ts path="/src/types.ts"
/**
* Rule type definitions for the vibe-rules utility
*/
export type * from "./schemas.js";
export interface RuleConfig {
name: string;
content: string;
description?: string;
// Additional properties can be added as needed
}
// Extended interface for storing rules locally with metadata
export interface StoredRuleConfig {
name: string;
content: string;
description?: string;
metadata?: RuleGeneratorOptions;
}
export const RuleType = {
CURSOR: "cursor",
WINDSURF: "windsurf",
CLAUDE_CODE: "claude-code",
CODEX: "codex",
AMP: "amp",
CLINERULES: "clinerules",
ROO: "roo",
ZED: "zed",
UNIFIED: "unified",
VSCODE: "vscode",
CUSTOM: "custom",
} as const;
export type RuleTypeArray = (typeof RuleType)[keyof typeof RuleType][];
export type RuleType = (typeof RuleType)[keyof typeof RuleType];
export interface RuleProvider {
/**
* Creates a new rule file with the given content
*/
saveRule(config: RuleConfig, options?: RuleGeneratorOptions): Promise<string>;
/**
* Loads a rule from storage
*/
loadRule(name: string): Promise<RuleConfig | null>;
/**
* Lists all available rules
*/
listRules(): Promise<string[]>;
/**
* Appends a rule to an existing file
*/
appendRule(name: string, targetPath?: string): Promise<boolean>;
/**
* Formats and appends a rule directly from a RuleConfig object
*/
appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean>;
/**
* Generates formatted rule content with editor-specific formatting
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string;
}
export interface RuleGeneratorOptions {
description?: string;
isGlobal?: boolean;
alwaysApply?: boolean;
globs?: string | string[];
debug?: boolean;
// Additional options for specific rule types
}
```
## /src/utils/frontmatter.test.ts
```ts path="/src/utils/frontmatter.test.ts"
import { test, expect, describe } from "bun:test";
import { parseFrontmatter } from "./frontmatter.js";
describe("Frontmatter Parser", () => {
test("should parse Cursor .mdc frontmatter correctly", () => {
const mdcContent = `---
description: Test API rule
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx"]
alwaysApply: false
---
# API Development Guidelines
- Use proper error handling
- Follow RESTful conventions`;
const { frontmatter, content } = parseFrontmatter(mdcContent);
expect(frontmatter.description).toBe("Test API rule");
expect(frontmatter.globs).toEqual(["src/api/**/*.ts", "src/routes/**/*.tsx"]);
expect(frontmatter.alwaysApply).toBe(false);
expect(content.trim()).toBe(`# API Development Guidelines
- Use proper error handling
- Follow RESTful conventions`);
});
test("should parse alwaysApply: true correctly", () => {
const mdcContent = `---
description: Always applied rule
alwaysApply: true
---
This rule is always active.`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.alwaysApply).toBe(true);
});
test("should parse single glob string", () => {
const mdcContent = `---
globs: "src/**/*.ts"
---
Content here.`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.globs).toBe("src/**/*.ts");
});
test("should handle content without frontmatter", () => {
const plainContent = "Just plain content without frontmatter";
const { frontmatter, content } = parseFrontmatter(plainContent);
expect(Object.keys(frontmatter)).toHaveLength(0);
expect(content).toBe(plainContent);
});
test("should skip empty/null values", () => {
const mdcContent = `---
description: Valid description
globs:
alwaysApply: false
emptyField:
---
Content`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.description).toBe("Valid description");
expect(frontmatter.alwaysApply).toBe(false);
expect(frontmatter.globs).toBeUndefined();
expect(frontmatter.emptyField).toBeUndefined();
});
test("should handle complex globs array", () => {
const mdcContent = `---
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx", "src/components/**/*.vue"]
---
Multi-file rule content`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.globs).toEqual([
"src/api/**/*.ts",
"src/routes/**/*.tsx",
"src/components/**/*.vue",
]);
});
test("should handle malformed frontmatter gracefully", () => {
const malformedContent = `---
invalid: yaml: content[
not-closed: "quote
---
Content should still be parsed`;
const { frontmatter, content } = parseFrontmatter(malformedContent);
// Should not crash and should extract content
expect(content).toContain("Content should still be parsed");
expect(typeof frontmatter).toBe("object");
});
});
```
## /src/utils/frontmatter.ts
```ts path="/src/utils/frontmatter.ts"
/**
* Simple frontmatter parser for .mdc files
* Parses YAML-like frontmatter without external dependencies
*/
export interface ParsedContent {
frontmatter: Record<string, any>;
content: string;
}
/**
* Parse frontmatter from content that may contain YAML frontmatter
* Returns the parsed frontmatter object and the remaining content
*/
export function parseFrontmatter(input: string): ParsedContent {
const lines = input.split("\n");
// Check if content starts with frontmatter delimiter
if (lines[0]?.trim() !== "---") {
return {
frontmatter: {},
content: input,
};
}
// Find the closing delimiter
let endIndex = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i]?.trim() === "---") {
endIndex = i;
break;
}
}
if (endIndex === -1) {
// No closing delimiter found, treat as regular content
return {
frontmatter: {},
content: input,
};
}
// Extract frontmatter lines
const frontmatterLines = lines.slice(1, endIndex);
const contentLines = lines.slice(endIndex + 1);
// Parse frontmatter (simple YAML-like parsing)
const frontmatter: Record<string, any> = {};
for (const line of frontmatterLines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue; // Skip empty lines and comments
}
const colonIndex = trimmed.indexOf(":");
if (colonIndex === -1) {
continue; // Skip lines without colons
}
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
// Parse different value types
if (value === "true") {
frontmatter[key] = true;
} else if (value === "false") {
frontmatter[key] = false;
} else if (value === "null" || value === "") {
// Skip null or empty values instead of setting them
continue;
} else if (/^\d+$/.test(value)) {
frontmatter[key] = parseInt(value, 10);
} else if (/^\d+\.\d+$/.test(value)) {
frontmatter[key] = parseFloat(value);
} else if (value.startsWith("[") && value.endsWith("]")) {
// Simple array parsing for globs
try {
const arrayContent = value.slice(1, -1);
if (arrayContent.trim() === "") {
frontmatter[key] = [];
} else {
const items = arrayContent.split(",").map((item) => {
const trimmed = item.trim();
// Remove quotes if present
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return trimmed.slice(1, -1);
}
return trimmed;
});
frontmatter[key] = items;
}
} catch {
frontmatter[key] = value; // Fallback to string if parsing fails
}
} else {
// Remove quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
frontmatter[key] = value;
}
}
return {
frontmatter,
content: contentLines.join("\n").replace(/^\n+/, ""), // Remove leading newlines
};
}
```
## /src/utils/path.ts
```ts path="/src/utils/path.ts"
import path from "path";
import os from "os";
import fs, { pathExists } from "fs-extra/esm";
import { RuleType } from "../types.js";
import { debugLog } from "../cli.js";
// Base directory for storing internal rule definitions
export const RULES_BASE_DIR = path.join(os.homedir(), ".vibe-rules");
// Home directories for specific IDEs/Tools
export const CLAUDE_HOME_DIR = path.join(os.homedir(), ".claude");
export const CODEX_HOME_DIR = path.join(os.homedir(), ".codex");
export const ZED_RULES_FILE = ".rules"; // Added for Zed
/**
* Get the common rules directory path
*/
export function getCommonRulesDir(): string {
const rulesDir = path.join(RULES_BASE_DIR, "rules");
fs.ensureDirSync(rulesDir);
return rulesDir;
}
/**
* Get path to store internal rule definitions based on rule type
* (Not the actual target paths for IDEs)
*/
export function getInternalRuleStoragePath(ruleType: RuleType, ruleName: string): string {
const typeDir = path.join(RULES_BASE_DIR, ruleType);
fs.ensureDirSync(typeDir);
// Internal storage uses a simple .txt for content
return path.join(typeDir, `${ruleName}.txt`);
}
/**
* Get the expected file path for a rule based on its type and context (local/global).
* This now returns the actual path where the rule should exist for the IDE/tool.
* The 'isGlobal' flag determines if we should use the home directory path.
*/
export function getRulePath(
ruleType: RuleType,
ruleName: string, // ruleName might not be relevant for some types like Claude/Codex global
isGlobal: boolean = false,
projectRoot: string = process.cwd()
): string {
switch (ruleType) {
case RuleType.CURSOR:
// Cursor rules are typically project-local in .cursor/rules/
const cursorDir = path.join(projectRoot, ".cursor", "rules");
// Use slugified name for the file
return path.join(cursorDir, `${slugifyRuleName(ruleName)}.mdc`);
case RuleType.WINDSURF:
// Windsurf rules are typically project-local .windsurfrules file
return path.join(projectRoot, ".windsurfrules"); // Single file, name not used for path
case RuleType.CLAUDE_CODE:
// Claude rules are CLAUDE.md, either global or local
return isGlobal
? path.join(CLAUDE_HOME_DIR, "CLAUDE.md")
: path.join(projectRoot, "CLAUDE.md");
case RuleType.CODEX:
// Codex uses AGENTS.md (global) or AGENTS.md (local)
return isGlobal
? path.join(CODEX_HOME_DIR, "AGENTS.md")
: path.join(projectRoot, "AGENTS.md");
case RuleType.AMP:
// Amp uses AGENT.md (local only, no global support)
return path.join(projectRoot, "AGENT.md");
case RuleType.CLINERULES:
case RuleType.ROO:
// Cline/Roo rules are project-local files in .clinerules/
return path.join(
projectRoot,
".clinerules",
slugifyRuleName(ruleName) + ".md" // Use .md extension
);
case RuleType.ZED: // Added for Zed
return path.join(projectRoot, ZED_RULES_FILE);
case RuleType.UNIFIED:
return path.join(projectRoot, ".rules"); // Unified also uses .rules in project root
case RuleType.VSCODE:
// VSCode instructions are project-local files in .github/instructions/
return path.join(
projectRoot,
".github",
"instructions",
slugifyRuleName(ruleName) + ".instructions.md"
);
case RuleType.CUSTOM:
default:
// Fallback for custom or unknown - store internally for now
// Or maybe this should throw an error?
return getInternalRuleStoragePath(ruleType, ruleName);
}
}
/**
* Get the default target path (directory or file) where a rule type is typically applied.
* This is used by commands like 'apply' if no specific target is given.
* Note: This might overlap with getRulePath for some types.
* Returns potential paths based on convention.
*/
export function getDefaultTargetPath(
ruleType: RuleType,
isGlobalHint: boolean = false // Hint for providers like Claude/Codex
): string {
switch (ruleType) {
case RuleType.CURSOR:
// Default target is the rules directory within .cursor
return path.join(process.cwd(), ".cursor", "rules");
case RuleType.WINDSURF:
// Default target is the .windsurfrules file
return path.join(process.cwd(), ".windsurfrules");
case RuleType.CLAUDE_CODE:
// Default target depends on global hint
return isGlobalHint
? CLAUDE_HOME_DIR // Directory
: process.cwd(); // Project root (for local CLAUDE.md)
case RuleType.CODEX:
// Default target depends on global hint
return isGlobalHint
? CODEX_HOME_DIR // Directory
: process.cwd(); // Project root (for local AGENTS.md)
case RuleType.AMP:
// Amp only supports local project root (for local AGENT.md)
return process.cwd();
case RuleType.CLINERULES:
case RuleType.ROO:
// Default target is the .clinerules directory
return path.join(process.cwd(), ".clinerules");
case RuleType.ZED: // Added for Zed
return path.join(process.cwd(), ZED_RULES_FILE);
case RuleType.UNIFIED:
return path.join(process.cwd(), ".rules");
case RuleType.VSCODE:
// Default target is the .github/instructions directory
return path.join(process.cwd(), ".github", "instructions");
default:
console.warn(
`Default target path not defined for rule type: ${ruleType}, defaulting to CWD.`
);
return process.cwd();
}
}
/**
* Ensures that a specific directory exists, creating it if necessary.
*
* @param dirPath The absolute or relative path to the directory to ensure.
*/
export function ensureDirectoryExists(dirPath: string): void {
try {
fs.ensureDirSync(dirPath);
debugLog(`Ensured directory exists: ${dirPath}`);
} catch (err: any) {
console.error(`Failed to ensure directory ${dirPath}:`, err);
// Depending on the desired behavior, you might want to re-throw or exit
// throw err;
}
}
/**
* Checks if the configuration for a given editor type exists.
* This is used to prevent the 'install' command from creating config files/dirs.
* @param ruleType The editor type to check.
* @param isGlobal Whether to check the global or local path.
* @param projectRoot The root directory of the project.
* @returns A promise that resolves to true if the configuration exists, false otherwise.
*/
export async function editorConfigExists(
ruleType: RuleType,
isGlobal: boolean,
projectRoot: string = process.cwd()
): Promise<boolean> {
let checkPath: string;
switch (ruleType) {
case RuleType.CURSOR:
checkPath = path.join(projectRoot, ".cursor");
break;
case RuleType.WINDSURF:
checkPath = path.join(projectRoot, ".windsurfrules");
break;
case RuleType.CLINERULES:
case RuleType.ROO:
checkPath = path.join(projectRoot, ".clinerules");
break;
case RuleType.ZED:
case RuleType.UNIFIED:
checkPath = path.join(projectRoot, ".rules");
break;
case RuleType.VSCODE:
checkPath = path.join(projectRoot, ".github", "instructions");
break;
case RuleType.CLAUDE_CODE:
checkPath = isGlobal
? path.join(CLAUDE_HOME_DIR, "CLAUDE.md")
: path.join(projectRoot, "CLAUDE.md");
break;
case RuleType.CODEX:
checkPath = isGlobal
? path.join(CODEX_HOME_DIR, "AGENTS.md")
: path.join(projectRoot, "AGENTS.md");
break;
case RuleType.AMP:
checkPath = path.join(projectRoot, "AGENT.md");
break;
default:
return false; // Unknown or unsupported for this check
}
return pathExists(checkPath);
}
/**
* Convert a rule name to a filename-safe slug.
*/
export function slugifyRuleName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9_]+/g, "-")
.replace(/^-|-$/g, "");
}
```
## /src/utils/rule-formatter.ts
```ts path="/src/utils/rule-formatter.ts"
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
/**
* Formats rule content for non-cursor providers that use XML-like tags.
* Includes additional metadata like alwaysApply and globs in a human-readable format
* within the rule content.
*/
export function formatRuleWithMetadata(config: RuleConfig, options?: RuleGeneratorOptions): string {
let formattedContent = config.content;
// Add metadata lines at the beginning of content if they exist
const metadataLines = [];
// Add alwaysApply information if provided
if (options?.alwaysApply !== undefined) {
metadataLines.push(
`Always Apply: ${options.alwaysApply ? "true" : "false"} - ${
options.alwaysApply
? "This rule should ALWAYS be applied by the AI"
: "This rule should only be applied when relevant files are open"
}`
);
}
// Add globs information if provided
if (options?.globs && options.globs.length > 0) {
const globsStr = Array.isArray(options.globs) ? options.globs.join(", ") : options.globs;
// Skip adding the glob info if it's just a generic catch-all pattern
const isCatchAllPattern =
globsStr === "**/*" ||
(Array.isArray(options.globs) && options.globs.length === 1 && options.globs[0] === "**/*");
if (!isCatchAllPattern) {
metadataLines.push(`Always apply this rule in these files: ${globsStr}`);
}
}
// If we have metadata, add it to the beginning of the content
if (metadataLines.length > 0) {
formattedContent = `${metadataLines.join("\n")}\n\n${config.content}`;
}
return formattedContent;
}
/**
* Creates a complete XML-like block for a rule, including start/end tags
* and formatted content with metadata
*/
export function createTaggedRuleBlock(config: RuleConfig, options?: RuleGeneratorOptions): string {
const formattedContent = formatRuleWithMetadata(config, options);
const startTag = `<${config.name}>`;
const endTag = `</${config.name}>`;
return `${startTag}\n${formattedContent}\n${endTag}`;
}
```
## /src/utils/rule-storage.test.ts
```ts path="/src/utils/rule-storage.test.ts"
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { readFile, writeFile, rm, mkdir, readdir } from "fs/promises";
import * as fs from "fs-extra/esm";
import * as path from "path";
// Test data directory
const TEST_DATA_DIR = ".test-rule-storage";
const TEST_RULES_DIR = path.join(TEST_DATA_DIR, "rules");
// Import the functions dynamically to avoid CLI trigger
const { StoredRuleConfigSchema } = await import("../schemas.js");
describe("Rule Storage", () => {
beforeEach(async () => {
// Clean up and create test directories
if (await fs.pathExists(TEST_DATA_DIR)) {
await rm(TEST_DATA_DIR, { recursive: true });
}
await mkdir(TEST_DATA_DIR, { recursive: true });
await mkdir(TEST_RULES_DIR, { recursive: true });
});
afterEach(async () => {
// Clean up test files
if (await fs.pathExists(TEST_DATA_DIR)) {
await rm(TEST_DATA_DIR, { recursive: true });
}
});
describe("StoredRuleConfig schema validation", () => {
test("should validate rule with metadata", () => {
const testRule = {
name: "test-rule",
content: "Test content",
description: "Test description",
metadata: {
alwaysApply: true,
globs: ["src/**/*.ts"],
},
};
const result = StoredRuleConfigSchema.safeParse(testRule);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("test-rule");
expect(result.data.content).toBe("Test content");
expect(result.data.metadata?.alwaysApply).toBe(true);
expect(result.data.metadata?.globs).toEqual(["src/**/*.ts"]);
}
});
test("should validate rule without metadata", () => {
const testRule = {
name: "simple-rule",
content: "Simple content",
description: "Simple description",
};
const result = StoredRuleConfigSchema.safeParse(testRule);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("simple-rule");
expect(result.data.content).toBe("Simple content");
expect(result.data.metadata).toBeUndefined();
}
});
test("should reject invalid rule config", () => {
const invalidRule = {
name: "", // Empty name should fail
content: "Some content",
};
const result = StoredRuleConfigSchema.safeParse(invalidRule);
expect(result.success).toBe(false);
});
});
describe("JSON storage format tests", () => {
test("should save and load JSON rule with metadata", async () => {
const testRule = {
name: "test-api-rule",
content: "# API Guidelines\nUse proper error handling",
description: "Test rule for APIs",
metadata: {
description: "Test rule for APIs",
alwaysApply: false,
globs: ["src/api/**/*.ts"],
},
};
// Save to JSON file
const jsonPath = path.join(TEST_RULES_DIR, "test-api-rule.json");
await writeFile(jsonPath, JSON.stringify(testRule, null, 2));
// Verify file was created
expect(await fs.pathExists(jsonPath)).toBe(true);
// Load and verify content structure
const savedContent = await readFile(jsonPath, "utf-8");
const savedRule = JSON.parse(savedContent);
expect(savedRule.name).toBe("test-api-rule");
expect(savedRule.content).toBe("# API Guidelines\nUse proper error handling");
expect(savedRule.description).toBe("Test rule for APIs");
expect(savedRule.metadata.alwaysApply).toBe(false);
expect(savedRule.metadata.globs).toEqual(["src/api/**/*.ts"]);
});
test("should save and load JSON rule without metadata", async () => {
const testRule = {
name: "plain-rule",
content: "This is a plain rule without metadata",
description: "A simple rule",
};
// Save to JSON file
const jsonPath = path.join(TEST_RULES_DIR, "plain-rule.json");
await writeFile(jsonPath, JSON.stringify(testRule, null, 2));
// Load and verify
const savedContent = await readFile(jsonPath, "utf-8");
const savedRule = JSON.parse(savedContent);
expect(savedRule.name).toBe("plain-rule");
expect(savedRule.content).toBe("This is a plain rule without metadata");
expect(savedRule.description).toBe("A simple rule");
expect(savedRule.metadata).toBeUndefined();
});
test("should handle mixed JSON and .txt files", async () => {
// Create JSON rule
const jsonRule = {
name: "json-rule",
content: "JSON content",
};
const jsonPath = path.join(TEST_RULES_DIR, "json-rule.json");
await writeFile(jsonPath, JSON.stringify(jsonRule, null, 2));
// Create .txt rule
const txtPath = path.join(TEST_RULES_DIR, "txt-rule.txt");
await writeFile(txtPath, "TXT content");
// List files manually (simulating listCommonRules behavior)
const files = await readdir(TEST_RULES_DIR);
const ruleNames = new Set<string>();
for (const file of files) {
if (file.endsWith(".json")) {
ruleNames.add(file.replace(".json", ""));
} else if (file.endsWith(".txt")) {
ruleNames.add(file.replace(".txt", ""));
}
}
const rules = Array.from(ruleNames);
expect(rules).toContain("json-rule");
expect(rules).toContain("txt-rule");
expect(rules.length).toBe(2);
});
test("should deduplicate when both .json and .txt exist", async () => {
// Create both formats for same rule name
const jsonRule = {
name: "dual-rule",
content: "JSON content",
};
const jsonPath = path.join(TEST_RULES_DIR, "dual-rule.json");
await writeFile(jsonPath, JSON.stringify(jsonRule, null, 2));
const txtPath = path.join(TEST_RULES_DIR, "dual-rule.txt");
await writeFile(txtPath, "TXT content");
// List and deduplicate (simulating listCommonRules behavior)
const files = await readdir(TEST_RULES_DIR);
const ruleNames = new Set<string>();
for (const file of files) {
if (file.endsWith(".json")) {
ruleNames.add(file.replace(".json", ""));
} else if (file.endsWith(".txt")) {
ruleNames.add(file.replace(".txt", ""));
}
}
const rules = Array.from(ruleNames);
expect(rules).toContain("dual-rule");
expect(rules.length).toBe(1); // Should only appear once
});
});
});
```
## /src/utils/rule-storage.ts
```ts path="/src/utils/rule-storage.ts"
import * as fs from "fs-extra/esm";
import { writeFile, readFile, readdir } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleType, StoredRuleConfig } from "../types.js";
import { StoredRuleConfigSchema } from "../schemas.js";
import { getInternalRuleStoragePath, getCommonRulesDir } from "./path.js";
const RULE_EXTENSION = ".txt";
const STORED_RULE_EXTENSION = ".json";
/**
* Saves a rule definition to the internal storage (~/.vibe-rules/<ruleType>/<name>.txt).
* @param ruleType - The type of the rule, determining the subdirectory.
* @param config - The rule configuration object containing name and content.
* @returns The full path where the rule was saved.
* @throws Error if file writing fails.
*/
export async function saveInternalRule(ruleType: RuleType, config: RuleConfig): Promise<string> {
const storagePath = getInternalRuleStoragePath(ruleType, config.name);
const dir = path.dirname(storagePath);
await fs.ensureDir(dir); // Ensure the directory exists
await writeFile(storagePath, config.content, "utf-8");
console.debug(`[Rule Storage] Saved internal rule: ${storagePath}`);
return storagePath;
}
/**
* Loads a rule definition from the internal storage.
* @param ruleType - The type of the rule.
* @param name - The name of the rule to load.
* @returns The RuleConfig object if found, otherwise null.
*/
export async function loadInternalRule(
ruleType: RuleType,
name: string
): Promise<RuleConfig | null> {
const storagePath = getInternalRuleStoragePath(ruleType, name);
try {
if (!(await fs.pathExists(storagePath))) {
console.debug(`[Rule Storage] Internal rule not found: ${storagePath}`);
return null;
}
const content = await readFile(storagePath, "utf-8");
console.debug(`[Rule Storage] Loaded internal rule: ${storagePath}`);
return { name, content };
} catch (error: any) {
// Log other errors but still return null as the rule couldn't be loaded
console.error(
`[Rule Storage] Error loading internal rule ${name} (${ruleType}): ${error.message}`
);
return null;
}
}
/**
* Lists the names of all rules stored internally for a given rule type.
* @param ruleType - The type of the rule.
* @returns An array of rule names.
*/
export async function listInternalRules(ruleType: RuleType): Promise<string[]> {
// Use getInternalRuleStoragePath with a dummy name to get the directory path
const dummyPath = getInternalRuleStoragePath(ruleType, "__dummy__");
const storageDir = path.dirname(dummyPath);
try {
if (!(await fs.pathExists(storageDir))) {
console.debug(
`[Rule Storage] Internal rule directory not found for ${ruleType}: ${storageDir}`
);
return []; // Directory doesn't exist, no rules
}
const files = await readdir(storageDir);
const ruleNames = files
.filter((file) => file.endsWith(RULE_EXTENSION))
.map((file) => path.basename(file, RULE_EXTENSION));
console.debug(`[Rule Storage] Listed ${ruleNames.length} internal rules for ${ruleType}`);
return ruleNames;
} catch (error: any) {
console.error(`[Rule Storage] Error listing internal rules for ${ruleType}: ${error.message}`);
return []; // Return empty list on error
}
}
// --- Common Rule Storage Functions (for user-saved rules with metadata) ---
/**
* Saves a rule with metadata to the common storage (~/.vibe-rules/rules/<name>.json).
* @param config - The stored rule configuration object containing name, content, and metadata.
* @returns The full path where the rule was saved.
* @throws Error if validation or file writing fails.
*/
export async function saveCommonRule(config: StoredRuleConfig): Promise<string> {
// Validate the configuration
StoredRuleConfigSchema.parse(config);
const commonRulesDir = getCommonRulesDir();
const storagePath = path.join(commonRulesDir, `${config.name}${STORED_RULE_EXTENSION}`);
await fs.ensureDir(commonRulesDir);
await writeFile(storagePath, JSON.stringify(config, null, 2), "utf-8");
console.debug(`[Rule Storage] Saved common rule with metadata: ${storagePath}`);
return storagePath;
}
/**
* Loads a rule with metadata from the common storage.
* Falls back to legacy .txt format for backwards compatibility.
* @param name - The name of the rule to load.
* @returns The StoredRuleConfig object if found, otherwise null.
*/
export async function loadCommonRule(name: string): Promise<StoredRuleConfig | null> {
const commonRulesDir = getCommonRulesDir();
// Try new JSON format first
const jsonPath = path.join(commonRulesDir, `${name}${STORED_RULE_EXTENSION}`);
if (await fs.pathExists(jsonPath)) {
try {
const content = await readFile(jsonPath, "utf-8");
const parsed = JSON.parse(content);
const validated = StoredRuleConfigSchema.parse(parsed);
console.debug(`[Rule Storage] Loaded common rule from JSON: ${jsonPath}`);
return validated;
} catch (error: any) {
console.error(`[Rule Storage] Error parsing JSON rule ${name}: ${error.message}`);
return null;
}
}
// Fall back to legacy .txt format
const txtPath = path.join(commonRulesDir, `${name}${RULE_EXTENSION}`);
if (await fs.pathExists(txtPath)) {
try {
const content = await readFile(txtPath, "utf-8");
console.debug(`[Rule Storage] Loaded common rule from legacy .txt: ${txtPath}`);
return {
name,
content,
metadata: {}, // No metadata in legacy format
};
} catch (error: any) {
console.error(`[Rule Storage] Error loading legacy rule ${name}: ${error.message}`);
return null;
}
}
console.debug(`[Rule Storage] Common rule not found: ${name}`);
return null;
}
/**
* Lists the names of all rules stored in the common storage (both JSON and legacy .txt).
* @returns An array of rule names.
*/
export async function listCommonRules(): Promise<string[]> {
const commonRulesDir = getCommonRulesDir();
try {
if (!(await fs.pathExists(commonRulesDir))) {
console.debug(`[Rule Storage] Common rules directory not found: ${commonRulesDir}`);
return [];
}
const files = await readdir(commonRulesDir);
const ruleNames = new Set<string>();
// Add names from both .json and .txt files
files.forEach((file) => {
if (file.endsWith(STORED_RULE_EXTENSION)) {
ruleNames.add(path.basename(file, STORED_RULE_EXTENSION));
} else if (file.endsWith(RULE_EXTENSION)) {
ruleNames.add(path.basename(file, RULE_EXTENSION));
}
});
const result = Array.from(ruleNames);
console.debug(`[Rule Storage] Listed ${result.length} common rules`);
return result;
} catch (error: any) {
console.error(`[Rule Storage] Error listing common rules: ${error.message}`);
return [];
}
}
```
## /src/utils/similarity.ts
```ts path="/src/utils/similarity.ts"
/**
* Text similarity utilities for finding similar rule names
*/
/**
* Calculate Levenshtein distance between two strings
* @param a First string
* @param b Second string
* @returns Distance score (lower means more similar)
*/
export function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
// Initialize matrix
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
return matrix[b.length][a.length];
}
/**
* Calculate similarity score between two strings (0-1, higher means more similar)
* @param a First string
* @param b Second string
* @returns Similarity score between 0 and 1
*/
export function calculateSimilarity(a: string, b: string): number {
if (a === b) return 1; // Exact match
if (!a || !b) return 0; // Handle empty strings
const distance = levenshteinDistance(a.toLowerCase(), b.toLowerCase());
const maxLength = Math.max(a.length, b.length);
// Convert distance to similarity score (1 - normalized distance)
return 1 - distance / maxLength;
}
/**
* Find similar rule names to the given name
* @param notFoundName The rule name that wasn't found
* @param availableRules List of available rule names
* @param limit Maximum number of similar rules to return
* @returns Array of similar rule names sorted by similarity (most similar first)
*/
export function findSimilarRules(
notFoundName: string,
availableRules: string[],
limit: number = 5
): string[] {
if (!availableRules.length) return [];
// Calculate similarity for each rule
const scoredRules = availableRules.map((ruleName) => ({
name: ruleName,
score: calculateSimilarity(notFoundName, ruleName),
}));
// Sort by similarity score (highest first) and take the top n
return scoredRules
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((rule) => rule.name);
}
```
## /src/utils/single-file-helpers.ts
```ts path="/src/utils/single-file-helpers.ts"
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra/esm";
import * as path from "path";
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
import { createTaggedRuleBlock } from "./rule-formatter.js";
import { ensureDirectoryExists } from "./path.js";
import { debugLog } from "../cli.js";
/**
* Appends or updates a tagged block within a single target file.
*
* Reads the file content, looks for an existing block matching the rule name tag,
* replaces it if found, or appends the new block otherwise.
*
* @param targetPath The path to the target file.
* @param config The rule configuration.
* @param options Optional generator options.
* @param appendInsideVibeRulesBlock If true, tries to append within <vibe-rules Integration> block.
* @returns Promise resolving to true if successful, false otherwise.
*/
export async function appendOrUpdateTaggedBlock(
targetPath: string,
config: RuleConfig,
options?: RuleGeneratorOptions,
appendInsideVibeRulesBlock: boolean = false
): Promise<boolean> {
try {
// Ensure the PARENT directory exists, but only if it's not the current directory itself.
const parentDir = path.dirname(targetPath);
// Avoid calling ensureDirectoryExists('.') as it's unnecessary and might mask other issues.
if (parentDir !== ".") {
ensureDirectoryExists(parentDir);
}
let currentContent = "";
let fileExists = true;
try {
// Check if the file exists (not directory)
const stats = await fs.stat(targetPath).catch(() => null);
if (stats) {
if (stats.isDirectory()) {
// If it's a directory but should be a file, remove it
console.warn(
`Found a directory at ${targetPath} but expected a file. Removing directory...`
);
await fs.rm(targetPath, { recursive: true, force: true });
fileExists = false;
} else {
// It's a file, read its content
currentContent = await fs.readFile(targetPath, "utf-8");
}
} else {
fileExists = false;
}
} catch (error: any) {
if (error.code !== "ENOENT") {
console.error(`Error accessing target file ${targetPath}:`, error);
return false;
}
// File doesn't exist, which is fine, we'll create it.
fileExists = false;
debugLog(`Target file ${targetPath} not found, will create.`);
}
// If file doesn't exist, explicitly create an empty file first
// This ensures we're creating a file, not a directory
if (!fileExists) {
try {
// Ensure the file exists by explicitly creating it as an empty file
// Use fsExtra.ensureFileSync which is designed to create the file (not directory)
fsExtra.ensureFileSync(targetPath);
debugLog(`Created empty file: ${targetPath}`);
} catch (error) {
console.error(`Failed to create empty file ${targetPath}:`, error);
return false;
}
}
const newBlock = createTaggedRuleBlock(config, options);
const ruleNameRegexStr = config.name.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); // Escape regex special chars
const existingBlockRegex = new RegExp(
`<${ruleNameRegexStr}>[\\s\\S]*?</${ruleNameRegexStr}>`,
"m"
);
let updatedContent: string;
const match = currentContent.match(existingBlockRegex);
if (match) {
// Update existing block
debugLog(`Updating existing block for rule "${config.name}" in ${targetPath}`);
updatedContent = currentContent.replace(existingBlockRegex, newBlock);
} else {
// Append new block
debugLog(`Appending new block for rule "${config.name}" to ${targetPath}`);
// Check for comment-style integration blocks
const commentIntegrationEndTag = "<!-- /vibe-rules Integration -->";
const commentEndIndex = currentContent.lastIndexOf(commentIntegrationEndTag);
let integrationEndIndex = -1;
let integrationStartTag = "";
if (commentEndIndex !== -1) {
integrationEndIndex = commentEndIndex;
integrationStartTag = "<!-- vibe-rules Integration -->";
}
if (appendInsideVibeRulesBlock && integrationEndIndex !== -1) {
// Append inside the vibe-rules block if requested and found
const insertionPoint = integrationEndIndex;
updatedContent =
currentContent.slice(0, insertionPoint).trimEnd() +
"\n\n" + // Ensure separation
newBlock +
"\n\n" + // Ensure separation
currentContent.slice(insertionPoint);
debugLog(`Appending rule inside ${integrationStartTag} block.`);
} else if (appendInsideVibeRulesBlock && integrationEndIndex === -1) {
// Create the integration block if it doesn't exist and we want to append inside it
const separator = currentContent.trim().length > 0 ? "\n\n" : "";
const startTag = "<!-- vibe-rules Integration -->";
const endTag = "<!-- /vibe-rules Integration -->";
updatedContent =
currentContent.trimEnd() + separator + startTag + "\n\n" + newBlock + "\n\n" + endTag;
debugLog(`Created new ${startTag} block with rule.`);
} else {
// Append to the end
const separator = currentContent.trim().length > 0 ? "\n\n" : ""; // Add separator if file not empty
updatedContent = currentContent.trimEnd() + separator + newBlock;
if (appendInsideVibeRulesBlock) {
debugLog(`Could not find vibe-rules Integration block, appending rule to the end.`);
}
}
}
// Ensure file ends with a newline
if (!updatedContent.endsWith("\n")) {
updatedContent += "\n";
}
await fs.writeFile(targetPath, updatedContent, "utf-8");
console.log(`Successfully applied rule "${config.name}" to ${targetPath}`);
return true;
} catch (error) {
console.error(`Error applying rule "${config.name}" to ${targetPath}:`, error);
return false;
}
}
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
```
├── .cursor/
├── rules/
├── architecture.mdc (omitted)
├── examples.mdc (omitted)
├── good-behaviour.mdc (omitted)
├── vibe-tools.mdc (omitted)
├── yo.mdc (omitted)
├── .cursorindexingignore (omitted)
├── .github/
├── instructions/
├── examples-test.instructions.md (omitted)
├── workflows/
├── ci.yml (omitted)
├── .gitignore (omitted)
├── .npmignore (omitted)
├── .prettierrc (omitted)
├── .repomixignore (omitted)
├── LICENSE (omitted)
├── README.md (2k tokens)
├── TODO.md (omitted)
├── UNIFIED_RULES_CONVENTION.md (omitted)
├── bun.lock (omitted)
├── context.json (omitted)
├── examples/
├── README.md (omitted)
├── end-user-cjs-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── metadata-integration.test.ts (omitted)
├── package.json (omitted)
├── end-user-esm-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── package.json (omitted)
├── library-cjs-package/
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── library-esm-package/
├── README.md (omitted)
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── package.json (omitted)
├── reference/
├── README.md (omitted)
├── amp-rules-directory/
├── AGENT.md (omitted)
├── claude-code-directory/
├── CLAUDE.md (omitted)
├── cline-rules-directory/
├── .clinerules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── codex-rules-directory/
├── AGENTS.md (omitted)
├── cursor-rules-directory/
├── .cursor/
├── always-on.mdc (omitted)
├── glob.mdc (omitted)
├── manual.mdc (omitted)
├── model-decision.mdc (omitted)
├── windsurf-rules-directory/
├── .windsurf/
├── rules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── zed-rules-directory/
├── .rules (omitted)
├── scripts/
├── build-examples.ts (omitted)
├── test-examples.ts (omitted)
├── src/
├── cli.ts (500 tokens)
├── commands/
├── install.ts (omitted)
├── list.ts (omitted)
├── load.ts (omitted)
├── save.ts (omitted)
├── index.ts (omitted)
├── llms/
├── index.ts
├── internal.ts (800 tokens)
├── llms.txt (omitted)
├── providers/
├── amp-provider.ts (omitted)
├── claude-code-provider.ts (omitted)
├── clinerules-provider.ts (omitted)
├── codex-provider.ts (omitted)
├── cursor-provider.ts (omitted)
├── index.ts (omitted)
├── metadata-application.test.ts (omitted)
├── unified-provider.ts (omitted)
├── vscode-provider.ts (omitted)
├── windsurf-provider.ts (omitted)
├── zed-provider.ts (omitted)
├── schemas.ts (500 tokens)
├── text.d.ts (omitted)
├── types.ts (400 tokens)
├── utils/
├── frontmatter.test.ts (omitted)
├── frontmatter.ts (omitted)
├── path.ts (omitted)
├── rule-formatter.ts (omitted)
├── rule-storage.test.ts (omitted)
├── rule-storage.ts (omitted)
├── similarity.ts (omitted)
├── single-file-helpers.ts (omitted)
├── tsconfig.json (omitted)
├── vibe-tools.config.json (omitted)
```
## /README.md
# vibe-rules
[](https://www.npmjs.com/package/vibe-rules)
A powerful CLI tool for managing and sharing AI rules (prompts, configurations) across different editors and tools.
✨ **Supercharge your workflow!** ✨
## Quick Context Guide
This guide highlights the essential parts of the codebase and how you can quickly start working with **vibe-rules**.
[](https://contextjson.com/futureexcited/vibe-rules)
# Guide
Quickly save your favorite prompts and apply them to any supported editor:
```bash
# 1. Save your prompt locally
vibe-rules save my-react-helper -f ./prompts/react-helper.md
# 2. Apply it to Cursor
vibe-rules load my-react-helper cursor
```
Or, automatically install shared prompts from your project's NPM packages:
```bash
# Find packages with 'llms' exports in node_modules and install for Cursor
vibe-rules install cursor
```
(See full command details below.)
## Installation
```bash
# Install globally (bun recommended, npm/yarn also work)
bun i -g vibe-rules
```
## Usage
`vibe-rules` helps you save, list, load, and install AI rules.
### Save a Rule Locally
Store a rule for later use in the common `vibe-rules` store (`~/.vibe-rules/rules/`).
```bash
# Save from a file (e.g., .mdc, .md, or plain text)
vibe-rules save my-rule-name -f ./path/to/rule-content.md
# Save directly from content string
vibe-rules save another-rule -c "This is the rule content."
# Add/update a description
vibe-rules save my-rule-name -d "A concise description of what this rule does."
```
Options:
- `-f, --file <file>`: Path to the file containing the rule content.
- `-c, --content <content>`: Rule content provided as a string. (One of `-f` or `-c` is required).
- `-d, --description <desc>`: An optional description for the rule.
### List Saved Rules
See all the rules you've saved to the common local store.
```bash
vibe-rules list
```
### Load (Apply) a Saved Rule to an Editor
Apply a rule _from your local store_ (`~/.vibe-rules/rules/`) to a specific editor's configuration file. `vibe-rules` handles the formatting.
```bash
# Load 'my-rule-name' for Cursor (creates/updates .cursor/rules/my-rule-name.mdc)
vibe-rules load my-rule-name cursor
# Alias: vibe-rules add my-rule-name cursor
# Load 'my-rule-name' for Claude Code IDE globally (updates ~/.claude/CLAUDE.md)
vibe-rules load my-rule-name claude-code --global
# Alias: vibe-rules add my-rule-name claude-code -g
# Load 'my-rule-name' for Windsurf (appends to ./.windsurfrules)
vibe-rules load my-rule-name windsurf
# Load into the project's unified .rules file
vibe-rules load unified my-unified-rule
# Load into a specific custom target path
vibe-rules load my-rule-name cursor -t ./my-project/.cursor-rules/custom-rule.mdc
```
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`, `codex`, `clinerules`, `roo`, `vscode`.
Options:
- `-g, --global`: Apply to the editor's global configuration path (if supported, e.g., `claude-code`, `codex`). Defaults to project-local.
- `-t, --target <path>`: Specify a custom target file path or directory, overriding default/global paths.
### Sharing and Installing Rules via NPM
In the evolving AI landscape, prompts and context are becoming increasingly crucial components of development workflows. Just like code libraries, reusable AI rules and prompts are emerging as shareable assets.
We anticipate more NPM packages will begin exporting standardized AI configurations, often via a `llms` entry point (e.g., `my-package/llms`). `vibe-rules` embraces this trend with the `install` command.
### Install Rules from NPM Packages
âš¡ **The easiest way to integrate shared rule sets!** âš¡
Install rules _directly from NPM packages_ into an editor's configuration. `vibe-rules` automatically scans your project's dependencies or a specified package for compatible rule exports.
```bash
# Most common: Install rules from ALL dependencies/devDependencies for Cursor
# Scans package.json, finds packages with 'llms' export, applies rules.
vibe-rules install cursor
# Install rules from a specific package for Cursor
# (Assumes 'my-rule-package' is in node_modules)
vibe-rules install cursor my-rule-package
# Install rules from a specific package into a custom target dir for Roo/Cline
vibe-rules install roo my-rule-package -t ./custom-ruleset/
# Install rules into the project's unified .rules file
vibe-rules install unified my-awesome-prompts
```
Add the `--debug` global option to any `vibe-rules` command to enable detailed debug logging during execution. This can be helpful for troubleshooting installation issues.
Arguments:
- `<editor>`: The target editor/tool type (mandatory). Supported: `cursor`, `windsurf`, `claude-code`, `codex`, `clinerules`, `roo`, `vscode`.
- `[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:
- `-g, --global`: Apply to the editor's global configuration path (if supported).
- `-t, --target <path>`: Specify a custom target file path or directory.
**How `install` finds rules:**
1. Looks for installed NPM packages (either the specified one or all dependencies based on whether `[packageName]` is provided).
2. Attempts to dynamically import a module named `llms` from the package root (e.g., `require('my-rule-package/llms')` or `import('my-rule-package/llms')`). Handles both CommonJS and ESM.
3. Examines the `default export` of the `llms` module:
- **If it's a string:** Treats it as a single rule's content.
- **If it's an array:** Expects an array of rule strings or rule objects.
- Rule Object Shape: `{ name: string, rule: string, description?: string, alwaysApply?: boolean, globs?: string | string[] }` (Validated using Zod). Note the use of `rule` for content.
4. Uses the appropriate editor provider (`cursor`, `windsurf`, etc.) to format and apply each found rule to the correct target path (respecting `-g` and `-t` options). Metadata like `alwaysApply` and `globs` is passed to the provider if present in a rule object.
5. **Important:** Rules installed this way are applied _directly_ to the editor configuration; they are **not** saved to the common local store (`~/.vibe-rules/rules/`). Use `vibe-rules save` for that.
## Supported Editors & Formats
`vibe-rules` automatically handles formatting for:
- **Cursor (`cursor`)**:
- Creates/updates individual `.mdc` files in `./.cursor/rules/` (local) or `~/.cursor/rules/` (global, if supported via `-t`).
- Uses frontmatter for metadata (`description`, `alwaysApply`, `globs`).
- **Windsurf (`windsurf`)**:
- Appends rules wrapped in `<rule-name>` tags to `./.windsurfrules` (local) or a target file specified by `-t`. Global (`-g`) is not typically used.
- **Claude Code (`claude-code`)**:
- Appends/updates rules within XML-like tagged blocks in a `<!-- vibe-rules Integration -->` section in `./CLAUDE.md` (local) or `~/.claude/CLAUDE.md` (global).
- Each rule is encapsulated in tags like `<rule-name>...</rule-name>` within the single markdown file.
- Supports metadata formatting for `alwaysApply` and `globs` configurations.
- **Codex (`codex`)**:
- Appends/updates rules within XML-like tagged blocks in a `<!-- vibe-rules Integration -->` section in `./AGENTS.md` (local) or `~/.codex/AGENTS.md` (global).
- Each rule is encapsulated in tags like `<rule-name>...</rule-name>` within the single markdown file.
- Supports metadata formatting for `alwaysApply` and `globs` configurations.
- **Codex File Hierarchy**: Codex looks for `AGENTS.md` files in this order: `~/.codex/AGENTS.md` (global), `AGENTS.md` at repo root (default), and `AGENTS.md` in current working directory (use `--target` for subdirectories).
- **Amp (`amp`)**:
- Manages rules within a single `AGENT.md` 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 (similar to ZED).
- **Local only**: Does not support global files or subdirectory configurations.
- 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.
- **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.
- Follows the unified .rules convention and supports metadata formatting for `alwaysApply` and `globs` configurations.
- **VSCode (`vscode`)**:
- Creates/updates individual `.instructions.md` files within `./.github/instructions/` (project-local).
- Uses YAML frontmatter with `applyTo` field for file targeting.
- Rule name and description are included in the markdown content, not frontmatter.
- **Note:** Due to VSCode's limitations with multiple globs in the `applyTo` field, all rules use `applyTo: "**"` for universal application and better reliability.
- **Unified (`unified`)**:
- Manages rules within a single `.rules` file in the project root.
- Ideal for project-specific, centralized rule management.
- See the [Unified .rules File Convention](./UNIFIED_RULES_CONVENTION.md) for detailed format and usage.
## Development
```bash
# Clone the repo
git clone https://github.com/your-username/vibe-rules.git
cd vibe-rules
# Install dependencies
bun install
# Run tests (if any)
bun test
# Build the project
bun run build
# Link for local development testing
bun link
# Now you can use 'vibe-rules' command locally
```
## License
MIT
## /src/cli.ts
```ts path="/src/cli.ts"
#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";
import { installCommandAction } from "./commands/install.js";
import { saveCommandAction } from "./commands/save.js";
import { loadCommandAction } from "./commands/load.js";
import { listCommandAction } from "./commands/list.js";
// Simple debug logger
export let isDebugEnabled = false;
export const debugLog = (message: string, ...optionalParams: any[]) => {
if (isDebugEnabled) {
console.log(chalk.dim(`[Debug] ${message}`), ...optionalParams);
}
};
const program = new Command();
program
.name("vibe-rules")
.description(
"A utility for managing Cursor rules, Windsurf rules, Amp rules, and other AI prompts"
)
.version("0.1.0")
.option("--debug", "Enable debug logging", false);
program.on("option:debug", () => {
isDebugEnabled = program.opts().debug;
debugLog("Debug logging enabled.");
});
program
.command("save")
.description("Save a rule to the local store")
.argument("<n>", "Name of the rule")
.option("-c, --content <content>", "Rule content")
.option("-f, --file <file>", "Load rule content from file")
.option("-d, --description <desc>", "Rule description")
.action(saveCommandAction);
program
.command("list")
.description("List all saved rules from the common store")
.action(listCommandAction);
program
.command("load")
.alias("add")
.description("Apply a saved rule to an editor configuration")
.argument("<n>", "Name of the rule to apply")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(loadCommandAction);
program
.command("install")
.description(
"Install rules from an NPM package or all dependencies directly into an editor configuration"
)
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.argument("[packageName]", "Optional NPM package name to install rules from")
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(installCommandAction);
program.parse(process.argv);
if (process.argv.length <= 2) {
program.help();
}
```
## /src/llms/index.ts
```ts path="/src/llms/index.ts"
export default [];
```
## /src/llms/internal.ts
```ts path="/src/llms/internal.ts"
import { PackageRuleItem } from "../schemas.js";
// Content from llms.txt
const llmsTxtContent = `
# vibe-rules save
**Purpose:** Saves a rule to the local store (~/.vibe-rules/rules/). This allows you to manage a collection of reusable AI prompts.
**Usage:**
\`\`\`bash
vibe-rules save <name> [options]
\`\`\`
**Arguments:**
- \`<name>\`: The unique name for the rule.
**Options:**
- \`-c, --content <content>\`: Provide the rule content directly as a string.
- \`-f, --file <file>\`: Load the rule content from a specified file (e.g., .mdc or .md). One of --content or --file is required.
- \`-d, --description <desc>\`: An optional description for the rule.
# vibe-rules list
**Purpose:** Lists all rules that have been saved to the common local store (~/.vibe-rules/rules/).
**Usage:**
\`\`\`bash
vibe-rules list
\`\`\`
**Options:**
(None)
# vibe-rules load (or add)
**Purpose:** Applies a previously saved rule to the configuration file(s) of a specific editor or tool. This command formats the rule content correctly for the target editor and places it in the appropriate location.
**Usage:**
\`\`\`bash
vibe-rules load <name> <editor> [options]
vibe-rules add <name> <editor> [options] # Alias
\`\`\`
**Arguments:**
- \`<name>\`: The name of the rule to load/apply (must exist in the local store).
- \`<editor>\`: The target editor or tool type (e.g., cursor, windsurf, claude-code, codex, clinerules, roo).
**Options:**
- \`-g, --global\`: Apply the rule to the global configuration path if supported by the editor (currently supported for claude-code and codex). Defaults to project-local.
- \`-t, --target <path>\`: Specify a custom target file path or directory. This option overrides the default and global paths determined by the editor type.
# vibe-rules install
**Purpose:** Installs rules that are exported from an NPM package. The tool looks for a default export in a file named 'llms.ts' or similar within the package, expecting an array of rule configurations.
**Usage:**
\`\`\`bash
vibe-rules install [packageName]
\`\`\`
**Arguments:**
- \`[packageName]\` (Optional): The name of a specific NPM package to install rules from. If omitted, the command attempts to install rules from all dependencies listed in the current project's package.json file.
**Mechanism:**
- Looks for a module like \`<packageName>/llms\`.
- Expects the default export to be an array conforming to the VibeRulesSchema (array of { name, content, description? }).
- Saves each valid rule found into the common local store (~/.vibe-rules/rules/).
`;
const vibeRulesRepoRules: PackageRuleItem[] = [
{
name: "vibe-rules-provider-impl",
rule: "When adding a new RuleProvider implementation in `src/providers/`, ensure it correctly implements all methods defined in the `RuleProvider` interface (`src/types.ts`), handles both global and local paths appropriately using `src/utils/path.ts` utilities, and generates editor-specific formatting correctly.",
alwaysApply: true,
globs: ["src/providers/*.js", "src/providers/index.js"],
},
{
name: "vibe-rules-cli-commands",
rule: "When adding new CLI commands or modifying existing ones in `src/cli.ts`, ensure comprehensive argument parsing using `commander`, validation using Zod schemas from `src/schemas.ts` (if applicable), clear user feedback using `chalk`, and robust error handling for file operations and external calls.",
alwaysApply: true,
globs: ["src/cli.js"],
},
// Add the content from llms.txt as a general info rule
{
name: "vibe-rules-cli-docs",
rule: llmsTxtContent,
description: "Documentation for the vibe-rules CLI commands (save, list, load, install).",
alwaysApply: true,
globs: ["**/*"], // Apply globally as it's general context
},
];
export default vibeRulesRepoRules;
```
## /src/schemas.ts
```ts path="/src/schemas.ts"
import { z } from "zod";
export const RuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
// Add other fields from RuleConfig if they exist and need validation
});
// Schema for metadata in stored rules
export const RuleGeneratorOptionsSchema = z
.object({
description: z.string().optional(),
isGlobal: z.boolean().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(),
debug: z.boolean().optional(),
})
.optional();
// Schema for the enhanced local storage format with metadata
export const StoredRuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
metadata: RuleGeneratorOptionsSchema,
});
// Original schema for reference (might still be used elsewhere, e.g., save command)
export const VibeRulesSchema = z.array(RuleConfigSchema);
// --- Schemas for Package Exports (`<packageName>/llms`) ---
// Schema for the flexible rule object format within packages
export const PackageRuleObjectSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
rule: z.string().min(1, "Rule content cannot be empty"), // Renamed from content
description: z.string().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(), // Allow string or array
});
// Schema for a single item in the package export (either a string or the object)
export const PackageRuleItemSchema = z.union([
z.string().min(1, "Rule string cannot be empty"),
PackageRuleObjectSchema, // Defined above now
]);
// Schema for the default export of package/llms (array of strings or objects)
export const VibePackageRulesSchema = z.array(PackageRuleItemSchema);
// --- Type Helpers ---
// Basic RuleConfig type
export type RuleConfig = z.infer<typeof RuleConfigSchema>;
// Type for the enhanced local storage format
export type StoredRuleConfig = z.infer<typeof StoredRuleConfigSchema>;
// Type for rule generator options
export type RuleGeneratorOptions = z.infer<typeof RuleGeneratorOptionsSchema>;
// Type for the flexible package rule object
export type PackageRuleObject = z.infer<typeof PackageRuleObjectSchema>;
// Type for a single item in the package export array
export type PackageRuleItem = z.infer<typeof PackageRuleItemSchema>;
```
## /src/types.ts
```ts path="/src/types.ts"
/**
* Rule type definitions for the vibe-rules utility
*/
export type * from "./schemas.js";
export interface RuleConfig {
name: string;
content: string;
description?: string;
// Additional properties can be added as needed
}
// Extended interface for storing rules locally with metadata
export interface StoredRuleConfig {
name: string;
content: string;
description?: string;
metadata?: RuleGeneratorOptions;
}
export const RuleType = {
CURSOR: "cursor",
WINDSURF: "windsurf",
CLAUDE_CODE: "claude-code",
CODEX: "codex",
AMP: "amp",
CLINERULES: "clinerules",
ROO: "roo",
ZED: "zed",
UNIFIED: "unified",
VSCODE: "vscode",
CUSTOM: "custom",
} as const;
export type RuleTypeArray = (typeof RuleType)[keyof typeof RuleType][];
export type RuleType = (typeof RuleType)[keyof typeof RuleType];
export interface RuleProvider {
/**
* Creates a new rule file with the given content
*/
saveRule(config: RuleConfig, options?: RuleGeneratorOptions): Promise<string>;
/**
* Loads a rule from storage
*/
loadRule(name: string): Promise<RuleConfig | null>;
/**
* Lists all available rules
*/
listRules(): Promise<string[]>;
/**
* Appends a rule to an existing file
*/
appendRule(name: string, targetPath?: string): Promise<boolean>;
/**
* Formats and appends a rule directly from a RuleConfig object
*/
appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean>;
/**
* Generates formatted rule content with editor-specific formatting
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string;
}
export interface RuleGeneratorOptions {
description?: string;
isGlobal?: boolean;
alwaysApply?: boolean;
globs?: string | string[];
debug?: boolean;
// Additional options for specific rule types
}
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
```
├── .cursor/
├── rules/
├── architecture.mdc (omitted)
├── examples.mdc (omitted)
├── good-behaviour.mdc (omitted)
├── vibe-tools.mdc (omitted)
├── yo.mdc (omitted)
├── .cursorindexingignore (omitted)
├── .github/
├── instructions/
├── examples-test.instructions.md (omitted)
├── workflows/
├── ci.yml (omitted)
├── .gitignore (omitted)
├── .npmignore (omitted)
├── .prettierrc (omitted)
├── .repomixignore (omitted)
├── ARCHITECTURE.md (13.2k tokens)
├── LICENSE (omitted)
├── README.md (omitted)
├── TODO.md (omitted)
├── UNIFIED_RULES_CONVENTION.md (omitted)
├── bun.lock (omitted)
├── context.json (omitted)
├── examples/
├── README.md (omitted)
├── end-user-cjs-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── metadata-integration.test.ts (omitted)
├── package.json (omitted)
├── end-user-esm-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── package.json (omitted)
├── library-cjs-package/
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── library-esm-package/
├── README.md (omitted)
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── package.json (omitted)
├── public/
├── unified.png (omitted)
├── reference/
├── README.md (omitted)
├── amp-rules-directory/
├── AGENT.md (omitted)
├── claude-code-directory/
├── CLAUDE.md (omitted)
├── cline-rules-directory/
├── .clinerules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── codex-rules-directory/
├── AGENTS.md (omitted)
├── cursor-rules-directory/
├── .cursor/
├── always-on.mdc (omitted)
├── glob.mdc (omitted)
├── manual.mdc (omitted)
├── model-decision.mdc (omitted)
├── windsurf-rules-directory/
├── .windsurf/
├── rules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── zed-rules-directory/
├── .rules (omitted)
├── scripts/
├── build-examples.ts (omitted)
├── test-examples.ts (omitted)
├── src/
├── cli.ts (500 tokens)
├── commands/
├── install.ts (omitted)
├── list.ts (omitted)
├── load.ts (omitted)
├── save.ts (omitted)
├── index.ts (omitted)
├── llms/
├── index.ts (omitted)
├── internal.ts (omitted)
├── llms.txt (omitted)
├── providers/
├── amp-provider.ts (700 tokens)
├── claude-code-provider.ts (1100 tokens)
├── clinerules-provider.ts (1000 tokens)
├── codex-provider.ts (700 tokens)
├── cursor-provider.ts (800 tokens)
├── index.ts (300 tokens)
├── metadata-application.test.ts (2.1k tokens)
├── unified-provider.ts (600 tokens)
├── vscode-provider.ts (800 tokens)
├── windsurf-provider.ts (600 tokens)
├── zed-provider.ts (400 tokens)
├── schemas.ts (500 tokens)
├── text.d.ts (omitted)
├── types.ts (400 tokens)
├── utils/
├── frontmatter.test.ts (600 tokens)
├── frontmatter.ts (700 tokens)
├── path.ts (1600 tokens)
├── rule-formatter.ts (400 tokens)
├── rule-storage.test.ts (1300 tokens)
├── rule-storage.ts (1400 tokens)
├── similarity.ts (500 tokens)
├── single-file-helpers.ts (1200 tokens)
├── tsconfig.json (omitted)
├── vibe-tools.config.json (omitted)
```
## /ARCHITECTURE.md
# vibe-rules Architecture
This document outlines the architecture of the vibe-rules utility - a tool for managing AI prompts for different editors.
**Note:** The tool is intended for global installation via `bun i -g vibe-rules`.
## Key Dependencies
The project relies on several critical dependencies for its functionality:
- **import-meta-resolve**: Ponyfill for `import.meta.resolve()` functionality, enabling module path resolution without the need for Node.js experimental flags. Essential for the dual CommonJS/ESM module loading strategy in the install command.
- **commander**: CLI framework for handling command-line interface, argument parsing, and command routing.
- **zod**: Schema validation library used for validating rule configurations and package exports.
- **fs-extra**: Enhanced file system utilities with promise support, used throughout for file operations.
- **chalk**: Terminal styling library for colored console output and improved user experience.
## Project Structure
```
vibe-rules/
├── src/ # Source code
│ ├── cli.ts # Command-line interface
│ ├── commands/ # CLI command action handlers (Added)
│ │ └── install.ts # Action handler for the 'install' command (Added)
│ ├── index.ts # Main exports
│ ├── types.ts # Type definitions (Updated: RuleType.UNIFIED)
│ ├── schemas.ts # Zod schema definitions
│ ├── llms/ # Rule definitions for package export
│ │ ├── index.ts # Public rule export (intentionally empty)
│ │ └── internal.ts # Internal rule definitions (not exported)
│ ├── providers/ # Provider implementations
│ │ ├── index.ts # Provider factory (Updated: UnifiedRuleProvider)
│ │ ├── cursor-provider.ts # Cursor editor provider
│ │ ├── windsurf-provider.ts # Windsurf editor provider
│ │ ├── claude-code-provider.ts # Claude Code provider
│ │ ├── codex-provider.ts # Codex provider (Updated: AGENTS.md support)
│ │ ├── amp-provider.ts # Amp provider (Added: AGENT.md support, local only)
│ │ ├── clinerules-provider.ts # Clinerules/Roo provider
│ │ ├── zed-provider.ts # Zed editor provider
│ │ └── unified-provider.ts # Unified .rules provider (Added)
│ └── utils/ # Utility functions
│ ├── path.ts # Path helpers (Updated: Codex AGENTS.md paths)
│ ├── similarity.ts # Text similarity utilities
│ └── rule-formatter.ts # Rule formatting utilities for metadata
│ └── single-file-helpers.ts # Helpers for single-file providers
│ └── rule-storage.ts # Helpers for internal rule storage
├── examples/ # Example packages for end-users and library authors
│ ├── end-user-cjs-package/ # CommonJS project consuming rules from multiple packages (Updated)
│ │ ├── src/
│ │ ├── install.test.ts # Integration test for vibe-rules install functionality (Updated: AGENTS.md)
│ │ └── package.json # Uses Bun for testing, includes vibe-rules script (Updated)
│ ├── end-user-esm-package/ # ES Module project consuming rules from multiple packages
│ ├── library-cjs-package/ # CommonJS library that exports AI prompt rules
│ ├── library-esm-package/ # ES Module library that exports AI prompt rules
│ └── README.md # Comprehensive guide to examples (Updated)
├── reference/ # Reference implementations for different editors
│ ├── cursor-rules-directory/ # Example Cursor workspace rules structure
│ ├── windsurf-rules-directory/ # Example Windsurf workspace rules structure
│ ├── cline-rules-directory/ # Example Cline workspace rules structure (Added)
│ ├── codex-rules-directory/ # Example CODEX workspace rules structure (Updated: AGENTS.md)
│ ├── amp-rules-directory/ # Example Amp workspace rules structure (Added: AGENT.md)
│ ├── zed-rules-directory/ # Example ZED editor rules structure (Added)
│ └── README.md # Documentation of reference structures (Updated: Codex AGENTS.md, Amp AGENT.md)
├── web/ # Web interface
│ ├── pages/ # Vue/Nuxt pages
│ │ └── index.vue # Landing page
│ ├── public/ # Public assets
│ └── nuxt.config.ts # Nuxt configuration
├── package.json # Project metadata and dependencies (including import-meta-resolve polyfill, dynamic build/test scripts, CI automation)
├── scripts/ # Build and utility scripts
│ ├── build-examples.ts # Dynamic parallel script for building all example projects
│ └── test-examples.ts # Dynamic parallel script for running tests across all example projects
├── .github/ # GitHub Actions workflows
│ └── workflows/
│ └── ci.yml # Continuous Integration workflow
├── README.md # Project documentation (Updated: Codex AGENTS.md references)
├── ARCHITECTURE.md # This file (Updated: Codex AGENTS.md integration)
├── UNIFIED_RULES_CONVENTION.md # Documentation for .rules (Added)
```
## File Descriptions
### scripts/build-examples.ts (Added)
A dynamic TypeScript script that automatically discovers and builds all example projects in the `examples/` directory using Bun shell for parallel execution.
#### Key Features
- **Dynamic Discovery**: Automatically finds all directories in `examples/` that contain a `package.json` file
- **Parallel Execution**: Runs npm install and build operations for all projects simultaneously using `Promise.all()`
- **Smart Build Detection**: Determines whether a project needs building by checking for a `build` script in `package.json`
- **Bun Shell Integration**: Uses Bun's shell (`$`) for efficient command execution
- **Comprehensive Logging**: Provides detailed progress feedback with emojis and clear status messages
- **Error Handling**: Gracefully handles failures and provides detailed error reporting
#### Functions
- `discoverExampleProjects(): Promise<ExampleProject[]>`
- Scans the `examples/` directory for subdirectories containing `package.json`
- Returns an array of project metadata including name, path, and build requirements
- Skips directories without `package.json` files
- `buildProject(project: ExampleProject): Promise<void>`
- Executes npm install for a single project
- Optionally runs npm build if the project has a build script
- Provides progress logging for each step
- `main(): Promise<void>`
- Orchestrates the entire build process
- Runs all project builds in parallel
- Provides summary statistics of built vs install-only projects
#### Usage
The script is executed via the `build:examples` npm script:
```bash
npm run build:examples
```
This replaces the previous hardcoded script with a dynamic solution that automatically adapts to new example projects without requiring configuration changes.
### scripts/test-examples.ts (Added)
A dynamic TypeScript script that automatically discovers and runs tests for all example projects in the `examples/` directory using Bun shell, with comprehensive output capture and reporting.
#### Key Features
- **Dynamic Discovery**: Automatically finds all directories in `examples/` that contain a `package.json` file with a valid test script
- **Parallel Execution**: Runs all test suites simultaneously using `Promise.allSettled()` to ensure all tests run even if some fail
- **Smart Test Detection**: Filters out placeholder test scripts (like `echo "Error: no test specified" && exit 1`)
- **Comprehensive Output Capture**: Captures both stdout and stderr from each test run for detailed reporting
- **Failure Tolerance**: Continues running all tests even when some fail, providing complete results
- **Rich Reporting**: Provides detailed summaries with timing, success rates, and organized output display
- **Bun Shell Integration**: Uses Bun's shell (`$`) for efficient command execution
#### Functions
- `discoverTestableProjects(): Promise<ExampleProject[]>`
- Scans the `examples/` directory for subdirectories containing `package.json` with valid test scripts
- Returns an array of project metadata including name, path, and test script command
- Skips directories without `package.json` files or placeholder test scripts
- `runProjectTests(project: ExampleProject): Promise<TestResult>`
- Executes the test command for a single project with timing measurement
- Captures both success and failure output for comprehensive reporting
- Returns detailed test results including duration, output, and error information
- `formatDuration(ms: number): string`
- Formats millisecond durations into human-readable strings (ms, s, m)
- `printTestResults(results: TestResult[]): void`
- Presents comprehensive test results in organized sections:
- Summary with pass/fail counts and success rate
- Detailed failure output for debugging failed tests
- Condensed passed test output (last 10 lines) for verification
- `main(): Promise<void>`
- Orchestrates the entire test process
- Uses `Promise.allSettled()` to run all tests in parallel without stopping on failures
- Provides final summary and appropriate exit codes
#### Output Format
The script provides structured output with multiple sections:
1. **Discovery Phase**: Lists all testable projects found
2. **Execution Phase**: Real-time progress updates as tests run
3. **Summary Section**: Pass/fail counts, timing, and success rate
4. **Detailed Failure Output**: Full output and error details for failed tests
5. **Passed Tests Output**: Last 10 lines of output for successful tests
#### Usage
The script is executed via the `test:examples` npm script:
```bash
npm run test:examples
```
This provides a comprehensive view of test results across all example projects, making it easy to identify and debug issues while ensuring all tests are run regardless of individual failures.
### .github/workflows/ci.yml (Added)
A comprehensive GitHub Actions workflow that provides continuous integration for the vibe-rules project, supporting both Bun and npm package managers.
#### Key Features
- **Dual Package Manager Support**: Sets up both Bun (for main project) and Node.js/npm (for example packages)
- **Comprehensive Testing**: Runs the complete build and test pipeline using our dynamic scripts
- **Efficient Caching**: Leverages Bun's fast dependency resolution and GitHub Actions runner caching
- **Multi-Branch Support**: Triggers on pushes to any branch (`branches: [ '*' ]`)
- **Smart Concurrency Control**: Automatically cancels older runs when new pushes are made to the same branch
- **Clear Progress Indicators**: Uses emojis and descriptive step names for easy monitoring
#### Workflow Configuration
- **Concurrency Control**: Uses `group: ci-${{ github.ref }}` with `cancel-in-progress: true` to automatically cancel older runs when new commits are pushed to the same branch
- **Branch Coverage**: Triggers on pushes to any branch (`branches: [ '*' ]`) for comprehensive testing
#### Workflow Steps
1. **Repository Checkout**: Uses `actions/checkout@v4` to get the latest code
2. **Bun Setup**: Uses `oven-sh/setup-bun@v2` with latest version for running our scripts
3. **Node.js Setup**: Uses `actions/setup-node@v4` with Node 20 to provide npm binary for example packages
4. **Dependency Installation**: Runs `bun install` to install root project dependencies
5. **Main Build**: Executes `bun run build` (TypeScript compilation via `tsc`)
6. **Example Build**: Runs `bun run build:examples` using our dynamic build script
7. **Example Testing**: Executes `bun run test:examples` using our comprehensive test runner
#### CI Pipeline Flow
```
Repository Checkout
↓
Setup Bun + Node.js (with caching)
↓
Install root dependencies
↓
Build main project (TypeScript)
↓
Build all example projects (parallel)
↓
Run all example tests (parallel with failure tolerance)
↓
Success confirmation
```
#### Benefits
- **Fast Execution**: Bun's speed for script execution and dependency management
- **Resource Efficient**: Concurrency controls cancel outdated runs, saving GitHub Actions quota
- **Robust Testing**: All example projects are built and tested automatically
- **Developer Friendly**: Clear step names and emoji indicators for easy monitoring
- **Failure Tolerance**: Tests continue running even if some examples fail, providing complete results
- **Scalable**: Automatically adapts to new example projects without workflow changes
This CI setup ensures that every change to the vibe-rules project is thoroughly validated across all supported use cases and package formats.
### src/cli.ts
Defines the command-line interface using `commander`.
It handles parsing of command-line arguments, options, and delegates the execution of command actions to handlers in `src/commands/`.
#### Global Variables/Functions
- `isDebugEnabled: boolean` (Exported)
- A global flag indicating if debug logging is active. Controlled by the `--debug` CLI option.
- `debugLog(message: string, ...optionalParams: any[]): void` (Exported)
- A global function for logging debug messages. Output is conditional based on `isDebugEnabled`.
#### Helper Functions (Internal to cli.ts)
- `saveRule(ruleConfig: RuleConfig, fileContent?: string): Promise<void>` (Added)
- Enhanced version of the previous `installRule` function with metadata extraction support.
- **Frontmatter Parsing**: When `fileContent` is provided (from `--file` option), extracts metadata from YAML-like frontmatter using `parseFrontmatter` utility.
- **Metadata Extraction**: Extracts `description`, `alwaysApply`, and `globs` from frontmatter and stores them separately from content.
- **JSON Storage**: Uses `saveCommonRule` to store rules as JSON files with metadata instead of plain .txt files.
- **User Feedback**: Provides detailed output showing extracted metadata (alwaysApply, globs) when saving .mdc files.
- **Content Separation**: Automatically separates frontmatter from content, storing only the content body in the rule.
- `clearExistingRules(pkgName: string, editorType: RuleType, options: { global?: boolean; target?: string }): Promise<void>` (Added)
- Clears previously installed rule files associated with a specific NPM package before installing new ones.
- Determines the target directory based on `editorType` and `options`.
- **Behavior for Single-File Providers** (e.g., Windsurf, Claude Code, Codex):
- Reads the determined single target file (e.g., `.windsurfrules`, `CLAUDE.md`).
- Removes any XML-like blocks within the file where the tag name starts with the package prefix (e.g., finds and removes `<pkgName_rule1>...</pkgName_rule1>`, `<pkgName_anotherRule>...</pkgName_anotherRule>`).
- Writes the modified content back to the file.
- Does _not_ delete the file itself.
- **Behavior for Multi-File Providers** (e.g., Cursor, Clinerules):
- Deletes files within the target directory whose names start with `${pkgName}_`.
#### Commands
- `save <name> [options]` (Enhanced)
- Saves a rule to the local store with enhanced metadata support.
- **Metadata Extraction**: When using `--file` with .mdc files, automatically extracts and stores metadata (description, alwaysApply, globs) from YAML frontmatter.
- **JSON Storage**: Rules are now saved as `<name>.json` files in `~/.vibe-rules/rules/` with structured metadata.
- **Backward Compatibility**: Still supports simple content and description options for rules without metadata.
- Uses the `saveRule` helper function with automatic frontmatter parsing.
- `list` (Enhanced)
- Lists all rules saved in the common local store (`~/.vibe-rules/rules`).
- **Dual Format Support**: Now lists both new JSON rules and legacy .txt rules in a unified display.
- Uses `listCommonRules` utility function for improved compatibility.
- `load <name> <editor> [options]` (Alias: `add`) (Enhanced)
- Applies a saved rule to a specific editor's configuration file with automatic metadata application.
- **Metadata Loading**: Automatically loads and applies stored metadata (alwaysApply, globs) when available.
- **Cross-Editor Compatibility**: Stored metadata is automatically adapted to each editor's format:
- **Cursor**: Applied as YAML frontmatter in .mdc files
- **VSCode**: Description applied, globs ignored due to VSCode limitations
- **Single-file providers** (Windsurf, Zed, etc.): Converted to human-readable text
- **Backward Compatibility**: Gracefully handles legacy .txt files without metadata.
- **Metadata Display**: Shows applied metadata in console output for user confirmation.
- Uses `loadCommonRule` for enhanced rule loading with automatic fallback.
- `install <editor> [packageName] [options]`
- Defines the CLI options and arguments for the install command.
- Delegates the action to `installCommandAction` from `src/commands/install.ts`.
### src/commands/install.ts (Added)
Contains the action handler for the `vibe-rules install` command and handles the complex module loading requirements for both CommonJS and ES modules.
### src/commands/save.ts (Added)
Contains the action handler for the `vibe-rules save` command with enhanced metadata extraction and storage capabilities.
#### Functions
- `saveCommandAction(name: string, options: { content?: string; file?: string; description?: string }): Promise<void>` (Exported)
- Main handler for the `save` command.
- Supports saving rules from either `--content` or `--file` options.
- Automatically extracts metadata from .mdc files when using `--file`.
- Delegates to `saveRule` helper function for actual processing.
- `extractMetadata(fileContent: string, ruleConfig: RuleConfig): { metadata: RuleGeneratorOptions; content: string }`
- Extracts metadata from YAML frontmatter in file content.
- Updates rule config with extracted description if not already set.
- Returns both extracted metadata and clean content (without frontmatter).
- `displayMetadata(metadata: RuleGeneratorOptions): void`
- Displays extracted metadata in console output for user feedback.
- Shows alwaysApply and globs information when available.
- `saveRule(ruleConfig: RuleConfig, fileContent?: string): Promise<void>`
- Helper function that handles the actual rule saving with metadata support.
- Uses `saveCommonRule` to store rules as JSON with metadata.
- Provides detailed console feedback about saved metadata.
### src/commands/load.ts (Added)
Contains the action handler for the `vibe-rules load` command with automatic metadata application across editors.
#### Functions
- `loadCommandAction(name: string, editor: string, options: { global?: boolean; target?: string }): Promise<void>` (Exported)
- Main handler for the `load` command.
- Uses `loadCommonRule` to load rules with metadata support.
- Automatically applies stored metadata to the target editor format.
- Provides user feedback showing applied metadata.
- Supports similar rule suggestions when rule is not found.
- `displayAppliedMetadata(metadata: RuleGeneratorOptions): void`
- Displays applied metadata information in console output.
- Shows which metadata was applied to the target editor.
### src/commands/list.ts (Added)
Contains the action handler for the `vibe-rules list` command with unified rule listing support.
#### Functions
- `listCommandAction(): Promise<void>` (Exported)
- Main handler for the `list` command.
- Uses `listCommonRules` to list both JSON and legacy .txt rules.
- Provides unified display of all available rules regardless of storage format.
#### Dependencies
- **import-meta-resolve**: A ponyfill for the native `import.meta.resolve()` functionality that resolves module specifiers to URLs without loading the module. This is required because Node.js's native `import.meta.resolve()` requires the `--experimental-import-meta-resolve` flag. The polyfill enables synchronous module path resolution and supports import maps and export maps, making it essential for the dynamic module loading strategy in `importModuleFromCwd`.
#### Functions
- `installCommandAction(editor: string, packageName: string | undefined, options: { global?: boolean; target?: string; debug?: boolean }): Promise<void>` (Exported)
- Main handler for the `install` command.
- If `packageName` is provided, it calls `installSinglePackage` for that specific package.
- If `packageName` is not provided, it reads `package.json`, gets all dependencies and devDependencies, and calls `installSinglePackage` for each.
- Uses `getRuleProvider` to get the appropriate provider for the editor.
- Handles overall error reporting for the command.
- `installSinglePackage(pkgName: string, editorType: RuleType, provider: RuleProvider, installOptions: { global?: boolean; target?: string; debug?: boolean }): Promise<number>`
- Installs rules from a single specified NPM package.
- **Pre-validation**: Checks if the package exports `./llms` in its `package.json` before attempting import.
- Creates the necessary directories for the specified editor type (e.g., `.cursor/` for Cursor, `.clinerules/` for Clinerules).
- Dynamically imports `<pkgName>/llms` using `importModuleFromCwd`.
- Calls `clearExistingRules` before processing new rules.
- Validates the imported module content (string or array of rules/rule objects) using `VibePackageRulesSchema`.
- **Rule Processing**: Handles both simple string rules and complex rule objects with metadata.
- For each rule, determines the final target path and uses `provider.appendFormattedRule` to apply it.
- Prefixes rule names with `${pkgName}_` if not already present.
- **Concise User Feedback (NEW)**: Upon successful application of each rule, the function now emits a short, always-on log line in the format `[vibe-rules] Installed rule "<ruleName>" from package "<pkgName>".` (only when **not** in debug mode). This gives end-users immediate visibility into which specific rules were installed from which package.
- **Metadata Handling**: Extracts and applies metadata (`alwaysApply`, `globs`) from rule objects to the provider options.
- **Error Handling**: Provides specific error messages for different failure modes (module not found, syntax errors, initialization errors).
- Returns the count of successfully applied rules.
- `clearExistingRules(pkgName: string, editorType: RuleType, options: { global?: boolean }): Promise<void>`
- Clears previously installed rules for a given package and editor type.
- **Single-File Providers** (Windsurf, Claude Code, Codex): Uses regex to remove XML-like blocks matching `<pkgName_ruleName>...</pkgName_ruleName>` patterns from the target file.
- **Multi-File Providers** (Cursor, Clinerules): Deletes files starting with `${pkgName}_` in the target directory.
- Handles cases where target paths don't exist gracefully.
- `importModuleFromCwd(ruleModulePath: string): Promise<any>`
- **Dual-Strategy Module Loading**: Implements a robust approach for loading modules that works with both CommonJS and ES modules.
- **Primary Strategy**: Attempts `createRequire()` with the current working directory's `package.json` as the context for CommonJS compatibility.
- **Fallback Strategy**: When `require()` fails (typically for ESM-only modules), uses `import-meta-resolve` to resolve the module specifier to a full URL, then performs dynamic `import()`.
- **Resolution Process**:
- Constructs a file URL from the current working directory's `package.json`
- Uses `importMetaResolve(ruleModulePath, fileUrlString)` to resolve the module path
- Performs dynamic import with the resolved path
- **Error Handling**: Provides detailed debug logging for each step and comprehensive error reporting.
- Returns the default export or the module itself, handling both export patterns.
### src/types.ts
Defines the core types and interfaces used throughout the application.
#### `RuleConfig`
- Interface for storing rule information
- Properties:
- `name`: string - The name of the rule
- `content`: string - The content of the rule
- `description?`: string - Optional description
#### `StoredRuleConfig` (Added)
- **Enhanced Interface**: Extended interface for storing rules locally with metadata
- Properties:
- `name`: string - The name of the rule
- `content`: string - The content of the rule
- `description?`: string - Optional description
- `metadata?`: RuleGeneratorOptions - Optional metadata including alwaysApply, globs, etc.
- **Purpose**: Used for the new JSON-based local storage format that preserves metadata from .mdc files
#### `RuleType`
- Enum defining supported editor types
- Values:
- `CURSOR`: "cursor" - For Cursor editor
- `WINDSURF`: "windsurf" - For Windsurf editor
- `CLAUDE_CODE`: "claude-code" - For Claude Code IDE
- `CODEX`: "codex" - For Codex IDE
- `AMP`: "amp" - For Amp AI coding assistant (Added)
- `CLINERULES`: "clinerules" - For Cline/Roo IDEs
- `ROO`: "roo" - Alias for CLINERULES
- `ZED`: "zed" - For Zed editor
- `UNIFIED`: "unified" - For the unified `.rules` file convention (Added)
- `VSCODE`: "vscode" - For Visual Studio Code (Added)
- `CUSTOM`: "custom" - For custom implementations
#### `RuleProvider`
- Interface that providers must implement
- Methods:
- `saveRule(config: RuleConfig, options?: RuleGeneratorOptions): Promise<string>` - Saves a rule definition (often internally) and returns the path
- `loadRule(name: string): Promise<RuleConfig | null>` - Loads a rule definition by name
- `listRules(): Promise<string[]>` - Lists all available rule definitions
- `appendRule(name: string, targetPath?: string, isGlobal?: boolean): Promise<boolean>` - Loads a rule definition and applies it to a target file/directory, considering global/local context
- `appendFormattedRule(config: RuleConfig, targetPath: string, isGlobal?: boolean, options?: RuleGeneratorOptions): Promise<boolean>` - Formats and applies a rule definition directly
- `generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string` - Generates formatted rule content suitable for the specific provider/IDE
#### `RuleGeneratorOptions`
- Interface for optional configuration when generating or applying rules
- Properties:
- `description?`: string - Custom description (used by some providers like Cursor)
- `isGlobal?`: boolean - Hint for providers supporting global/local paths (e.g., Claude, Codex)
- `alwaysApply?`: boolean - Cursor-specific metadata.
- `globs?`: string | string[] - Cursor-specific metadata.
- `debug?`: boolean - Debug logging flag
### src/schemas.ts (Added)
Defines Zod schemas for validating rule configurations.
#### `RuleConfigSchema`
- Zod schema corresponding to the `RuleConfig` interface.
- Validates `name` (non-empty string), `content` (non-empty string), and optional `description` (string).
- Used by the `save` command and potentially internally.
#### `RuleGeneratorOptionsSchema` (Added)
- **Metadata Validation**: Zod schema for validating rule metadata and generator options.
- **Properties**: Validates all optional properties:
- `description?: string` - Optional description override
- `isGlobal?: boolean` - Global context flag
- `alwaysApply?: boolean` - Cursor-specific always-apply setting
- `globs?: string | string[]` - File glob patterns (string or array of strings)
- `debug?: boolean` - Debug logging flag
- **Optional Schema**: The entire schema is wrapped as optional to allow rules without metadata.
#### `StoredRuleConfigSchema` (Added)
- **Enhanced Validation**: Zod schema for the new JSON storage format with metadata support.
- **Properties**:
- `name`: string (non-empty, required)
- `content`: string (non-empty, required)
- `description?: string` (optional)
- `metadata?: RuleGeneratorOptionsSchema` (optional, uses the metadata schema above)
- **Purpose**: Validates the structure of JSON files saved by the new metadata storage system.
#### `PackageRuleObjectSchema` (Added)
- Zod schema for the flexible rule object structure found in `package/llms` exports.
- Validates:
- `name`: string (non-empty)
- `rule`: string (non-empty) - Note: uses `rule` field for content.
- `description?`: string
- `alwaysApply?`: boolean - Cursor-specific metadata.
- `globs?`: string | string[] - Cursor-specific metadata.
#### `PackageRuleItemSchema` (Added)
- Zod schema representing a single item in the `package/llms` array export.
- It's a union of:
- `z.string().min(1)`: A simple rule string.
- `PackageRuleObjectSchema`: The flexible rule object.
#### `VibePackageRulesSchema` (Added)
- Zod schema for the entire default export of `package/llms` when it's an array.
- Defined as `z.array(PackageRuleItemSchema)`.
- Used by the `install` command to validate package exports.
#### `VibeRulesSchema`
- Original Zod schema for an array of basic `RuleConfigSchema`. Kept for potential other uses but **not** the primary schema for the `install` command anymore.
### src/utils/path.ts
Provides utility functions for managing file paths related to rules and IDE configurations.
Utilizes `debugLog` from `cli.ts` for conditional logging in `ensureDirectoryExists`.
#### `RULES_BASE_DIR`
- Constant storing the base directory for vibe-rules internal storage (`~/.vibe-rules`)
#### `CLAUDE_HOME_DIR`, `CODEX_HOME_DIR`
- Constants storing the conventional home directories for Claude (`~/.claude`) and Codex (`~/.codex`)
#### `getCommonRulesDir(): string`
- Gets (and ensures exists) the directory for storing common rule definitions within `RULES_BASE_DIR`
- Returns: The path to `~/.vibe-rules/rules`
#### `getInternalRuleStoragePath(ruleType: RuleType, ruleName: string): string`
- Gets the path for storing internal rule _definitions_ based on type.
- Parameters:
- `ruleType`: The type of rule
- `ruleName`: The name of the rule
- Returns: Path within `~/.vibe-rules/<ruleType>/<ruleName>.txt`
#### `getRulePath(ruleType: RuleType, ruleName: string, isGlobal: boolean = false, projectRoot: string = process.cwd()): string`
- Gets the _actual_ expected file or directory path where a rule should exist for the target IDE/tool.
- Parameters:
- `ruleType`: The type of rule
- `ruleName`: The name of the rule (used by some types like Cursor)
- `isGlobal`: Flag indicating global context (uses home dir paths for Claude/Codex)
- `projectRoot`: The root directory for local project paths
- Returns: The specific path (e.g., `~/.claude/CLAUDE.md`, `./.cursor/rules/my-rule.mdc`, `./.clinerules`, `./.rules` for Zed and Unified)
#### `getDefaultTargetPath(ruleType: RuleType, isGlobalHint: boolean = false): string`
- Gets the default target directory or file path where rules of a certain type are typically applied (used by commands like `apply` if no target is specified).
- Parameters:
- `ruleType`: The type of rule/editor
- `isGlobalHint`: Hint for global context
- Returns: The conventional default path (e.g., `~/.codex`, `./.cursor/rules`, `./.clinerules`, `./.rules` for Zed and Unified)
#### `editorConfigExists(ruleType: RuleType, isGlobal: boolean, projectRoot: string = process.cwd()): Promise<boolean>` (Updated)
- Utility function that checks if the configuration for a given editor type exists in the project.
- Currently available for potential future use but not actively used by install command.
- Parameters:
- `ruleType`: The editor type to check
- `isGlobal`: Whether to check the global or local path
- `projectRoot`: The root directory of the project (defaults to current working directory)
- Returns: A promise that resolves to `true` if the configuration exists, `false` otherwise
- **Behavior by Editor Type:**
- `CURSOR`: Checks for `.cursor` directory
- `WINDSURF`: Checks for `.windsurfrules` file
- `CLINERULES`/`ROO`: Checks for `.clinerules` directory
- `ZED`/`UNIFIED`: Checks for `.rules` file
- `CLAUDE_CODE`: Checks for `CLAUDE.md` (global: `~/.claude/CLAUDE.md`, local: `./CLAUDE.md`)
- `CODEX`: Checks for AGENTS.md file (global: `~/.codex/AGENTS.md`, local: `./AGENTS.md`)
- `AMP`: Checks for AGENT.md file (local only: `./AGENT.md`)
#### `slugifyRuleName(name: string): string`
- Converts a rule name to a filename-safe slug.
- Parameters:
- `name`: The rule name to convert
- Returns: A slug-formatted string
### src/utils/frontmatter.ts (Added)
Provides utilities for parsing YAML-like frontmatter from .mdc files without external dependencies.
#### `ParsedContent` (interface)
- Interface for the result of frontmatter parsing
- Properties:
- `frontmatter`: Record<string, any> - Parsed metadata object
- `content`: string - Content body without frontmatter
#### `parseFrontmatter(input: string): ParsedContent`
- **Simple YAML Parser**: Parses YAML-like frontmatter without external dependencies, specifically designed for .mdc files.
- **Frontmatter Detection**: Detects content starting and ending with `---` delimiters.
- **Value Type Parsing**: Automatically converts values to appropriate types:
- Booleans: `true`/`false` → boolean values
- Numbers: Integers and floats → numeric values
- Arrays: `[item1, item2]` → array values (supports quoted and unquoted items)
- Strings: Quoted and unquoted strings → string values
- Empty/null values are skipped instead of set to null (prevents schema validation issues)
- **Content Separation**: Returns clean content body with frontmatter completely removed and leading newlines trimmed.
- **Error Handling**: Gracefully handles malformed frontmatter by treating content as plain text.
- **Cursor Compatibility**: Specifically designed to parse Cursor .mdc file frontmatter including `description`, `alwaysApply`, and `globs` fields.
### src/utils/similarity.ts
Provides text similarity utilities for finding related rules based on name similarity.
#### `levenshteinDistance(a: string, b: string): number`
- Calculates the Levenshtein distance between two strings
- Parameters:
- `a`: First string
- `b`: Second string
- Returns: A distance score (lower means more similar)
#### `calculateSimilarity(a: string, b: string): number`
- Calculates similarity score between two strings
- Parameters:
- `a`: First string
- `b`: Second string
- Returns: A similarity score between 0 and 1 (higher means more similar)
#### `findSimilarRules(notFoundName: string, availableRules: string[], limit: number = 5): string[]`
- Finds similar rule names to a given query
- Parameters:
- `notFoundName`: The rule name that wasn't found
- `availableRules`: List of available rule names
- `limit`: Maximum number of similar rules to return (default: 5)
- Returns: Array of similar rule names sorted by similarity (most similar first)
### src/utils/rule-formatter.ts (Added)
Provides utility functions for formatting rule content with metadata like `alwaysApply` and `globs`.
#### `formatRuleWithMetadata(config: RuleConfig, options?: RuleGeneratorOptions): string`
- Formats rule content with human-readable metadata lines at the beginning
- Parameters:
- `config`: The rule config to format
- `options`: Optional metadata like alwaysApply and globs
- Returns: The formatted rule content with metadata lines included
- Handles both alwaysApply (true/false) and globs (string or array)
- Used by non-cursor providers to include metadata in their rule content
#### `createTaggedRuleBlock(config: RuleConfig, options?: RuleGeneratorOptions): string`
- Creates a complete XML-like block for a rule, with start/end tags and formatted content
- Parameters:
- `config`: The rule config to format
- `options`: Optional metadata like alwaysApply and globs
- Returns: The complete tagged rule block with metadata
- Used by providers that store rules in a single file with XML-like tags (via `single-file-helpers.ts`)
### src/utils/single-file-helpers.ts (Added)
Provides utility functions specific to providers that manage rules within a single configuration file using tagged blocks.
Utilizes `debugLog` from `cli.ts` for conditional logging (Updated).
#### `appendOrUpdateTaggedBlock(targetPath: string, config: RuleConfig, options?: RuleGeneratorOptions, appendInsideVibeRulesBlock: boolean = false): Promise<boolean>` (Updated)
- Encapsulates the logic for managing rules in single-file providers (Windsurf, Claude Code, Codex, Zed).
- Reads the `targetPath` file content.
- Uses `createTaggedRuleBlock` to generate the XML-like block for the rule.
- **Bug Fix:** Now properly handles file creation for dot-files (e.g., `.rules`, `.windsurfrules`):
- Explicitly checks if a target file exists using `fs.stat`
- If the path exists but is a directory, removes it with `fs.rm` (recursive: true)
- For new files, uses `fsExtra.ensureFileSync` to create an empty file before writing content
- This prevents the dot-files from being incorrectly created as directories
- Searches for an existing block using a regex based on the `config.name` (e.g., `<rule-name>...</rule-name>`).
- **If found:** Replaces the existing block with the newly generated one.
- **If not found:** Appends the new block.
- If `appendInsideVibeRulesBlock` is `true` (for Claude, Codex), it attempts to insert the block just before the `<!-- /vibe-rules Integration -->` tag if present.
- Otherwise (or if the integration block isn't found), it appends the block to the end of the file.
- Writes the updated content back to the `targetPath`.
- Ensures the parent directory exists using `ensureDirectoryExists`.
- Handles file not found errors gracefully (creates the file).
- Returns `true` on success, `false` on failure.
### src/utils/rule-storage.ts (Added)
Provides utility functions for interacting with the internal rule definition storage located at `~/.vibe-rules/rules/<ruleType>/*`.
#### `saveInternalRule(ruleType: RuleType, config: RuleConfig): Promise<string>`
- Saves a rule definition (`config.content`) to the internal storage path determined by `ruleType` and `config.name`.
- Ensures the target directory exists.
- Returns the full path where the rule was saved.
#### `loadInternalRule(ruleType: RuleType, name: string): Promise<RuleConfig | null>`
- Loads a rule definition from the internal storage file corresponding to `ruleType` and `name`.
- Returns a `RuleConfig` object containing the name and content if found, otherwise returns `null`.
#### `listInternalRules(ruleType: RuleType): Promise<string[]>`
- Lists the names of all rules (.txt files) stored internally for the given `ruleType`.
- Returns an array of rule names (filenames without the .txt extension).
#### Common Rule Storage Functions (for user-saved rules with metadata)
#### `saveCommonRule(config: StoredRuleConfig): Promise<string>` (Added)
- **Enhanced Storage**: Saves rules with metadata to the common storage (`~/.vibe-rules/rules/<name>.json`).
- **JSON Format**: Stores rules as structured JSON files instead of plain text.
- **Metadata Support**: Preserves metadata like `alwaysApply`, `globs`, and `description` alongside content.
- **Validation**: Validates the configuration against `StoredRuleConfigSchema` before saving.
- **Directory Creation**: Automatically ensures the common rules directory exists.
- Returns the full path where the rule was saved.
#### `loadCommonRule(name: string): Promise<StoredRuleConfig | null>` (Added)
- **Dual Format Support**: Loads rules from either new JSON format or legacy .txt format.
- **Automatic Fallback**: First tries to load `<name>.json`, then falls back to `<name>.txt` for backward compatibility.
- **Metadata Preservation**: When loading JSON files, preserves all stored metadata.
- **Legacy Compatibility**: When loading .txt files, returns them as `StoredRuleConfig` with empty metadata.
- **Error Handling**: Gracefully handles parsing errors and file access issues.
- Returns a `StoredRuleConfig` object if found, otherwise returns `null`.
#### `listCommonRules(): Promise<string[]>` (Added)
- **Unified Listing**: Lists rules from both JSON and legacy .txt formats in a single array.
- **Deduplication**: Uses a Set to ensure rule names appear only once even if both .json and .txt versions exist.
- **Backward Compatibility**: Seamlessly handles mixed storage formats in the same directory.
- Returns an array of unique rule names available in the common storage.
### src/providers/index.ts
Contains a factory function `getRuleProvider(ruleType: RuleType)` that returns the appropriate provider instance based on the `RuleType` enum.
Handles `CURSOR`, `WINDSURF`, `CLAUDE_CODE`, `CODEX`, `AMP`, `CLINERULES`, `ROO`, `ZED`, `UNIFIED`, and `VSCODE` (Added).
### src/providers/cursor-provider.ts (Refactored)
Implementation of the `RuleProvider` interface for Cursor editor.
#### `CursorRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Handles Cursor's `.mdc` files with frontmatter.
- **`saveRule`, `loadRule`, `listRules` (Refactored):** Now use utility functions from `src/utils/rule-storage.ts` to interact with internal storage.
- **`generateRuleContent` (Updated):**
- Now accepts `RuleGeneratorOptions` (which includes optional `alwaysApply` and `globs`).
- If `alwaysApply` or `globs` are present in the options, they are included in the generated frontmatter.
- Uses `options.description` preferentially over `config.description` if provided.
- Uses a custom internal function to format the frontmatter instead of the `yaml` library.
- `appendRule` loads a rule from internal storage and calls `appendFormattedRule`. Note that rules loaded this way might not have the dynamic `alwaysApply`/`globs` metadata unless it was also saved (currently it isn't).
- **`appendFormattedRule` (Updated):**
- Now accepts `RuleGeneratorOptions`.
- Passes these options along to `generateRuleContent` to allow for dynamic frontmatter generation based on the source of the rule (e.g., from `install`
### src/providers/claude-code-provider.ts
Implementation of the `RuleProvider` interface for Claude Code IDE.
#### `ClaudeCodeRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within a single `CLAUDE.md` file with XML-like tagged blocks contained within a `<!-- vibe-rules Integration -->` wrapper.
- **`saveRule`, `loadRule`, `listRules`:** Use utility functions from `src/utils/rule-storage.ts` to interact with internal storage.
- **`generateRuleContent`:** Formats rule content with metadata using `formatRuleWithMetadata`.
- **`appendFormattedRule` (Updated):**
- Creates XML-like tagged blocks using `createTaggedRuleBlock` from `rule-formatter.ts`.
- **Integration Block Management:** When no existing `<!-- vibe-rules Integration -->` block is found:
- For new/empty files: Creates the integration block wrapper with the new rule inside.
- For files with existing content but no integration block: Wraps all existing content and the new rule within the integration block.
- **Rule Updating:** If a rule with the same name already exists, replaces its content.
- **Rule Insertion:** If an integration block exists, inserts new rules just before the closing `<!-- /vibe-rules Integration -->` tag.
- Ensures all rules are properly contained within the integration wrapper for consistency with expected file format.
### src/providers/zed-provider.ts (Added)
Implementation of the `RuleProvider` interface for Zed editor.
#### `ZedRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within a single `.rules` file in the project root.
- Uses `appendOrUpdateTaggedBlock` from `single-file-helpers.ts` for file operations.
- Uses `createTaggedRuleBlock` from `rule-formatter.ts` to generate rule content with metadata.
- Delegates rule definition storage (`saveRule`, `loadRule`, `listRules`) to `src/utils/rule-storage.ts` using `RuleType.ZED`.
### src/providers/unified-provider.ts (Added)
Implementation of the `RuleProvider` interface for the unified `.rules` file convention.
#### `UnifiedRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within a single `.rules` file located in the project root (by convention).
- Uses `appendOrUpdateTaggedBlock` from `src/utils/single-file-helpers.ts` to read, update, or append tagged rule blocks within the `.rules` file.
- Uses `createTaggedRuleBlock` from `src/utils/rule-formatter.ts` to generate the XML-like tagged content for each rule, including any metadata provided.
- Delegates the storage of rule definitions (when using `vibe-rules save unified <name> ...`) to the common internal storage via `src/utils/rule-storage.ts`, using `RuleType.UNIFIED`.
- The `targetPath` for `appendFormattedRule` is expected to be the path to the `.rules` file itself (e.g., `./.rules`).
### src/providers/vscode-provider.ts (Added)
Implementation of the `RuleProvider` interface for Visual Studio Code.
#### `VSCodeRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within individual `.instructions.md` files stored in the `.github/instructions/` directory.
- **`saveRule`, `loadRule`, `listRules`:** Use utility functions from `src/utils/rule-storage.ts` to interact with internal storage.
- **`generateRuleContent` (VSCode-specific format):**
- Uses YAML frontmatter with `applyTo` field for file targeting.
- **VSCode Bug Workaround:** Always uses `applyTo: "**"` due to VSCode's limitations with multiple globs in the `applyTo` field.
- Preserves the original rule content without adding redundant headings (since rule content already includes proper headings).
- Adds rule name and description as markdown headings in the content section.
- **`appendFormattedRule`:** Creates individual `.instructions.md` files in the `.github/instructions/` directory using the multi-file pattern (similar to Cursor and Clinerules providers).
- File naming follows the pattern: `{packageName}_{ruleName}.instructions.md`
### src/providers/codex-provider.ts (Updated)
Implementation of the `RuleProvider` interface for Codex IDE with proper `AGENTS.md` file support.
#### `CodexRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within a single `AGENTS.md` file with XML-like tagged blocks contained within a `<!-- vibe-rules Integration -->` wrapper.
- **File Structure (Updated):** Now correctly uses `AGENTS.md` files following Codex's actual file hierarchy:
- **Global:** `~/.codex/AGENTS.md` - personal global guidance
- **Local:** `AGENTS.md` at repo root - shared project notes
- **Subdirectory:** Use `--target` flag for `AGENTS.md` in specific directories
- **`saveRule`, `loadRule`, `listRules`:** Use utility functions from `src/utils/rule-storage.ts` to interact with internal storage.
- **`generateRuleContent`:** Formats rule content with metadata using `formatRuleWithMetadata`.
- **`appendFormattedRule` (Updated):**
- Creates XML-like tagged blocks using `createTaggedRuleBlock` from `rule-formatter.ts`.
- **Integration Block Management:** When no existing `<!-- vibe-rules Integration -->` block is found:
- For new/empty files: Creates the integration block wrapper with the new rule inside.
- For files with existing content but no integration block: Wraps all existing content and the new rule within the integration block.
- **Rule Updating:** If a rule with the same name already exists, replaces its content.
- **Rule Insertion:** If an integration block exists, inserts new rules just before the closing `<!-- /vibe-rules Integration -->` tag.
- Ensures all rules are properly contained within the integration wrapper for consistency with expected file format.
### src/providers/amp-provider.ts (Added)
Implementation of the `RuleProvider` interface for Amp AI coding assistant with simplified `AGENT.md` file support.
#### `AmpRuleProvider` (class)
##### Methods: `generateRuleContent`, `saveRule`, `loadRule`, `listRules`, `appendRule`, `appendFormattedRule`
- Manages rules within a single `AGENT.md` file with XML-like tagged blocks using a simplified approach similar to ZED.
- **File Structure (Local Only):** Only supports local project configurations:
- **Local:** `AGENT.md` at project root - project-specific agent guidance
- **Global:** Not supported by Amp
- **Subdirectory:** Not yet supported by Amp
- **`saveRule`, `loadRule`, `listRules`:** Use utility functions from `src/utils/rule-storage.ts` to interact with internal storage.
- **`generateRuleContent`:** Formats rule content with metadata using `formatRuleWithMetadata`.
- **`appendFormattedRule`:**
- Creates XML-like tagged blocks using `createTaggedRuleBlock` from `rule-formatter.ts`.
- **Simple Structure:** Uses direct tagged blocks without wrapper integration comments (similar to ZED provider).
- **Rule Updating:** If a rule with the same name already exists, replaces its content.
- **Rule Insertion:** Appends new rules directly to the file without special integration blocks.
- **Local Only:** Always ignores `isGlobal` parameter as Amp only supports local project files.
## New Documentation Files
### UNIFIED_RULES_CONVENTION.md (Added)
- Describes the purpose, location, format, and usage of the `.rules` file.
- Provides examples of the tagged rule block structure and how to interact with it using `vibe-rules` commands (`load unified`, `install unified`).
### examples/README.md (Updated)
- Comprehensive documentation for the examples directory.
- Explains what vibe-rules is and its purpose for managing AI prompts across different editors.
- Describes the difference between end-user examples (CommonJS/ESM projects consuming rules) and library examples (packages that export rules).
- Provides detailed instructions for testing the examples and understanding the package structure.
- Documents the workflow for both library authors (how to export rules) and end users (how to install rules).
- Explains the generated files and supported editors (Cursor, Windsurf, Claude Code, VSCode, etc.).
### examples/end-user-cjs-package/ (Updated)
CommonJS example project demonstrating how end-users consume vibe-rules from multiple library packages.
#### Key Changes
- **Testing Framework:** Migrated from vitest to Bun's built-in test runner for improved performance and reduced dependencies.
- **Dependencies:** Removed vitest (^3.2.2 → removed), added @types/bun for TypeScript support.
- **Scripts:** Updated test script from incomplete `"vibe-"` to `"bun test"`, added `"vibe-rules": "vibe-rules"` script.
- **Node.js Version:** Added `engines.node: ">=22"` requirement.
### examples/end-user-esm-package/ (Updated)
ES Module example project demonstrating how end-users consume vibe-rules from multiple library packages.
#### Key Changes
- **Shared Test File:** Uses a symbolic link (`install.test.ts -> ../end-user-cjs-package/install.test.ts`) to share the exact same comprehensive test suite with the CJS package.
- **Testing Framework:** Updated to use Bun's built-in test runner for consistency with CJS package.
- **Dependencies:** Added @types/bun for TypeScript support.
- **Scripts:** Updated test script to `"bun test"`, added `"vibe-rules": "vibe-rules"` script.
- **Node.js Version:** Added `engines.node: ">=22"` requirement matching CJS package.
#### Symlinked Test Benefits
- **Zero Duplication:** Maintains the exact same test logic across both CJS and ESM packages without file duplication.
- **Automatic Synchronization:** Any changes to the test file in the CJS package are automatically reflected in the ESM package.
- **Cross-Platform Compatibility:** The symlink approach works well on macOS and Linux development environments.
- **Comprehensive Coverage:** Both packages now validate the complete vibe-rules installation workflow for all supported editors (Cursor, Windsurf, Clinerules, Claude Code, Codex, ZED, VSCode).
#### install.test.ts (Added)
Integration test that validates the complete vibe-rules installation workflow for both Cursor and Windsurf editors:
##### Test Functionality
**Cursor Installation Test:**
- **Setup:** Cleans existing `.cursor` directory to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install cursor` to install rules from all package dependencies
- **Validation:**
- Verifies `.cursor/rules` directory exists
- Counts generated `.mdc` files (expects exactly 8 files)
- Validates rule naming convention (files prefixed with package names)
- Confirms presence of expected rule types from both `cjs-package` and `esm-package` dependencies
**Windsurf Installation Test:**
- **Setup:** Cleans existing `.windsurfrules` file to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install windsurf` to install rules from all package dependencies
- **Validation:**
- Verifies `.windsurfrules` file exists
- Counts generated rule blocks (expects exactly 8 tagged blocks)
- Validates XML-like tagged block structure with proper opening/closing tags
- Confirms presence of expected rule content and metadata from both dependency packages
##### Expected Rules Structure
The tests validate that 8 rules are generated from the two dependency packages:
- **From cjs-package:** `usage`, `api`, `architecture`, `routing` (4 rules)
- **From esm-package:** `usage`, `api`, `architecture`, `routing` (4 rules)
##### Enhanced PackageRuleObject Examples
The example packages now demonstrate the full range of `PackageRuleObject` configurations:
**CJS Package Examples:**
- **Usage Rule**: Always applied rule (`alwaysApply: true`) with no globs for universal guidelines
- **API Rule**: Specific globs for API development (`src/routes/**/*.tsx`, `src/api/**/*.ts`)
- **Architecture Rule**: Model decision rule with description for architecture/state management tasks
- **Routing Rule**: Basic rule with no specific configuration for manual triggering
**ESM Package Examples:**
- **Usage Rule**: Always applied rule (`alwaysApply: true`) with universal API usage guidelines
- **API Rule**: RESTful design principles with route and API-specific globs
- **Architecture Rule**: Component architecture patterns triggered by description matching
- **Routing Rule**: Navigation best practices with no specific triggering configuration
##### Robust Test Implementation
The test suite has been enhanced for maximum robustness:
- **Dynamic Rule Discovery**: Tests derive expected rule names from imported llms modules instead of hardcoded values
- **Flexible Property Validation**: Optional properties like `description` and `globs` are validated conditionally
- **Future-Proof Design**: Tests automatically adapt to changes in rule definitions without requiring manual updates
- **Cross-Reference Validation**: Ensures imported module structure matches generated files using actual data
**Cursor Format:**
- **File Format:** All rules stored as separate `.mdc` files in `.cursor/rules/` directory
- **Naming Pattern:** `{packageName}_{ruleName}.mdc` (e.g., `cjs-package_api.mdc`)
**Windsurf Format:**
- **File Format:** All rules stored as XML-like tagged blocks within a single `.windsurfrules` file
- **Block Pattern:** `<{packageName}_{ruleName}>...rule content...</{packageName}_{ruleName}>` (e.g., `<cjs-package_api>...content...</cjs-package_api>`)
**Clinerules Format:**
- **File Format:** All rules stored as separate `.md` files in `.clinerules/` directory
- **Naming Pattern:** `{packageName}_{ruleName}.md` (e.g., `cjs-package_api.md`)
- **Content:** Formatted using `formatRuleWithMetadata` with human-readable metadata lines
**Claude Code Installation Test:** (Added)
- **Setup:** Cleans existing `CLAUDE.md` file to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install claude-code` to install rules from all package dependencies
- **Validation:**
- Verifies `CLAUDE.md` file exists
- Counts generated rule blocks (expects exactly 8 tagged blocks)
- Validates XML-like tagged block structure with proper opening/closing tags
- Confirms presence of `<!-- vibe-rules Integration -->` wrapper block
- Validates rule content and metadata from both dependency packages
**Claude Code Format:**
- **File Format:** All rules stored as XML-like tagged blocks within a single `CLAUDE.md` file
- **Block Pattern:** `<{packageName}_{ruleName}>...rule content...</{packageName}_{ruleName}>` (e.g., `<cjs-package_api>...content...</cjs-package_api>`)
- **Wrapper Block:** All rules are contained within a `<!-- vibe-rules Integration -->...<!-- /vibe-rules Integration -->` block
- **Content:** Formatted using `formatRuleWithMetadata` with human-readable metadata lines
**CODEX Installation Test:** (Updated)
- **Setup:** Cleans existing `AGENTS.md` file to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install codex` to install rules from all package dependencies
- **Validation:**
- Verifies `AGENTS.md` file exists
- Counts generated rule blocks (expects exactly 8 tagged blocks)
- Validates XML-like tagged block structure with proper opening/closing tags
- Confirms presence of `<!-- vibe-rules Integration -->` comment wrapper block
- Validates rule content and metadata from both dependency packages
**CODEX Format:**
- **File Format:** All rules stored as XML-like tagged blocks within a single `AGENTS.md` file
- **Block Pattern:** `<{packageName}_{ruleName}>...rule content...</{packageName}_{ruleName}>` (e.g., `<cjs-package_api>...content...</cjs-package_api>`)
- **Wrapper Block:** All rules are contained within a `<!-- vibe-rules Integration -->...<!-- /vibe-rules Integration -->` comment block
- **Content:** Formatted using `formatRuleWithMetadata` with human-readable metadata lines
- **File Hierarchy:** Supports Codex's actual file lookup order:
1. `~/.codex/AGENTS.md` - personal global guidance (use `--global` flag)
2. `AGENTS.md` at repo root - shared project notes (default behavior)
3. `AGENTS.md` in current working directory - sub-folder/feature specifics (use `--target` flag)
**Amp Installation Test:** (Added)
- **Setup:** Cleans existing `AGENT.md` file to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install amp` to install rules from all package dependencies
- **Validation:**
- Verifies `AGENT.md` file exists
- Counts generated rule blocks (expects exactly 8 tagged blocks)
- Validates XML-like tagged block structure with proper opening/closing tags
- Confirms no wrapper blocks are used (similar to ZED, unlike Claude Code or Codex)
- Validates rule content and metadata from both dependency packages
**Amp Format:**
- **File Format:** All rules stored as XML-like tagged blocks within a single `AGENT.md` file
- **Block Pattern:** `<{packageName}_{ruleName}>...rule content...</{packageName}_{ruleName}>` (e.g., `<cjs-package_api>...content...</cjs-package_api>`)
- **No Wrapper Block:** Rules are stored directly without any integration wrapper (follows simple approach like ZED)
- **Content:** Formatted using `formatRuleWithMetadata` with human-readable metadata lines
- **Local Only:** Only supports local project `AGENT.md` files, no global or subdirectory support
**ZED Installation Test:** (Added)
- **Setup:** Cleans existing `.rules` file to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install zed` to install rules from all package dependencies
- **Validation:**
- Verifies `.rules` file exists
- Counts generated rule blocks (expects exactly 8 tagged blocks)
- Validates XML-like tagged block structure with proper opening/closing tags
- Confirms no wrapper blocks are used (unlike Claude Code or Codex)
- Validates rule content and metadata from both dependency packages
**ZED Format:**
- **File Format:** All rules stored as XML-like tagged blocks within a single `.rules` file
- **Block Pattern:** `<{packageName}_{ruleName}>...rule content...</{packageName}_{ruleName}>` (e.g., `<cjs-package_api>...content...</cjs-package_api>`)
- **No Wrapper Block:** Rules are stored directly without any integration wrapper (follows unified .rules convention)
- **Content:** Formatted using `formatRuleWithMetadata` with human-readable metadata lines
**VSCode Installation Test:** (Added)
- **Setup:** Cleans existing `.github/instructions` directory to ensure fresh test environment
- **Installation Process:**
- Runs `npm install` to ensure dependencies are available
- Executes `npm run vibe-rules install vscode` to install rules from all package dependencies
- **Validation:**
- Verifies `.github/instructions` directory exists
- Counts generated `.instructions.md` files (expects exactly 8 files)
- Validates file naming convention (files prefixed with package names)
- Confirms presence of expected rule content and metadata from both dependency packages
**VSCode Format:**
- **File Format:** All rules stored as separate `.instructions.md` files in `.github/instructions/` directory
- **Naming Pattern:** `{packageName}_{ruleName}.instructions.md` (e.g., `cjs-package_api.instructions.md`)
- **Frontmatter:** Uses `applyTo: "**"` for all rules due to VSCode's limitations with multiple globs
- **Content:** Preserves original rule content with rule name and description as markdown headings
- **VSCode Bug Workaround:** Always applies rules universally (`**`) for better reliability
##### Test Implementation Details
- Uses Bun's test framework (`import { test, expect } from "bun:test"`)
- Leverages Bun's shell integration (`import { $ } from "bun"`) for command execution
- **Test Environment Cleanup:** Clears existing editor directories/files at both the start and end of each test to ensure clean test isolation
- **Direct Module Imports:** Imports actual llms modules from both dependency packages:
- `const cjsRules = require("cjs-package/llms")` - CommonJS import
- `const esmRules = await import("esm-package/llms")` - ES Module import
- **Comprehensive Validation (covering Cursor, Windsurf, Clinerules, Claude Code, Codex, ZED, and VSCode formats):**
- **File System Validation:** Verifies directory/file creation, file count, and naming patterns for all supported editors
- **Module Structure Validation:** Validates imported arrays have correct length and structure
- **Rule Object Validation:** Ensures each rule has required properties (`name`, `rule`, `description`, `alwaysApply`, `globs`)
- **Type Validation:** Confirms correct data types for all rule properties
- **Content Validation:**
- For Cursor: Reads generated `.mdc` files and verifies they contain the actual rule content
- For Windsurf: Parses `.windsurfrules` file and validates tagged blocks contain rule content and metadata
- For Clinerules: Reads generated `.md` files in `.clinerules/` directory and validates content and metadata formatting
- For VSCode: Reads generated `.instructions.md` files in `.github/instructions/` directory and validates content and VSCode-specific frontmatter
- **Cross-Reference Validation:** Ensures rule names from imported modules match generated file names or tag names
- Includes comprehensive error handling and detailed logging
- Provides clear success confirmation message upon completion
## Recent Updates
### Code Quality and Linting Improvements
**Oxlint Integration (Latest)**
- **Linting Tool:** Integrated oxlint, a Rust-based fast linter, replacing traditional ESLint
- **Package.json:** Added `"lint": "bunx oxlint@latest"` script for easy linting
- **Zero Warnings:** Fixed all 31 linting warnings across the codebase including:
- Removed unused imports from all provider files
- Fixed unnecessary escape characters in template literals
- Removed unused catch parameters and variables
- Cleaned up unused function parameters
- **Performance:** Oxlint runs in 9ms on 28 files with 87 rules using 12 threads
- **CI Integration:** Linting can now be run via `bun run lint` for consistent code quality
**Import Cleanup:**
- **Provider Files:** Removed unused imports from `claude-code-provider.ts`, `codex-provider.ts`, `cursor-provider.ts`, `clinerules-provider.ts`, `windsurf-provider.ts`, and `unified-provider.ts`
- **Utility Files:** Fixed unused variables in `single-file-helpers.ts` and `install.ts`
- **Core Files:** Cleaned up unused imports in `llms/internal.ts` and escape character fixes in `cli.ts`
### VSCode Glob Limitation Fix
Due to Microsoft's VSCode having a bug where multiple globs in the `applyTo` field don't work properly, the VSCode provider was updated to always use `applyTo: "**"` for universal application. This change includes:
- **Provider Logic:** Updated `generateRuleContent` to always use `**` regardless of globs configuration
- **User Warnings:** Added installation-time warnings to inform users about this limitation and workaround
- **Documentation:** Updated README and ARCHITECTURE to reflect this VSCode limitation
- **Tests:** Modified test expectations to verify `**` is used for all rules regardless of original globs
## /src/cli.ts
```ts path="/src/cli.ts"
#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";
import { installCommandAction } from "./commands/install.js";
import { saveCommandAction } from "./commands/save.js";
import { loadCommandAction } from "./commands/load.js";
import { listCommandAction } from "./commands/list.js";
// Simple debug logger
export let isDebugEnabled = false;
export const debugLog = (message: string, ...optionalParams: any[]) => {
if (isDebugEnabled) {
console.log(chalk.dim(`[Debug] ${message}`), ...optionalParams);
}
};
const program = new Command();
program
.name("vibe-rules")
.description(
"A utility for managing Cursor rules, Windsurf rules, Amp rules, and other AI prompts"
)
.version("0.1.0")
.option("--debug", "Enable debug logging", false);
program.on("option:debug", () => {
isDebugEnabled = program.opts().debug;
debugLog("Debug logging enabled.");
});
program
.command("save")
.description("Save a rule to the local store")
.argument("<n>", "Name of the rule")
.option("-c, --content <content>", "Rule content")
.option("-f, --file <file>", "Load rule content from file")
.option("-d, --description <desc>", "Rule description")
.action(saveCommandAction);
program
.command("list")
.description("List all saved rules from the common store")
.action(listCommandAction);
program
.command("load")
.alias("add")
.description("Apply a saved rule to an editor configuration")
.argument("<n>", "Name of the rule to apply")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(loadCommandAction);
program
.command("install")
.description(
"Install rules from an NPM package or all dependencies directly into an editor configuration"
)
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.argument("[packageName]", "Optional NPM package name to install rules from")
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(installCommandAction);
program.parse(process.argv);
if (process.argv.length <= 2) {
program.help();
}
```
## /src/providers/amp-provider.ts
```ts path="/src/providers/amp-provider.ts"
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class AmpRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.AMP;
/**
* Generates formatted content for Amp including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Appends a rule loaded from internal storage to the target Amp file.
* @param name - The name of the rule in internal storage.
* @param targetPath - Optional explicit target file path.
* @param isGlobal - Not supported for Amp (always local project files).
* @param options - Additional generation options.
* @returns True on success, false on failure.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal storage.`);
return false;
}
// Amp only supports local project files, ignore isGlobal
const actualTargetPath = targetPath ?? getRulePath(this.ruleType, "", false);
return this.appendFormattedRule(ruleConfig, actualTargetPath, false, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
* @param config - The rule configuration to apply.
* @param targetPath - The target file path (e.g., ./AGENT.md).
* @param isGlobal - Not supported for Amp (always false).
* @param options - Additional options like description, alwaysApply, globs.
* @returns True on success, false on failure.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean | undefined,
options?: RuleGeneratorOptions | undefined
): Promise<boolean> {
// Amp uses a simpler approach without vibe-rules integration block
return appendOrUpdateTaggedBlock(
targetPath,
config,
options,
false // No special integration block needed for Amp
);
}
}
```
## /src/providers/claude-code-provider.ts
```ts path="/src/providers/claude-code-provider.ts"
import * as fs from "fs-extra/esm";
import { readFile, writeFile } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata, createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
import chalk from "chalk";
export class ClaudeCodeRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CLAUDE_CODE;
/**
* Generates formatted content for Claude Code including metadata.
* This content is intended to be placed within the <!-- vibe-rules Integration --> block.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// Format the content with metadata
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.CLAUDE_CODE, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.CLAUDE_CODE, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.CLAUDE_CODE);
}
/**
* Applies a rule by updating the <vibe-rules> section in the target CLAUDE.md.
* If targetPath is omitted, it determines local vs global based on isGlobal option.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${this.ruleType}.`);
return false;
}
const destinationPath = targetPath || getRulePath(this.ruleType, name, isGlobal); // name might not be needed by getRulePath here
return this.appendFormattedRule(rule, destinationPath, isGlobal, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const destinationPath = targetPath;
// Ensure the parent directory exists
fs.ensureDirSync(path.dirname(destinationPath));
const newBlock = createTaggedRuleBlock(config, options);
let fileContent = "";
if (await fs.pathExists(destinationPath)) {
fileContent = await readFile(destinationPath, "utf-8");
}
// Escape rule name for regex
const ruleNameRegex = config.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`^<${ruleNameRegex}>[\\s\\S]*?</${ruleNameRegex}>`, "m");
let updatedContent: string;
const match = fileContent.match(regex);
if (match) {
// Rule exists, replace its content
console.log(
chalk.blue(`Updating existing rule block for "${config.name}" in ${destinationPath}...`)
);
updatedContent = fileContent.replace(regex, newBlock);
} else {
// Rule doesn't exist, append it
// Attempt to append within <!-- vibe-rules Integration --> if possible
const integrationStartTag = "<!-- vibe-rules Integration -->";
const integrationEndTag = "<!-- /vibe-rules Integration -->";
const startIndex = fileContent.indexOf(integrationStartTag);
const endIndex = fileContent.indexOf(integrationEndTag);
console.log(
chalk.blue(`Appending new rule block for "${config.name}" to ${destinationPath}...`)
);
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
// Insert before the end tag of the integration block
const insertionPoint = endIndex;
const before = fileContent.slice(0, insertionPoint);
const after = fileContent.slice(insertionPoint);
updatedContent = `${before.trimEnd()}\n\n${newBlock}\n\n${after.trimStart()}`;
} else {
// Create the vibe-rules Integration block if it doesn't exist
if (fileContent.trim().length > 0) {
// File has content but no integration block, wrap everything
updatedContent = `<!-- vibe-rules Integration -->\n\n${fileContent.trim()}\n\n${newBlock}\n\n<!-- /vibe-rules Integration -->`;
} else {
// New/empty file, create integration block with the new rule
updatedContent = `<!-- vibe-rules Integration -->\n\n${newBlock}\n\n<!-- /vibe-rules Integration -->`;
}
}
}
try {
await writeFile(destinationPath, updatedContent.trim() + "\n");
return true;
} catch (error) {
console.error(chalk.red(`Error writing updated rules to ${destinationPath}: ${error}`));
return false;
}
}
}
```
## /src/providers/clinerules-provider.ts
```ts path="/src/providers/clinerules-provider.ts"
import * as fs from "fs-extra/esm";
import { writeFile } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import {
getRulePath, // Returns the .clinerules directory path
ensureDirectoryExists,
} from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import chalk from "chalk";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
// Helper function specifically for clinerules/roo setup
// Focuses on the directory structure: .clinerules/vibe-rules.md
async function setupClinerulesDirectory(
clinerulesDirPath: string,
rulesContent: string
): Promise<void> {
await fs.ensureDir(clinerulesDirPath); // Ensure the .clinerules directory exists
const vibeRulesRulePath = path.join(clinerulesDirPath, "vibe-rules.md");
const rulesTemplate = rulesContent.replace(/\r\n/g, "\n").trim();
// Wrap content with <!-- vibe-rules --> tags if not already present
const startTag = "<!-- vibe-rules Integration -->";
const endTag = "<!-- /vibe-rules Integration -->";
let contentToWrite = rulesTemplate;
if (!contentToWrite.includes(startTag)) {
contentToWrite = `${startTag}\n${rulesTemplate}\n${endTag}`;
}
await writeFile(vibeRulesRulePath, contentToWrite + "\n");
}
export class ClinerulesRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CLINERULES;
/**
* Generates formatted content for Clinerules/Roo including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Applies a rule by setting up the .clinerules/vibe-rules.md structure.
* Always targets the project-local .clinerules directory.
*/
async appendRule(
name: string,
targetPath?: string, // If provided, should be the .clinerules directory path
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${RuleType.CLINERULES}/${RuleType.ROO}.`);
return false;
}
// getRulePath for CLINERULES/ROO returns the directory path
const destinationDir = targetPath || getRulePath(RuleType.CLINERULES, name); // name is ignored here
try {
const contentToAppend = this.generateRuleContent(rule, options);
await setupClinerulesDirectory(destinationDir, contentToAppend);
console.log(
chalk.green(
`Successfully set up ${RuleType.CLINERULES}/${RuleType.ROO} rules in: ${destinationDir}`
)
);
return true;
} catch (error) {
console.error(
chalk.red(
`Error setting up ${RuleType.CLINERULES}/${RuleType.ROO} rules in ${destinationDir}:`
),
error
);
return false;
}
}
/**
* Formats and applies a rule directly from a RuleConfig object.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string, // Should now receive the correct .../.clinerules/slugified-name.md path
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
// Ensure the parent .clinerules directory exists
const parentDir = path.dirname(targetPath);
ensureDirectoryExists(parentDir);
// Generate the content
const content = this.generateRuleContent(config, options);
// Log metadata inclusion (optional, but kept from previous state)
if (options?.alwaysApply !== undefined || options?.globs) {
console.log(
chalk.blue(
` Including metadata in rule content: alwaysApply=${options.alwaysApply}, globs=${JSON.stringify(options.globs)}`
)
);
}
try {
// Write directly to the target file path
await writeFile(targetPath, content, "utf-8");
console.log(chalk.green(`Successfully applied rule "${config.name}" to ${targetPath}`));
return true;
} catch (error) {
console.error(chalk.red(`Error applying rule "${config.name}" to ${targetPath}: ${error}`));
return false;
}
}
}
```
## /src/providers/codex-provider.ts
```ts path="/src/providers/codex-provider.ts"
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath } from "../utils/path.js";
import { formatRuleWithMetadata } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class CodexRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.CODEX;
/**
* Generates formatted content for Codex including metadata.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return formatRuleWithMetadata(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(this.ruleType, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(this.ruleType, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(this.ruleType);
}
/**
* Appends a rule loaded from internal storage to the target Codex file.
* @param name - The name of the rule in internal storage.
* @param targetPath - Optional explicit target file path.
* @param isGlobal - Hint for global context (uses ~/.codex/instructions.md).
* @param options - Additional generation options.
* @returns True on success, false on failure.
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal storage.`);
return false;
}
const actualTargetPath = targetPath ?? getRulePath(this.ruleType, "", isGlobal);
return this.appendFormattedRule(ruleConfig, actualTargetPath, isGlobal, options);
}
/**
* Formats and applies a rule directly from a RuleConfig object using XML-like tags.
* If a rule with the same name (tag) already exists, its content is updated.
* @param config - The rule configuration to apply.
* @param targetPath - The target file path (e.g., ~/.codex/AGENTS.md or ./AGENTS.md).
* @param isGlobal - Unused by this method but kept for interface compatibility.
* @param options - Additional options like description, alwaysApply, globs.
* @returns True on success, false on failure.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean | undefined,
options?: RuleGeneratorOptions | undefined
): Promise<boolean> {
// Delegate to the shared helper, ensuring insertion within <vibe-rules>
return appendOrUpdateTaggedBlock(
targetPath,
config,
options,
true // Append inside <vibe-rules Integration>
);
}
}
```
## /src/providers/cursor-provider.ts
```ts path="/src/providers/cursor-provider.ts"
import * as path from "path";
import { writeFile } from "fs/promises";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath, ensureDirectoryExists } from "../utils/path.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
// Custom function to format frontmatter simply
const formatFrontmatter = (fm: Record<string, any>): string => {
let result = "";
if (fm.description) {
result += `description: ${fm.description}\n`;
}
if (fm.globs) {
if (fm.debug) {
console.log(`[Debug] Formatting globs: ${JSON.stringify(fm.globs)}`);
}
const globsString = Array.isArray(fm.globs) ? fm.globs.join(",") : fm.globs;
if (globsString) {
result += `globs: ${globsString}\n`;
}
}
if (fm.alwaysApply === false) {
result += `alwaysApply: false\n`;
} else if (fm.alwaysApply === true) {
result += `alwaysApply: true\n`;
}
return result.trim();
};
export class CursorRuleProvider implements RuleProvider {
/**
* Generate cursor rule content with frontmatter
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
const frontmatter: Record<string, any> = {};
if (options?.description ?? config.description) {
frontmatter.description = options?.description ?? config.description;
}
if (options?.globs) {
frontmatter.globs = options.globs;
}
if (options?.alwaysApply !== undefined) {
frontmatter.alwaysApply = options.alwaysApply;
}
if (options?.debug) {
frontmatter.debug = options.debug;
}
const frontmatterString =
Object.keys(frontmatter).length > 0 ? `---\n${formatFrontmatter(frontmatter)}\n---\n` : "";
return `${frontmatterString}${config.content}`;
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
// Use the utility function to save to internal storage
return saveInternalRule(RuleType.CURSOR, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
// Use the utility function to load from internal storage
return loadInternalRule(RuleType.CURSOR, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
// Use the utility function to list rules from internal storage
return listInternalRules(RuleType.CURSOR);
}
/**
* Append a cursor rule to a target file
*/
async appendRule(name: string, targetPath?: string, isGlobal?: boolean): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal Cursor storage.`);
return false;
}
const finalTargetPath = targetPath || getRulePath(RuleType.CURSOR, name, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal);
}
/**
* Formats and applies a rule directly from a RuleConfig object.
* Creates the .cursor/rules directory if it doesn't exist.
* Uses slugified rule name for the filename.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const fullPath = targetPath;
const dir = path.dirname(fullPath);
try {
await ensureDirectoryExists(dir); // Ensure the PARENT directory exists
const formattedContent = this.generateRuleContent(config, options); // Pass options
await writeFile(fullPath, formattedContent, "utf-8");
return true;
} catch (error) {
console.error(`Error applying Cursor rule "${config.name}" to ${fullPath}:`, error);
return false;
}
}
}
```
## /src/providers/index.ts
```ts path="/src/providers/index.ts"
import { RuleProvider, RuleType } from "../types.js";
import { CursorRuleProvider } from "./cursor-provider.js";
import { WindsurfRuleProvider } from "./windsurf-provider.js";
import { ClaudeCodeRuleProvider } from "./claude-code-provider.js";
import { CodexRuleProvider } from "./codex-provider.js";
import { AmpRuleProvider } from "./amp-provider.js";
import { ClinerulesRuleProvider } from "./clinerules-provider.js";
import { ZedRuleProvider } from "./zed-provider.js";
import { UnifiedRuleProvider } from "./unified-provider.js";
import { VSCodeRuleProvider } from "./vscode-provider.js";
/**
* Factory function to get the appropriate rule provider based on rule type
*/
export function getRuleProvider(ruleType: RuleType): RuleProvider {
switch (ruleType) {
case RuleType.CURSOR:
return new CursorRuleProvider();
case RuleType.WINDSURF:
return new WindsurfRuleProvider();
case RuleType.CLAUDE_CODE:
return new ClaudeCodeRuleProvider();
case RuleType.CODEX:
return new CodexRuleProvider();
case RuleType.AMP:
return new AmpRuleProvider();
case RuleType.CLINERULES:
case RuleType.ROO:
return new ClinerulesRuleProvider();
case RuleType.ZED:
return new ZedRuleProvider();
case RuleType.UNIFIED:
return new UnifiedRuleProvider();
case RuleType.VSCODE:
return new VSCodeRuleProvider();
case RuleType.CUSTOM:
default:
throw new Error(`Unsupported rule type: ${ruleType}`);
}
}
```
## /src/providers/metadata-application.test.ts
```ts path="/src/providers/metadata-application.test.ts"
import { test, expect, describe } from "bun:test";
// Test data that would be used by providers
const testRule = {
name: "test-rule",
content: "# Test Rule\n\nThis is test content for metadata application.",
description: "Test rule for validating metadata application",
};
const testMetadata = {
alwaysApply: false,
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx"],
description: "API and routing patterns",
};
const alwaysApplyMetadata = {
alwaysApply: true,
description: "Universal guidelines",
};
describe("Metadata Application Patterns", () => {
describe("Cursor Provider Format", () => {
test("should generate YAML frontmatter pattern", () => {
// Simulate what Cursor provider generateRuleContent should produce
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
if (metadata?.globs) {
if (Array.isArray(metadata.globs)) {
result += "globs:\n";
metadata.globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
} else {
result += `globs: ${metadata.globs}\n`;
}
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, testMetadata);
expect(result).toContain("---");
expect(result).toContain("description: API and routing patterns");
expect(result).toContain("alwaysApply: false");
expect(result).toContain("globs:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("---");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should handle alwaysApply: true", () => {
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, alwaysApplyMetadata);
expect(result).toContain("alwaysApply: true");
expect(result).not.toContain("globs:");
});
test("should handle single glob string", () => {
const singleGlobMetadata = {
...testMetadata,
globs: "src/**/*.ts",
};
const generateCursorFormat = (rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) {
result += `description: ${metadata.description}\n`;
}
if (metadata?.alwaysApply !== undefined) {
result += `alwaysApply: ${metadata.alwaysApply}\n`;
}
if (metadata?.globs) {
if (Array.isArray(metadata.globs)) {
result += "globs:\n";
metadata.globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
} else {
result += `globs: ${metadata.globs}\n`;
}
}
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateCursorFormat(testRule, singleGlobMetadata);
expect(result).toContain("globs: src/**/*.ts");
});
});
describe("Windsurf Provider Format", () => {
test("should generate human-readable metadata", () => {
// Simulate what Windsurf provider should produce
const generateWindsurfFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateWindsurfFormat(testRule, testMetadata);
expect(result).toContain("Always Apply: false");
expect(result).toContain("Applies to files matching:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should handle alwaysApply: true with no globs", () => {
const generateWindsurfFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateWindsurfFormat(testRule, alwaysApplyMetadata);
expect(result).toContain("Always Apply: true");
expect(result).not.toContain("Applies to files matching:");
});
});
describe("VSCode Provider Format", () => {
test("should generate .instructions.md with YAML frontmatter", () => {
// Simulate what VSCode provider should produce
const generateVSCodeFormat = (rule: any, _metadata: any) => {
let result = "---\n";
// VSCode limitation: always use universal glob
result += `applyTo: "**"\n`;
result += "---\n\n";
result += `# ${rule.name}\n\n`;
if (rule.description) {
result += `**Description:** ${rule.description}\n\n`;
}
result += rule.content;
return result;
};
const result = generateVSCodeFormat(testRule, testMetadata);
expect(result).toContain("---");
expect(result).toContain('applyTo: "**"'); // VSCode limitation workaround
expect(result).toContain("---");
expect(result).toContain("# test-rule");
expect(result).toContain("**Description:** Test rule for validating metadata application");
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
test("should always use universal glob due to VSCode bug", () => {
const generateVSCodeFormat = (rule: any, _metadata: any) => {
let result = "---\n";
// VSCode limitation: always use universal glob
result += `applyTo: "**"\n`;
result += "---\n\n";
result += rule.content;
return result;
};
const result = generateVSCodeFormat(testRule, testMetadata);
// Should always use "**" regardless of original globs
expect(result).toContain('applyTo: "**"');
expect(result).not.toContain("src/api");
expect(result).not.toContain("src/routes");
});
});
describe("Single-File Provider Patterns", () => {
test("should generate content with metadata lines for Zed/Claude/Codex", () => {
// Simulate what single-file providers should produce
const generateSingleFileFormat = (rule: any, metadata: any) => {
let result = "";
if (metadata?.alwaysApply !== undefined) {
result += `Always Apply: ${metadata.alwaysApply}\n\n`;
}
if (metadata?.globs) {
result += "Applies to files matching:\n";
const globs = Array.isArray(metadata.globs) ? metadata.globs : [metadata.globs];
globs.forEach((glob: string) => {
result += `- ${glob}\n`;
});
result += "\n";
}
result += rule.content;
return result;
};
const result = generateSingleFileFormat(testRule, testMetadata);
expect(result).toContain("Always Apply: false");
expect(result).toContain("Applies to files matching:");
expect(result).toContain("- src/api/**/*.ts");
expect(result).toContain("- src/routes/**/*.tsx");
expect(result).toContain("# Test Rule");
});
});
describe("Cross-Provider Consistency", () => {
test("all provider formats should preserve rule content", () => {
const formats = [
// Cursor format
(rule: any, metadata: any) => {
let result = "---\n";
if (metadata?.description) result += `description: ${metadata.description}\n`;
result += "---\n\n" + rule.content;
return result;
},
// Windsurf format
(rule: any, _metadata: any) => {
return rule.content;
},
// VSCode format
(rule: any, _metadata: any) => {
return `---\napplyTo: "**"\n---\n\n${rule.content}`;
},
// Single-file format
(rule: any, _metadata: any) => {
return rule.content;
},
];
formats.forEach((format) => {
const result = format(testRule, testMetadata);
expect(result).toContain("# Test Rule");
expect(result).toContain("This is test content for metadata application.");
});
});
test("all provider formats should handle rules without metadata", () => {
const plainRule = {
name: "plain-rule",
content: "# Plain Rule\n\nNo metadata here.",
description: "A simple rule",
};
const formats = [
// Cursor format (minimal frontmatter)
(rule: any) => {
return `---\n---\n\n${rule.content}`;
},
// Others just return content
(rule: any) => rule.content,
(rule: any) => rule.content,
(rule: any) => rule.content,
];
formats.forEach((format) => {
const result = format(plainRule);
expect(result).toContain("# Plain Rule");
expect(result).toContain("No metadata here.");
// Should not crash or add invalid metadata
});
});
});
});
```
## /src/providers/unified-provider.ts
```ts path="/src/providers/unified-provider.ts"
import path from "path";
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import {
saveInternalRule as saveRuleToInternalStorage,
loadInternalRule as loadRuleFromInternalStorage,
listInternalRules as listRulesFromInternalStorage,
} from "../utils/rule-storage.js";
const UNIFIED_RULE_FILENAME = ".rules";
export class UnifiedRuleProvider implements RuleProvider {
private getRuleFilePath(
ruleName: string,
isGlobal: boolean = false,
projectRoot: string = process.cwd()
): string {
// For unified provider, ruleName is not used in the path, always '.rules'
// isGlobal might determine if it's in projectRoot or user's global .rules, but for now, always project root.
if (isGlobal) {
// Potentially handle a global ~/.rules file in the future
// For now, global unified rules are not distinct from project unified rules
// console.warn('Global unified rules are not yet uniquely supported, using project .rules');
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
async saveRule(config: RuleConfig, _options?: RuleGeneratorOptions): Promise<string> {
return saveRuleToInternalStorage(RuleType.UNIFIED, config);
}
async loadRule(name: string): Promise<RuleConfig | null> {
return loadRuleFromInternalStorage(RuleType.UNIFIED, name);
}
async listRules(): Promise<string[]> {
return listRulesFromInternalStorage(RuleType.UNIFIED);
}
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// For .rules, we use the tagged block format directly
return createTaggedRuleBlock(config, options);
}
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in local unified store.`);
return false;
}
const filePath = targetPath || this.getRuleFilePath(name, isGlobal);
return this.appendFormattedRule(ruleConfig, filePath, isGlobal, options);
}
async appendFormattedRule(
config: RuleConfig,
targetPath: string, // This will be the path to the .rules file
_isGlobal?: boolean, // isGlobal might be less relevant here if .rules is always project-based
options?: RuleGeneratorOptions
): Promise<boolean> {
// The 'targetPath' for unified provider should directly be the path to the '.rules' file.
// The 'config.name' is used for the tag name within the '.rules' file.
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/providers/vscode-provider.ts
```ts path="/src/providers/vscode-provider.ts"
import * as path from "path";
import { writeFile } from "fs/promises";
import { RuleConfig, RuleProvider, RuleGeneratorOptions, RuleType } from "../types.js";
import { getRulePath, ensureDirectoryExists } from "../utils/path.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
//vs code bugged for 2+ globs...
export class VSCodeRuleProvider implements RuleProvider {
/**
* Generate VSCode instruction content with frontmatter
*
* NOTE: VSCode has a bug where multiple globs in applyTo don't work properly,
* so we always use "**" to apply rules universally for better reliability.
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// VSCode Bug Workaround: Always use "**" because VSCode's applyTo field
// doesn't properly handle multiple globs or complex glob patterns.
// This ensures rules work consistently across all files.
const applyToValue = "**";
// Always include frontmatter with applyTo (required for VSCode)
const frontmatterString = `---\napplyTo: "${applyToValue}"\n---\n`;
// Start with the original rule content
let content = config.content;
// Add name and description in content if available
let contentPrefix = "";
// Extract original rule name from prefixed config.name (remove package prefix)
let displayName = config.name;
if (displayName.includes("_")) {
displayName = displayName.split("_").slice(1).join("_");
}
// Add name as heading if not already present in content
if (displayName && !content.includes(`# ${displayName}`)) {
contentPrefix += `# ${displayName}\n`;
}
// Add description if provided
if (options?.description ?? config.description) {
contentPrefix += `## ${options?.description ?? config.description}\n\n`;
} else if (contentPrefix) {
contentPrefix += "\n";
}
return `${frontmatterString}${contentPrefix}${content}`;
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.VSCODE, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.VSCODE, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.VSCODE);
}
/**
* Append a VSCode instruction rule to a target file
*/
async appendRule(name: string, targetPath?: string, isGlobal?: boolean): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal VSCode storage.`);
return false;
}
const finalTargetPath = targetPath || getRulePath(RuleType.VSCODE, name, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal);
}
/**
* Formats and applies a rule directly from a RuleConfig object.
* Creates the .github/instructions directory if it doesn't exist.
* Uses slugified rule name for the filename with .instructions.md extension.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal: boolean = false,
options?: RuleGeneratorOptions
): Promise<boolean> {
const fullPath = targetPath;
const dir = path.dirname(fullPath);
try {
await ensureDirectoryExists(dir);
const formattedContent = this.generateRuleContent(config, options);
await writeFile(fullPath, formattedContent, "utf-8");
return true;
} catch (error) {
console.error(`Error applying VSCode rule "${config.name}" to ${fullPath}:`, error);
return false;
}
}
}
```
## /src/providers/windsurf-provider.ts
```ts path="/src/providers/windsurf-provider.ts"
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { getDefaultTargetPath } from "../utils/path.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
export class WindsurfRuleProvider implements RuleProvider {
private readonly ruleType = RuleType.WINDSURF;
/**
* Format rule content with XML tags
*/
private formatRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return createTaggedRuleBlock(config, options);
}
/**
* Generates formatted rule content with Windsurf XML tags
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
return this.formatRuleContent(config, options);
}
/**
* Saves a rule definition to internal storage for later use.
* @param config - The rule configuration.
* @returns Path where the rule definition was saved internally.
*/
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.WINDSURF, config);
}
/**
* Loads a rule definition from internal storage.
* @param name - The name of the rule to load.
* @returns The RuleConfig if found, otherwise null.
*/
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.WINDSURF, name);
}
/**
* Lists rule definitions available in internal storage.
* @returns An array of rule names.
*/
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.WINDSURF);
}
/**
* Append a windsurf rule to a target file
*/
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const rule = await this.loadRule(name);
if (!rule) {
console.error(`Rule '${name}' not found for type ${this.ruleType}.`);
return false;
}
// Windsurf typically doesn't use global paths or specific rule name paths for the target
// It uses a single default file.
const destinationPath = targetPath || getDefaultTargetPath(this.ruleType);
return this.appendFormattedRule(rule, destinationPath, false, options);
}
/**
* Format and append a rule directly from a RuleConfig object.
* If a rule with the same name (tag) already exists, its content is updated.
*/
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
// Delegate to the shared helper
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/providers/zed-provider.ts
```ts path="/src/providers/zed-provider.ts"
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { saveInternalRule, loadInternalRule, listInternalRules } from "../utils/rule-storage.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { getDefaultTargetPath } from "../utils/path.js";
export class ZedRuleProvider implements RuleProvider {
async saveRule(config: RuleConfig): Promise<string> {
return saveInternalRule(RuleType.ZED, config);
}
async loadRule(name: string): Promise<RuleConfig | null> {
return loadInternalRule(RuleType.ZED, name);
}
async listRules(): Promise<string[]> {
return listInternalRules(RuleType.ZED);
}
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// Zed .rules files are expected to be plain text or use a format
// compatible with simple tagged blocks if we are managing multiple rules within it.
// For consistency with Windsurf and other single-file providers, we use tagged blocks.
return createTaggedRuleBlock(config, options);
}
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean, // isGlobal is not typically used for Zed's .rules
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in internal ZED storage.`);
return false;
}
const finalTargetPath = targetPath || getDefaultTargetPath(RuleType.ZED, isGlobal);
return this.appendFormattedRule(ruleConfig, finalTargetPath, isGlobal, options);
}
async appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean, // isGlobal is not typically used for Zed's .rules
options?: RuleGeneratorOptions
): Promise<boolean> {
// Zed .rules files are at the root of the worktree, so isGlobal is likely false.
// We don't append inside a specific <vibe-rules Integration> block by default for .rules
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/schemas.ts
```ts path="/src/schemas.ts"
import { z } from "zod";
export const RuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
// Add other fields from RuleConfig if they exist and need validation
});
// Schema for metadata in stored rules
export const RuleGeneratorOptionsSchema = z
.object({
description: z.string().optional(),
isGlobal: z.boolean().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(),
debug: z.boolean().optional(),
})
.optional();
// Schema for the enhanced local storage format with metadata
export const StoredRuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
metadata: RuleGeneratorOptionsSchema,
});
// Original schema for reference (might still be used elsewhere, e.g., save command)
export const VibeRulesSchema = z.array(RuleConfigSchema);
// --- Schemas for Package Exports (`<packageName>/llms`) ---
// Schema for the flexible rule object format within packages
export const PackageRuleObjectSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
rule: z.string().min(1, "Rule content cannot be empty"), // Renamed from content
description: z.string().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(), // Allow string or array
});
// Schema for a single item in the package export (either a string or the object)
export const PackageRuleItemSchema = z.union([
z.string().min(1, "Rule string cannot be empty"),
PackageRuleObjectSchema, // Defined above now
]);
// Schema for the default export of package/llms (array of strings or objects)
export const VibePackageRulesSchema = z.array(PackageRuleItemSchema);
// --- Type Helpers ---
// Basic RuleConfig type
export type RuleConfig = z.infer<typeof RuleConfigSchema>;
// Type for the enhanced local storage format
export type StoredRuleConfig = z.infer<typeof StoredRuleConfigSchema>;
// Type for rule generator options
export type RuleGeneratorOptions = z.infer<typeof RuleGeneratorOptionsSchema>;
// Type for the flexible package rule object
export type PackageRuleObject = z.infer<typeof PackageRuleObjectSchema>;
// Type for a single item in the package export array
export type PackageRuleItem = z.infer<typeof PackageRuleItemSchema>;
```
## /src/types.ts
```ts path="/src/types.ts"
/**
* Rule type definitions for the vibe-rules utility
*/
export type * from "./schemas.js";
export interface RuleConfig {
name: string;
content: string;
description?: string;
// Additional properties can be added as needed
}
// Extended interface for storing rules locally with metadata
export interface StoredRuleConfig {
name: string;
content: string;
description?: string;
metadata?: RuleGeneratorOptions;
}
export const RuleType = {
CURSOR: "cursor",
WINDSURF: "windsurf",
CLAUDE_CODE: "claude-code",
CODEX: "codex",
AMP: "amp",
CLINERULES: "clinerules",
ROO: "roo",
ZED: "zed",
UNIFIED: "unified",
VSCODE: "vscode",
CUSTOM: "custom",
} as const;
export type RuleTypeArray = (typeof RuleType)[keyof typeof RuleType][];
export type RuleType = (typeof RuleType)[keyof typeof RuleType];
export interface RuleProvider {
/**
* Creates a new rule file with the given content
*/
saveRule(config: RuleConfig, options?: RuleGeneratorOptions): Promise<string>;
/**
* Loads a rule from storage
*/
loadRule(name: string): Promise<RuleConfig | null>;
/**
* Lists all available rules
*/
listRules(): Promise<string[]>;
/**
* Appends a rule to an existing file
*/
appendRule(name: string, targetPath?: string): Promise<boolean>;
/**
* Formats and appends a rule directly from a RuleConfig object
*/
appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean>;
/**
* Generates formatted rule content with editor-specific formatting
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string;
}
export interface RuleGeneratorOptions {
description?: string;
isGlobal?: boolean;
alwaysApply?: boolean;
globs?: string | string[];
debug?: boolean;
// Additional options for specific rule types
}
```
## /src/utils/frontmatter.test.ts
```ts path="/src/utils/frontmatter.test.ts"
import { test, expect, describe } from "bun:test";
import { parseFrontmatter } from "./frontmatter.js";
describe("Frontmatter Parser", () => {
test("should parse Cursor .mdc frontmatter correctly", () => {
const mdcContent = `---
description: Test API rule
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx"]
alwaysApply: false
---
# API Development Guidelines
- Use proper error handling
- Follow RESTful conventions`;
const { frontmatter, content } = parseFrontmatter(mdcContent);
expect(frontmatter.description).toBe("Test API rule");
expect(frontmatter.globs).toEqual(["src/api/**/*.ts", "src/routes/**/*.tsx"]);
expect(frontmatter.alwaysApply).toBe(false);
expect(content.trim()).toBe(`# API Development Guidelines
- Use proper error handling
- Follow RESTful conventions`);
});
test("should parse alwaysApply: true correctly", () => {
const mdcContent = `---
description: Always applied rule
alwaysApply: true
---
This rule is always active.`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.alwaysApply).toBe(true);
});
test("should parse single glob string", () => {
const mdcContent = `---
globs: "src/**/*.ts"
---
Content here.`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.globs).toBe("src/**/*.ts");
});
test("should handle content without frontmatter", () => {
const plainContent = "Just plain content without frontmatter";
const { frontmatter, content } = parseFrontmatter(plainContent);
expect(Object.keys(frontmatter)).toHaveLength(0);
expect(content).toBe(plainContent);
});
test("should skip empty/null values", () => {
const mdcContent = `---
description: Valid description
globs:
alwaysApply: false
emptyField:
---
Content`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.description).toBe("Valid description");
expect(frontmatter.alwaysApply).toBe(false);
expect(frontmatter.globs).toBeUndefined();
expect(frontmatter.emptyField).toBeUndefined();
});
test("should handle complex globs array", () => {
const mdcContent = `---
globs: ["src/api/**/*.ts", "src/routes/**/*.tsx", "src/components/**/*.vue"]
---
Multi-file rule content`;
const { frontmatter } = parseFrontmatter(mdcContent);
expect(frontmatter.globs).toEqual([
"src/api/**/*.ts",
"src/routes/**/*.tsx",
"src/components/**/*.vue",
]);
});
test("should handle malformed frontmatter gracefully", () => {
const malformedContent = `---
invalid: yaml: content[
not-closed: "quote
---
Content should still be parsed`;
const { frontmatter, content } = parseFrontmatter(malformedContent);
// Should not crash and should extract content
expect(content).toContain("Content should still be parsed");
expect(typeof frontmatter).toBe("object");
});
});
```
## /src/utils/frontmatter.ts
```ts path="/src/utils/frontmatter.ts"
/**
* Simple frontmatter parser for .mdc files
* Parses YAML-like frontmatter without external dependencies
*/
export interface ParsedContent {
frontmatter: Record<string, any>;
content: string;
}
/**
* Parse frontmatter from content that may contain YAML frontmatter
* Returns the parsed frontmatter object and the remaining content
*/
export function parseFrontmatter(input: string): ParsedContent {
const lines = input.split("\n");
// Check if content starts with frontmatter delimiter
if (lines[0]?.trim() !== "---") {
return {
frontmatter: {},
content: input,
};
}
// Find the closing delimiter
let endIndex = -1;
for (let i = 1; i < lines.length; i++) {
if (lines[i]?.trim() === "---") {
endIndex = i;
break;
}
}
if (endIndex === -1) {
// No closing delimiter found, treat as regular content
return {
frontmatter: {},
content: input,
};
}
// Extract frontmatter lines
const frontmatterLines = lines.slice(1, endIndex);
const contentLines = lines.slice(endIndex + 1);
// Parse frontmatter (simple YAML-like parsing)
const frontmatter: Record<string, any> = {};
for (const line of frontmatterLines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue; // Skip empty lines and comments
}
const colonIndex = trimmed.indexOf(":");
if (colonIndex === -1) {
continue; // Skip lines without colons
}
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
// Parse different value types
if (value === "true") {
frontmatter[key] = true;
} else if (value === "false") {
frontmatter[key] = false;
} else if (value === "null" || value === "") {
// Skip null or empty values instead of setting them
continue;
} else if (/^\d+$/.test(value)) {
frontmatter[key] = parseInt(value, 10);
} else if (/^\d+\.\d+$/.test(value)) {
frontmatter[key] = parseFloat(value);
} else if (value.startsWith("[") && value.endsWith("]")) {
// Simple array parsing for globs
try {
const arrayContent = value.slice(1, -1);
if (arrayContent.trim() === "") {
frontmatter[key] = [];
} else {
const items = arrayContent.split(",").map((item) => {
const trimmed = item.trim();
// Remove quotes if present
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return trimmed.slice(1, -1);
}
return trimmed;
});
frontmatter[key] = items;
}
} catch {
frontmatter[key] = value; // Fallback to string if parsing fails
}
} else {
// Remove quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
frontmatter[key] = value;
}
}
return {
frontmatter,
content: contentLines.join("\n").replace(/^\n+/, ""), // Remove leading newlines
};
}
```
## /src/utils/path.ts
```ts path="/src/utils/path.ts"
import path from "path";
import os from "os";
import fs, { pathExists } from "fs-extra/esm";
import { RuleType } from "../types.js";
import { debugLog } from "../cli.js";
// Base directory for storing internal rule definitions
export const RULES_BASE_DIR = path.join(os.homedir(), ".vibe-rules");
// Home directories for specific IDEs/Tools
export const CLAUDE_HOME_DIR = path.join(os.homedir(), ".claude");
export const CODEX_HOME_DIR = path.join(os.homedir(), ".codex");
export const ZED_RULES_FILE = ".rules"; // Added for Zed
/**
* Get the common rules directory path
*/
export function getCommonRulesDir(): string {
const rulesDir = path.join(RULES_BASE_DIR, "rules");
fs.ensureDirSync(rulesDir);
return rulesDir;
}
/**
* Get path to store internal rule definitions based on rule type
* (Not the actual target paths for IDEs)
*/
export function getInternalRuleStoragePath(ruleType: RuleType, ruleName: string): string {
const typeDir = path.join(RULES_BASE_DIR, ruleType);
fs.ensureDirSync(typeDir);
// Internal storage uses a simple .txt for content
return path.join(typeDir, `${ruleName}.txt`);
}
/**
* Get the expected file path for a rule based on its type and context (local/global).
* This now returns the actual path where the rule should exist for the IDE/tool.
* The 'isGlobal' flag determines if we should use the home directory path.
*/
export function getRulePath(
ruleType: RuleType,
ruleName: string, // ruleName might not be relevant for some types like Claude/Codex global
isGlobal: boolean = false,
projectRoot: string = process.cwd()
): string {
switch (ruleType) {
case RuleType.CURSOR:
// Cursor rules are typically project-local in .cursor/rules/
const cursorDir = path.join(projectRoot, ".cursor", "rules");
// Use slugified name for the file
return path.join(cursorDir, `${slugifyRuleName(ruleName)}.mdc`);
case RuleType.WINDSURF:
// Windsurf rules are typically project-local .windsurfrules file
return path.join(projectRoot, ".windsurfrules"); // Single file, name not used for path
case RuleType.CLAUDE_CODE:
// Claude rules are CLAUDE.md, either global or local
return isGlobal
? path.join(CLAUDE_HOME_DIR, "CLAUDE.md")
: path.join(projectRoot, "CLAUDE.md");
case RuleType.CODEX:
// Codex uses AGENTS.md (global) or AGENTS.md (local)
return isGlobal
? path.join(CODEX_HOME_DIR, "AGENTS.md")
: path.join(projectRoot, "AGENTS.md");
case RuleType.AMP:
// Amp uses AGENT.md (local only, no global support)
return path.join(projectRoot, "AGENT.md");
case RuleType.CLINERULES:
case RuleType.ROO:
// Cline/Roo rules are project-local files in .clinerules/
return path.join(
projectRoot,
".clinerules",
slugifyRuleName(ruleName) + ".md" // Use .md extension
);
case RuleType.ZED: // Added for Zed
return path.join(projectRoot, ZED_RULES_FILE);
case RuleType.UNIFIED:
return path.join(projectRoot, ".rules"); // Unified also uses .rules in project root
case RuleType.VSCODE:
// VSCode instructions are project-local files in .github/instructions/
return path.join(
projectRoot,
".github",
"instructions",
slugifyRuleName(ruleName) + ".instructions.md"
);
case RuleType.CUSTOM:
default:
// Fallback for custom or unknown - store internally for now
// Or maybe this should throw an error?
return getInternalRuleStoragePath(ruleType, ruleName);
}
}
/**
* Get the default target path (directory or file) where a rule type is typically applied.
* This is used by commands like 'apply' if no specific target is given.
* Note: This might overlap with getRulePath for some types.
* Returns potential paths based on convention.
*/
export function getDefaultTargetPath(
ruleType: RuleType,
isGlobalHint: boolean = false // Hint for providers like Claude/Codex
): string {
switch (ruleType) {
case RuleType.CURSOR:
// Default target is the rules directory within .cursor
return path.join(process.cwd(), ".cursor", "rules");
case RuleType.WINDSURF:
// Default target is the .windsurfrules file
return path.join(process.cwd(), ".windsurfrules");
case RuleType.CLAUDE_CODE:
// Default target depends on global hint
return isGlobalHint
? CLAUDE_HOME_DIR // Directory
: process.cwd(); // Project root (for local CLAUDE.md)
case RuleType.CODEX:
// Default target depends on global hint
return isGlobalHint
? CODEX_HOME_DIR // Directory
: process.cwd(); // Project root (for local AGENTS.md)
case RuleType.AMP:
// Amp only supports local project root (for local AGENT.md)
return process.cwd();
case RuleType.CLINERULES:
case RuleType.ROO:
// Default target is the .clinerules directory
return path.join(process.cwd(), ".clinerules");
case RuleType.ZED: // Added for Zed
return path.join(process.cwd(), ZED_RULES_FILE);
case RuleType.UNIFIED:
return path.join(process.cwd(), ".rules");
case RuleType.VSCODE:
// Default target is the .github/instructions directory
return path.join(process.cwd(), ".github", "instructions");
default:
console.warn(
`Default target path not defined for rule type: ${ruleType}, defaulting to CWD.`
);
return process.cwd();
}
}
/**
* Ensures that a specific directory exists, creating it if necessary.
*
* @param dirPath The absolute or relative path to the directory to ensure.
*/
export function ensureDirectoryExists(dirPath: string): void {
try {
fs.ensureDirSync(dirPath);
debugLog(`Ensured directory exists: ${dirPath}`);
} catch (err: any) {
console.error(`Failed to ensure directory ${dirPath}:`, err);
// Depending on the desired behavior, you might want to re-throw or exit
// throw err;
}
}
/**
* Checks if the configuration for a given editor type exists.
* This is used to prevent the 'install' command from creating config files/dirs.
* @param ruleType The editor type to check.
* @param isGlobal Whether to check the global or local path.
* @param projectRoot The root directory of the project.
* @returns A promise that resolves to true if the configuration exists, false otherwise.
*/
export async function editorConfigExists(
ruleType: RuleType,
isGlobal: boolean,
projectRoot: string = process.cwd()
): Promise<boolean> {
let checkPath: string;
switch (ruleType) {
case RuleType.CURSOR:
checkPath = path.join(projectRoot, ".cursor");
break;
case RuleType.WINDSURF:
checkPath = path.join(projectRoot, ".windsurfrules");
break;
case RuleType.CLINERULES:
case RuleType.ROO:
checkPath = path.join(projectRoot, ".clinerules");
break;
case RuleType.ZED:
case RuleType.UNIFIED:
checkPath = path.join(projectRoot, ".rules");
break;
case RuleType.VSCODE:
checkPath = path.join(projectRoot, ".github", "instructions");
break;
case RuleType.CLAUDE_CODE:
checkPath = isGlobal
? path.join(CLAUDE_HOME_DIR, "CLAUDE.md")
: path.join(projectRoot, "CLAUDE.md");
break;
case RuleType.CODEX:
checkPath = isGlobal
? path.join(CODEX_HOME_DIR, "AGENTS.md")
: path.join(projectRoot, "AGENTS.md");
break;
case RuleType.AMP:
checkPath = path.join(projectRoot, "AGENT.md");
break;
default:
return false; // Unknown or unsupported for this check
}
return pathExists(checkPath);
}
/**
* Convert a rule name to a filename-safe slug.
*/
export function slugifyRuleName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9_]+/g, "-")
.replace(/^-|-$/g, "");
}
```
## /src/utils/rule-formatter.ts
```ts path="/src/utils/rule-formatter.ts"
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
/**
* Formats rule content for non-cursor providers that use XML-like tags.
* Includes additional metadata like alwaysApply and globs in a human-readable format
* within the rule content.
*/
export function formatRuleWithMetadata(config: RuleConfig, options?: RuleGeneratorOptions): string {
let formattedContent = config.content;
// Add metadata lines at the beginning of content if they exist
const metadataLines = [];
// Add alwaysApply information if provided
if (options?.alwaysApply !== undefined) {
metadataLines.push(
`Always Apply: ${options.alwaysApply ? "true" : "false"} - ${
options.alwaysApply
? "This rule should ALWAYS be applied by the AI"
: "This rule should only be applied when relevant files are open"
}`
);
}
// Add globs information if provided
if (options?.globs && options.globs.length > 0) {
const globsStr = Array.isArray(options.globs) ? options.globs.join(", ") : options.globs;
// Skip adding the glob info if it's just a generic catch-all pattern
const isCatchAllPattern =
globsStr === "**/*" ||
(Array.isArray(options.globs) && options.globs.length === 1 && options.globs[0] === "**/*");
if (!isCatchAllPattern) {
metadataLines.push(`Always apply this rule in these files: ${globsStr}`);
}
}
// If we have metadata, add it to the beginning of the content
if (metadataLines.length > 0) {
formattedContent = `${metadataLines.join("\n")}\n\n${config.content}`;
}
return formattedContent;
}
/**
* Creates a complete XML-like block for a rule, including start/end tags
* and formatted content with metadata
*/
export function createTaggedRuleBlock(config: RuleConfig, options?: RuleGeneratorOptions): string {
const formattedContent = formatRuleWithMetadata(config, options);
const startTag = `<${config.name}>`;
const endTag = `</${config.name}>`;
return `${startTag}\n${formattedContent}\n${endTag}`;
}
```
## /src/utils/rule-storage.test.ts
```ts path="/src/utils/rule-storage.test.ts"
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import { readFile, writeFile, rm, mkdir, readdir } from "fs/promises";
import * as fs from "fs-extra/esm";
import * as path from "path";
// Test data directory
const TEST_DATA_DIR = ".test-rule-storage";
const TEST_RULES_DIR = path.join(TEST_DATA_DIR, "rules");
// Import the functions dynamically to avoid CLI trigger
const { StoredRuleConfigSchema } = await import("../schemas.js");
describe("Rule Storage", () => {
beforeEach(async () => {
// Clean up and create test directories
if (await fs.pathExists(TEST_DATA_DIR)) {
await rm(TEST_DATA_DIR, { recursive: true });
}
await mkdir(TEST_DATA_DIR, { recursive: true });
await mkdir(TEST_RULES_DIR, { recursive: true });
});
afterEach(async () => {
// Clean up test files
if (await fs.pathExists(TEST_DATA_DIR)) {
await rm(TEST_DATA_DIR, { recursive: true });
}
});
describe("StoredRuleConfig schema validation", () => {
test("should validate rule with metadata", () => {
const testRule = {
name: "test-rule",
content: "Test content",
description: "Test description",
metadata: {
alwaysApply: true,
globs: ["src/**/*.ts"],
},
};
const result = StoredRuleConfigSchema.safeParse(testRule);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("test-rule");
expect(result.data.content).toBe("Test content");
expect(result.data.metadata?.alwaysApply).toBe(true);
expect(result.data.metadata?.globs).toEqual(["src/**/*.ts"]);
}
});
test("should validate rule without metadata", () => {
const testRule = {
name: "simple-rule",
content: "Simple content",
description: "Simple description",
};
const result = StoredRuleConfigSchema.safeParse(testRule);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe("simple-rule");
expect(result.data.content).toBe("Simple content");
expect(result.data.metadata).toBeUndefined();
}
});
test("should reject invalid rule config", () => {
const invalidRule = {
name: "", // Empty name should fail
content: "Some content",
};
const result = StoredRuleConfigSchema.safeParse(invalidRule);
expect(result.success).toBe(false);
});
});
describe("JSON storage format tests", () => {
test("should save and load JSON rule with metadata", async () => {
const testRule = {
name: "test-api-rule",
content: "# API Guidelines\nUse proper error handling",
description: "Test rule for APIs",
metadata: {
description: "Test rule for APIs",
alwaysApply: false,
globs: ["src/api/**/*.ts"],
},
};
// Save to JSON file
const jsonPath = path.join(TEST_RULES_DIR, "test-api-rule.json");
await writeFile(jsonPath, JSON.stringify(testRule, null, 2));
// Verify file was created
expect(await fs.pathExists(jsonPath)).toBe(true);
// Load and verify content structure
const savedContent = await readFile(jsonPath, "utf-8");
const savedRule = JSON.parse(savedContent);
expect(savedRule.name).toBe("test-api-rule");
expect(savedRule.content).toBe("# API Guidelines\nUse proper error handling");
expect(savedRule.description).toBe("Test rule for APIs");
expect(savedRule.metadata.alwaysApply).toBe(false);
expect(savedRule.metadata.globs).toEqual(["src/api/**/*.ts"]);
});
test("should save and load JSON rule without metadata", async () => {
const testRule = {
name: "plain-rule",
content: "This is a plain rule without metadata",
description: "A simple rule",
};
// Save to JSON file
const jsonPath = path.join(TEST_RULES_DIR, "plain-rule.json");
await writeFile(jsonPath, JSON.stringify(testRule, null, 2));
// Load and verify
const savedContent = await readFile(jsonPath, "utf-8");
const savedRule = JSON.parse(savedContent);
expect(savedRule.name).toBe("plain-rule");
expect(savedRule.content).toBe("This is a plain rule without metadata");
expect(savedRule.description).toBe("A simple rule");
expect(savedRule.metadata).toBeUndefined();
});
test("should handle mixed JSON and .txt files", async () => {
// Create JSON rule
const jsonRule = {
name: "json-rule",
content: "JSON content",
};
const jsonPath = path.join(TEST_RULES_DIR, "json-rule.json");
await writeFile(jsonPath, JSON.stringify(jsonRule, null, 2));
// Create .txt rule
const txtPath = path.join(TEST_RULES_DIR, "txt-rule.txt");
await writeFile(txtPath, "TXT content");
// List files manually (simulating listCommonRules behavior)
const files = await readdir(TEST_RULES_DIR);
const ruleNames = new Set<string>();
for (const file of files) {
if (file.endsWith(".json")) {
ruleNames.add(file.replace(".json", ""));
} else if (file.endsWith(".txt")) {
ruleNames.add(file.replace(".txt", ""));
}
}
const rules = Array.from(ruleNames);
expect(rules).toContain("json-rule");
expect(rules).toContain("txt-rule");
expect(rules.length).toBe(2);
});
test("should deduplicate when both .json and .txt exist", async () => {
// Create both formats for same rule name
const jsonRule = {
name: "dual-rule",
content: "JSON content",
};
const jsonPath = path.join(TEST_RULES_DIR, "dual-rule.json");
await writeFile(jsonPath, JSON.stringify(jsonRule, null, 2));
const txtPath = path.join(TEST_RULES_DIR, "dual-rule.txt");
await writeFile(txtPath, "TXT content");
// List and deduplicate (simulating listCommonRules behavior)
const files = await readdir(TEST_RULES_DIR);
const ruleNames = new Set<string>();
for (const file of files) {
if (file.endsWith(".json")) {
ruleNames.add(file.replace(".json", ""));
} else if (file.endsWith(".txt")) {
ruleNames.add(file.replace(".txt", ""));
}
}
const rules = Array.from(ruleNames);
expect(rules).toContain("dual-rule");
expect(rules.length).toBe(1); // Should only appear once
});
});
});
```
## /src/utils/rule-storage.ts
```ts path="/src/utils/rule-storage.ts"
import * as fs from "fs-extra/esm";
import { writeFile, readFile, readdir } from "fs/promises";
import * as path from "path";
import { RuleConfig, RuleType, StoredRuleConfig } from "../types.js";
import { StoredRuleConfigSchema } from "../schemas.js";
import { getInternalRuleStoragePath, getCommonRulesDir } from "./path.js";
const RULE_EXTENSION = ".txt";
const STORED_RULE_EXTENSION = ".json";
/**
* Saves a rule definition to the internal storage (~/.vibe-rules/<ruleType>/<name>.txt).
* @param ruleType - The type of the rule, determining the subdirectory.
* @param config - The rule configuration object containing name and content.
* @returns The full path where the rule was saved.
* @throws Error if file writing fails.
*/
export async function saveInternalRule(ruleType: RuleType, config: RuleConfig): Promise<string> {
const storagePath = getInternalRuleStoragePath(ruleType, config.name);
const dir = path.dirname(storagePath);
await fs.ensureDir(dir); // Ensure the directory exists
await writeFile(storagePath, config.content, "utf-8");
console.debug(`[Rule Storage] Saved internal rule: ${storagePath}`);
return storagePath;
}
/**
* Loads a rule definition from the internal storage.
* @param ruleType - The type of the rule.
* @param name - The name of the rule to load.
* @returns The RuleConfig object if found, otherwise null.
*/
export async function loadInternalRule(
ruleType: RuleType,
name: string
): Promise<RuleConfig | null> {
const storagePath = getInternalRuleStoragePath(ruleType, name);
try {
if (!(await fs.pathExists(storagePath))) {
console.debug(`[Rule Storage] Internal rule not found: ${storagePath}`);
return null;
}
const content = await readFile(storagePath, "utf-8");
console.debug(`[Rule Storage] Loaded internal rule: ${storagePath}`);
return { name, content };
} catch (error: any) {
// Log other errors but still return null as the rule couldn't be loaded
console.error(
`[Rule Storage] Error loading internal rule ${name} (${ruleType}): ${error.message}`
);
return null;
}
}
/**
* Lists the names of all rules stored internally for a given rule type.
* @param ruleType - The type of the rule.
* @returns An array of rule names.
*/
export async function listInternalRules(ruleType: RuleType): Promise<string[]> {
// Use getInternalRuleStoragePath with a dummy name to get the directory path
const dummyPath = getInternalRuleStoragePath(ruleType, "__dummy__");
const storageDir = path.dirname(dummyPath);
try {
if (!(await fs.pathExists(storageDir))) {
console.debug(
`[Rule Storage] Internal rule directory not found for ${ruleType}: ${storageDir}`
);
return []; // Directory doesn't exist, no rules
}
const files = await readdir(storageDir);
const ruleNames = files
.filter((file) => file.endsWith(RULE_EXTENSION))
.map((file) => path.basename(file, RULE_EXTENSION));
console.debug(`[Rule Storage] Listed ${ruleNames.length} internal rules for ${ruleType}`);
return ruleNames;
} catch (error: any) {
console.error(`[Rule Storage] Error listing internal rules for ${ruleType}: ${error.message}`);
return []; // Return empty list on error
}
}
// --- Common Rule Storage Functions (for user-saved rules with metadata) ---
/**
* Saves a rule with metadata to the common storage (~/.vibe-rules/rules/<name>.json).
* @param config - The stored rule configuration object containing name, content, and metadata.
* @returns The full path where the rule was saved.
* @throws Error if validation or file writing fails.
*/
export async function saveCommonRule(config: StoredRuleConfig): Promise<string> {
// Validate the configuration
StoredRuleConfigSchema.parse(config);
const commonRulesDir = getCommonRulesDir();
const storagePath = path.join(commonRulesDir, `${config.name}${STORED_RULE_EXTENSION}`);
await fs.ensureDir(commonRulesDir);
await writeFile(storagePath, JSON.stringify(config, null, 2), "utf-8");
console.debug(`[Rule Storage] Saved common rule with metadata: ${storagePath}`);
return storagePath;
}
/**
* Loads a rule with metadata from the common storage.
* Falls back to legacy .txt format for backwards compatibility.
* @param name - The name of the rule to load.
* @returns The StoredRuleConfig object if found, otherwise null.
*/
export async function loadCommonRule(name: string): Promise<StoredRuleConfig | null> {
const commonRulesDir = getCommonRulesDir();
// Try new JSON format first
const jsonPath = path.join(commonRulesDir, `${name}${STORED_RULE_EXTENSION}`);
if (await fs.pathExists(jsonPath)) {
try {
const content = await readFile(jsonPath, "utf-8");
const parsed = JSON.parse(content);
const validated = StoredRuleConfigSchema.parse(parsed);
console.debug(`[Rule Storage] Loaded common rule from JSON: ${jsonPath}`);
return validated;
} catch (error: any) {
console.error(`[Rule Storage] Error parsing JSON rule ${name}: ${error.message}`);
return null;
}
}
// Fall back to legacy .txt format
const txtPath = path.join(commonRulesDir, `${name}${RULE_EXTENSION}`);
if (await fs.pathExists(txtPath)) {
try {
const content = await readFile(txtPath, "utf-8");
console.debug(`[Rule Storage] Loaded common rule from legacy .txt: ${txtPath}`);
return {
name,
content,
metadata: {}, // No metadata in legacy format
};
} catch (error: any) {
console.error(`[Rule Storage] Error loading legacy rule ${name}: ${error.message}`);
return null;
}
}
console.debug(`[Rule Storage] Common rule not found: ${name}`);
return null;
}
/**
* Lists the names of all rules stored in the common storage (both JSON and legacy .txt).
* @returns An array of rule names.
*/
export async function listCommonRules(): Promise<string[]> {
const commonRulesDir = getCommonRulesDir();
try {
if (!(await fs.pathExists(commonRulesDir))) {
console.debug(`[Rule Storage] Common rules directory not found: ${commonRulesDir}`);
return [];
}
const files = await readdir(commonRulesDir);
const ruleNames = new Set<string>();
// Add names from both .json and .txt files
files.forEach((file) => {
if (file.endsWith(STORED_RULE_EXTENSION)) {
ruleNames.add(path.basename(file, STORED_RULE_EXTENSION));
} else if (file.endsWith(RULE_EXTENSION)) {
ruleNames.add(path.basename(file, RULE_EXTENSION));
}
});
const result = Array.from(ruleNames);
console.debug(`[Rule Storage] Listed ${result.length} common rules`);
return result;
} catch (error: any) {
console.error(`[Rule Storage] Error listing common rules: ${error.message}`);
return [];
}
}
```
## /src/utils/similarity.ts
```ts path="/src/utils/similarity.ts"
/**
* Text similarity utilities for finding similar rule names
*/
/**
* Calculate Levenshtein distance between two strings
* @param a First string
* @param b Second string
* @returns Distance score (lower means more similar)
*/
export function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
// Initialize matrix
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
return matrix[b.length][a.length];
}
/**
* Calculate similarity score between two strings (0-1, higher means more similar)
* @param a First string
* @param b Second string
* @returns Similarity score between 0 and 1
*/
export function calculateSimilarity(a: string, b: string): number {
if (a === b) return 1; // Exact match
if (!a || !b) return 0; // Handle empty strings
const distance = levenshteinDistance(a.toLowerCase(), b.toLowerCase());
const maxLength = Math.max(a.length, b.length);
// Convert distance to similarity score (1 - normalized distance)
return 1 - distance / maxLength;
}
/**
* Find similar rule names to the given name
* @param notFoundName The rule name that wasn't found
* @param availableRules List of available rule names
* @param limit Maximum number of similar rules to return
* @returns Array of similar rule names sorted by similarity (most similar first)
*/
export function findSimilarRules(
notFoundName: string,
availableRules: string[],
limit: number = 5
): string[] {
if (!availableRules.length) return [];
// Calculate similarity for each rule
const scoredRules = availableRules.map((ruleName) => ({
name: ruleName,
score: calculateSimilarity(notFoundName, ruleName),
}));
// Sort by similarity score (highest first) and take the top n
return scoredRules
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((rule) => rule.name);
}
```
## /src/utils/single-file-helpers.ts
```ts path="/src/utils/single-file-helpers.ts"
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra/esm";
import * as path from "path";
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
import { createTaggedRuleBlock } from "./rule-formatter.js";
import { ensureDirectoryExists } from "./path.js";
import { debugLog } from "../cli.js";
/**
* Appends or updates a tagged block within a single target file.
*
* Reads the file content, looks for an existing block matching the rule name tag,
* replaces it if found, or appends the new block otherwise.
*
* @param targetPath The path to the target file.
* @param config The rule configuration.
* @param options Optional generator options.
* @param appendInsideVibeRulesBlock If true, tries to append within <vibe-rules Integration> block.
* @returns Promise resolving to true if successful, false otherwise.
*/
export async function appendOrUpdateTaggedBlock(
targetPath: string,
config: RuleConfig,
options?: RuleGeneratorOptions,
appendInsideVibeRulesBlock: boolean = false
): Promise<boolean> {
try {
// Ensure the PARENT directory exists, but only if it's not the current directory itself.
const parentDir = path.dirname(targetPath);
// Avoid calling ensureDirectoryExists('.') as it's unnecessary and might mask other issues.
if (parentDir !== ".") {
ensureDirectoryExists(parentDir);
}
let currentContent = "";
let fileExists = true;
try {
// Check if the file exists (not directory)
const stats = await fs.stat(targetPath).catch(() => null);
if (stats) {
if (stats.isDirectory()) {
// If it's a directory but should be a file, remove it
console.warn(
`Found a directory at ${targetPath} but expected a file. Removing directory...`
);
await fs.rm(targetPath, { recursive: true, force: true });
fileExists = false;
} else {
// It's a file, read its content
currentContent = await fs.readFile(targetPath, "utf-8");
}
} else {
fileExists = false;
}
} catch (error: any) {
if (error.code !== "ENOENT") {
console.error(`Error accessing target file ${targetPath}:`, error);
return false;
}
// File doesn't exist, which is fine, we'll create it.
fileExists = false;
debugLog(`Target file ${targetPath} not found, will create.`);
}
// If file doesn't exist, explicitly create an empty file first
// This ensures we're creating a file, not a directory
if (!fileExists) {
try {
// Ensure the file exists by explicitly creating it as an empty file
// Use fsExtra.ensureFileSync which is designed to create the file (not directory)
fsExtra.ensureFileSync(targetPath);
debugLog(`Created empty file: ${targetPath}`);
} catch (error) {
console.error(`Failed to create empty file ${targetPath}:`, error);
return false;
}
}
const newBlock = createTaggedRuleBlock(config, options);
const ruleNameRegexStr = config.name.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); // Escape regex special chars
const existingBlockRegex = new RegExp(
`<${ruleNameRegexStr}>[\\s\\S]*?</${ruleNameRegexStr}>`,
"m"
);
let updatedContent: string;
const match = currentContent.match(existingBlockRegex);
if (match) {
// Update existing block
debugLog(`Updating existing block for rule "${config.name}" in ${targetPath}`);
updatedContent = currentContent.replace(existingBlockRegex, newBlock);
} else {
// Append new block
debugLog(`Appending new block for rule "${config.name}" to ${targetPath}`);
// Check for comment-style integration blocks
const commentIntegrationEndTag = "<!-- /vibe-rules Integration -->";
const commentEndIndex = currentContent.lastIndexOf(commentIntegrationEndTag);
let integrationEndIndex = -1;
let integrationStartTag = "";
if (commentEndIndex !== -1) {
integrationEndIndex = commentEndIndex;
integrationStartTag = "<!-- vibe-rules Integration -->";
}
if (appendInsideVibeRulesBlock && integrationEndIndex !== -1) {
// Append inside the vibe-rules block if requested and found
const insertionPoint = integrationEndIndex;
updatedContent =
currentContent.slice(0, insertionPoint).trimEnd() +
"\n\n" + // Ensure separation
newBlock +
"\n\n" + // Ensure separation
currentContent.slice(insertionPoint);
debugLog(`Appending rule inside ${integrationStartTag} block.`);
} else if (appendInsideVibeRulesBlock && integrationEndIndex === -1) {
// Create the integration block if it doesn't exist and we want to append inside it
const separator = currentContent.trim().length > 0 ? "\n\n" : "";
const startTag = "<!-- vibe-rules Integration -->";
const endTag = "<!-- /vibe-rules Integration -->";
updatedContent =
currentContent.trimEnd() + separator + startTag + "\n\n" + newBlock + "\n\n" + endTag;
debugLog(`Created new ${startTag} block with rule.`);
} else {
// Append to the end
const separator = currentContent.trim().length > 0 ? "\n\n" : ""; // Add separator if file not empty
updatedContent = currentContent.trimEnd() + separator + newBlock;
if (appendInsideVibeRulesBlock) {
debugLog(`Could not find vibe-rules Integration block, appending rule to the end.`);
}
}
}
// Ensure file ends with a newline
if (!updatedContent.endsWith("\n")) {
updatedContent += "\n";
}
await fs.writeFile(targetPath, updatedContent, "utf-8");
console.log(`Successfully applied rule "${config.name}" to ${targetPath}`);
return true;
} catch (error) {
console.error(`Error applying rule "${config.name}" to ${targetPath}:`, error);
return false;
}
}
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
```
├── .cursor/
├── rules/
├── architecture.mdc (omitted)
├── examples.mdc (omitted)
├── good-behaviour.mdc (omitted)
├── vibe-tools.mdc (omitted)
├── yo.mdc (omitted)
├── .cursorindexingignore (omitted)
├── .github/
├── instructions/
├── examples-test.instructions.md (omitted)
├── workflows/
├── ci.yml (omitted)
├── .gitignore (omitted)
├── .npmignore (omitted)
├── .prettierrc (omitted)
├── .repomixignore (omitted)
├── ARCHITECTURE.md (omitted)
├── LICENSE (omitted)
├── README.md (omitted)
├── TODO.md (omitted)
├── UNIFIED_RULES_CONVENTION.md (1100 tokens)
├── bun.lock (omitted)
├── context.json (omitted)
├── examples/
├── README.md (omitted)
├── end-user-cjs-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── metadata-integration.test.ts (omitted)
├── package.json (omitted)
├── end-user-esm-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── package.json (omitted)
├── library-cjs-package/
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── library-esm-package/
├── README.md (omitted)
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── package.json (omitted)
├── reference/
├── README.md (omitted)
├── amp-rules-directory/
├── AGENT.md (omitted)
├── claude-code-directory/
├── CLAUDE.md (omitted)
├── cline-rules-directory/
├── .clinerules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── codex-rules-directory/
├── AGENTS.md (omitted)
├── cursor-rules-directory/
├── .cursor/
├── always-on.mdc (omitted)
├── glob.mdc (omitted)
├── manual.mdc (omitted)
├── model-decision.mdc (omitted)
├── windsurf-rules-directory/
├── .windsurf/
├── rules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── zed-rules-directory/
├── .rules (omitted)
├── scripts/
├── build-examples.ts (omitted)
├── test-examples.ts (omitted)
├── src/
├── cli.ts (omitted)
├── commands/
├── install.ts (omitted)
├── list.ts (omitted)
├── load.ts (omitted)
├── save.ts (omitted)
├── index.ts (omitted)
├── llms/
├── index.ts (omitted)
├── internal.ts (omitted)
├── llms.txt (omitted)
├── providers/
├── amp-provider.ts (omitted)
├── claude-code-provider.ts (omitted)
├── clinerules-provider.ts (omitted)
├── codex-provider.ts (omitted)
├── cursor-provider.ts (omitted)
├── index.ts (omitted)
├── metadata-application.test.ts (omitted)
├── unified-provider.ts (600 tokens)
├── vscode-provider.ts (omitted)
├── windsurf-provider.ts (omitted)
├── zed-provider.ts (omitted)
├── schemas.ts (omitted)
├── text.d.ts (omitted)
├── types.ts (omitted)
├── utils/
├── frontmatter.test.ts (omitted)
├── frontmatter.ts (omitted)
├── path.ts (omitted)
├── rule-formatter.ts (400 tokens)
├── rule-storage.test.ts (omitted)
├── rule-storage.ts (omitted)
├── similarity.ts (omitted)
├── single-file-helpers.ts (1200 tokens)
├── tsconfig.json (omitted)
├── vibe-tools.config.json (omitted)
```
## /UNIFIED_RULES_CONVENTION.md
# Unified .rules File Convention
This document outlines the convention for the unified `.rules` file, a single file designed to store all your AI assistant/editor rules for a project in a centralized and portable manner.
## Purpose
The `.rules` file aims to:
1. **Simplify Rule Management**: Instead of scattering rules across various editor-specific files (e.g., `.cursor/rules/`, `.windsurfrules`, `CLAUDE.md`), you can keep them all in one place.
2. **Improve Portability**: Easily share and version control your project-specific rules along with your codebase.
3. **Standardize Format**: Provide a consistent way to define rules, including common metadata.
### The Challenge: Rule File Proliferation
Many development tools and AI assistants rely on specific configuration files for custom instructions or rules. While this allows for tailored behavior, it can lead to a multitude of rule files scattered across a project or system, each with its own format. This is the problem the `unified` provider and the `.rules` convention aim to simplify.

_Image concept from a [tweet by @aidenybai](https://x.com/aidenybai/status/1923781810820968857/photo/1), illustrating the proliferation of rule files._
## File Location
The `.rules` file should be located in the root directory of your project.
```
my-project/
├── .rules <-- Your unified rules file
├── src/
└── package.json
```
## File Format
The `.rules` file uses an XML-like tagged format to define individual rules. Each rule is enclosed in a tag named after the rule itself. This format is human-readable and easy to parse.
### Structure of a Rule Block
```xml
<your-rule-name>
<!-- Optional: Human-readable metadata -->
<!-- Description: A brief explanation of what the rule does. -->
<!-- AlwaysApply: true/false (Cursor-specific, can be included for compatibility) -->
<!-- Globs: pattern1,pattern2 (Cursor-specific, can be included for compatibility) -->
Your actual rule content goes here.
This can be multi-line plain text, a prompt, or any specific syntax required by the target AI model or tool.
</your-rule-name>
```
**Explanation:**
- **`<your-rule-name>`**: This is the opening tag for the rule. The name should be unique within the `.rules` file and is used to identify the rule (e.g., by the `vibe-rules load unified <your-rule-name>` command).
- **Metadata Comments (Optional)**:
- `<!-- Description: ... -->`: A description of the rule.
- `<!-- AlwaysApply: true/false -->`: Corresponds to Cursor's `alwaysApply` metadata. Useful if you intend to use these rules with Cursor or a system that understands this flag.
- `<!-- Globs: pattern1,pattern2 -->`: Corresponds to Cursor's `globs` metadata. Specify comma-separated glob patterns.
- **Rule Content**: The actual text or prompt for the rule.
- **`</your-rule-name>`**: The closing tag for the rule.
### Example `.rules` File
```xml
<explain-code-snippet>
<!-- Description: Explains the selected code snippet. -->
Explain the following code snippet:
```
{{code}}
```
</explain-code-snippet>
<generate-test-cases>
<!-- Description: Generates test cases for the given function. -->
<!-- Globs: *.test.js,*.spec.ts -->
Given the following function, please generate a comprehensive set of unit test cases in Jest format:
```
{{code}}
```
</generate-test-cases>
<my-custom-task-for-pkg>
This is a rule installed from 'my-package'.
It helps with a specific task related to this package.
</my-custom-task-for-pkg>
```
## Using with `vibe-rules`
### Saving Rules
You can save rules to your local common store and then load them into the `.rules` file, or install them directly from packages.
1. **Save a rule to the local store (optional first step):**
```bash
vibe-rules save my-new-rule -c "This is the content of my new rule."
```
2. **Load a rule into the project's `.rules` file:**
```bash
vibe-rules load unified my-new-rule
# This will create/update .rules in the current directory with the 'my-new-rule' block.
```
### Installing Rules from Packages
When you install rules from an NPM package using the `unified` editor type, `vibe-rules` will add them to your project's `.rules` file. Rule names will be prefixed with the package name.
```bash
# Install rules from 'some-npm-package' into .rules
vibe-rules install unified some-npm-package
# Install rules from all dependencies into .rules
vibe-rules install unified
```
This would result in entries like `<some-npm-package-rulename>...</some-npm-package-rulename>` in your `.rules` file.
### Listing Rules
To see rules specifically defined for the unified provider in your local store:
```bash
vibe-rules list --type unified
```
(Note: `list` command currently lists from the global store, not directly from a project's `.rules` file. To see rules in a project, open the `.rules` file.)
## Benefits of the `.rules` Convention
- **Centralization**: All project-specific AI prompts and rules in one predictable location.
- **Version Control**: Easily track changes to your rules alongside your code.
- **Collaboration**: Share rule configurations seamlessly with your team.
- **Editor Agnostic (Conceptual)**: While `vibe-rules` helps manage this file, the raw `.rules` file can be understood or parsed by other tools or future IDE integrations that adopt this convention.
## Future Considerations
- **Global `.rules`**: Support for a global `~/.rules` file for user-wide rules.
- **Enhanced Tooling**: IDE extensions or features that directly consume and suggest rules from the `.rules` file.
## /src/providers/unified-provider.ts
```ts path="/src/providers/unified-provider.ts"
import path from "path";
import { RuleConfig, RuleProvider, RuleType, RuleGeneratorOptions } from "../types.js";
import { appendOrUpdateTaggedBlock } from "../utils/single-file-helpers.js";
import { createTaggedRuleBlock } from "../utils/rule-formatter.js";
import {
saveInternalRule as saveRuleToInternalStorage,
loadInternalRule as loadRuleFromInternalStorage,
listInternalRules as listRulesFromInternalStorage,
} from "../utils/rule-storage.js";
const UNIFIED_RULE_FILENAME = ".rules";
export class UnifiedRuleProvider implements RuleProvider {
private getRuleFilePath(
ruleName: string,
isGlobal: boolean = false,
projectRoot: string = process.cwd()
): string {
// For unified provider, ruleName is not used in the path, always '.rules'
// isGlobal might determine if it's in projectRoot or user's global .rules, but for now, always project root.
if (isGlobal) {
// Potentially handle a global ~/.rules file in the future
// For now, global unified rules are not distinct from project unified rules
// console.warn('Global unified rules are not yet uniquely supported, using project .rules');
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
return path.join(projectRoot, UNIFIED_RULE_FILENAME);
}
async saveRule(config: RuleConfig, _options?: RuleGeneratorOptions): Promise<string> {
return saveRuleToInternalStorage(RuleType.UNIFIED, config);
}
async loadRule(name: string): Promise<RuleConfig | null> {
return loadRuleFromInternalStorage(RuleType.UNIFIED, name);
}
async listRules(): Promise<string[]> {
return listRulesFromInternalStorage(RuleType.UNIFIED);
}
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string {
// For .rules, we use the tagged block format directly
return createTaggedRuleBlock(config, options);
}
async appendRule(
name: string,
targetPath?: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean> {
const ruleConfig = await this.loadRule(name);
if (!ruleConfig) {
console.error(`Rule "${name}" not found in local unified store.`);
return false;
}
const filePath = targetPath || this.getRuleFilePath(name, isGlobal);
return this.appendFormattedRule(ruleConfig, filePath, isGlobal, options);
}
async appendFormattedRule(
config: RuleConfig,
targetPath: string, // This will be the path to the .rules file
_isGlobal?: boolean, // isGlobal might be less relevant here if .rules is always project-based
options?: RuleGeneratorOptions
): Promise<boolean> {
// The 'targetPath' for unified provider should directly be the path to the '.rules' file.
// The 'config.name' is used for the tag name within the '.rules' file.
return appendOrUpdateTaggedBlock(targetPath, config, options, false);
}
}
```
## /src/utils/rule-formatter.ts
```ts path="/src/utils/rule-formatter.ts"
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
/**
* Formats rule content for non-cursor providers that use XML-like tags.
* Includes additional metadata like alwaysApply and globs in a human-readable format
* within the rule content.
*/
export function formatRuleWithMetadata(config: RuleConfig, options?: RuleGeneratorOptions): string {
let formattedContent = config.content;
// Add metadata lines at the beginning of content if they exist
const metadataLines = [];
// Add alwaysApply information if provided
if (options?.alwaysApply !== undefined) {
metadataLines.push(
`Always Apply: ${options.alwaysApply ? "true" : "false"} - ${
options.alwaysApply
? "This rule should ALWAYS be applied by the AI"
: "This rule should only be applied when relevant files are open"
}`
);
}
// Add globs information if provided
if (options?.globs && options.globs.length > 0) {
const globsStr = Array.isArray(options.globs) ? options.globs.join(", ") : options.globs;
// Skip adding the glob info if it's just a generic catch-all pattern
const isCatchAllPattern =
globsStr === "**/*" ||
(Array.isArray(options.globs) && options.globs.length === 1 && options.globs[0] === "**/*");
if (!isCatchAllPattern) {
metadataLines.push(`Always apply this rule in these files: ${globsStr}`);
}
}
// If we have metadata, add it to the beginning of the content
if (metadataLines.length > 0) {
formattedContent = `${metadataLines.join("\n")}\n\n${config.content}`;
}
return formattedContent;
}
/**
* Creates a complete XML-like block for a rule, including start/end tags
* and formatted content with metadata
*/
export function createTaggedRuleBlock(config: RuleConfig, options?: RuleGeneratorOptions): string {
const formattedContent = formatRuleWithMetadata(config, options);
const startTag = `<${config.name}>`;
const endTag = `</${config.name}>`;
return `${startTag}\n${formattedContent}\n${endTag}`;
}
```
## /src/utils/single-file-helpers.ts
```ts path="/src/utils/single-file-helpers.ts"
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra/esm";
import * as path from "path";
import { RuleConfig, RuleGeneratorOptions } from "../types.js";
import { createTaggedRuleBlock } from "./rule-formatter.js";
import { ensureDirectoryExists } from "./path.js";
import { debugLog } from "../cli.js";
/**
* Appends or updates a tagged block within a single target file.
*
* Reads the file content, looks for an existing block matching the rule name tag,
* replaces it if found, or appends the new block otherwise.
*
* @param targetPath The path to the target file.
* @param config The rule configuration.
* @param options Optional generator options.
* @param appendInsideVibeRulesBlock If true, tries to append within <vibe-rules Integration> block.
* @returns Promise resolving to true if successful, false otherwise.
*/
export async function appendOrUpdateTaggedBlock(
targetPath: string,
config: RuleConfig,
options?: RuleGeneratorOptions,
appendInsideVibeRulesBlock: boolean = false
): Promise<boolean> {
try {
// Ensure the PARENT directory exists, but only if it's not the current directory itself.
const parentDir = path.dirname(targetPath);
// Avoid calling ensureDirectoryExists('.') as it's unnecessary and might mask other issues.
if (parentDir !== ".") {
ensureDirectoryExists(parentDir);
}
let currentContent = "";
let fileExists = true;
try {
// Check if the file exists (not directory)
const stats = await fs.stat(targetPath).catch(() => null);
if (stats) {
if (stats.isDirectory()) {
// If it's a directory but should be a file, remove it
console.warn(
`Found a directory at ${targetPath} but expected a file. Removing directory...`
);
await fs.rm(targetPath, { recursive: true, force: true });
fileExists = false;
} else {
// It's a file, read its content
currentContent = await fs.readFile(targetPath, "utf-8");
}
} else {
fileExists = false;
}
} catch (error: any) {
if (error.code !== "ENOENT") {
console.error(`Error accessing target file ${targetPath}:`, error);
return false;
}
// File doesn't exist, which is fine, we'll create it.
fileExists = false;
debugLog(`Target file ${targetPath} not found, will create.`);
}
// If file doesn't exist, explicitly create an empty file first
// This ensures we're creating a file, not a directory
if (!fileExists) {
try {
// Ensure the file exists by explicitly creating it as an empty file
// Use fsExtra.ensureFileSync which is designed to create the file (not directory)
fsExtra.ensureFileSync(targetPath);
debugLog(`Created empty file: ${targetPath}`);
} catch (error) {
console.error(`Failed to create empty file ${targetPath}:`, error);
return false;
}
}
const newBlock = createTaggedRuleBlock(config, options);
const ruleNameRegexStr = config.name.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); // Escape regex special chars
const existingBlockRegex = new RegExp(
`<${ruleNameRegexStr}>[\\s\\S]*?</${ruleNameRegexStr}>`,
"m"
);
let updatedContent: string;
const match = currentContent.match(existingBlockRegex);
if (match) {
// Update existing block
debugLog(`Updating existing block for rule "${config.name}" in ${targetPath}`);
updatedContent = currentContent.replace(existingBlockRegex, newBlock);
} else {
// Append new block
debugLog(`Appending new block for rule "${config.name}" to ${targetPath}`);
// Check for comment-style integration blocks
const commentIntegrationEndTag = "<!-- /vibe-rules Integration -->";
const commentEndIndex = currentContent.lastIndexOf(commentIntegrationEndTag);
let integrationEndIndex = -1;
let integrationStartTag = "";
if (commentEndIndex !== -1) {
integrationEndIndex = commentEndIndex;
integrationStartTag = "<!-- vibe-rules Integration -->";
}
if (appendInsideVibeRulesBlock && integrationEndIndex !== -1) {
// Append inside the vibe-rules block if requested and found
const insertionPoint = integrationEndIndex;
updatedContent =
currentContent.slice(0, insertionPoint).trimEnd() +
"\n\n" + // Ensure separation
newBlock +
"\n\n" + // Ensure separation
currentContent.slice(insertionPoint);
debugLog(`Appending rule inside ${integrationStartTag} block.`);
} else if (appendInsideVibeRulesBlock && integrationEndIndex === -1) {
// Create the integration block if it doesn't exist and we want to append inside it
const separator = currentContent.trim().length > 0 ? "\n\n" : "";
const startTag = "<!-- vibe-rules Integration -->";
const endTag = "<!-- /vibe-rules Integration -->";
updatedContent =
currentContent.trimEnd() + separator + startTag + "\n\n" + newBlock + "\n\n" + endTag;
debugLog(`Created new ${startTag} block with rule.`);
} else {
// Append to the end
const separator = currentContent.trim().length > 0 ? "\n\n" : ""; // Add separator if file not empty
updatedContent = currentContent.trimEnd() + separator + newBlock;
if (appendInsideVibeRulesBlock) {
debugLog(`Could not find vibe-rules Integration block, appending rule to the end.`);
}
}
}
// Ensure file ends with a newline
if (!updatedContent.endsWith("\n")) {
updatedContent += "\n";
}
await fs.writeFile(targetPath, updatedContent, "utf-8");
console.log(`Successfully applied rule "${config.name}" to ${targetPath}`);
return true;
} catch (error) {
console.error(`Error applying rule "${config.name}" to ${targetPath}:`, error);
return false;
}
}
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
```
├── .cursor/
├── rules/
├── architecture.mdc (omitted)
├── examples.mdc (omitted)
├── good-behaviour.mdc (omitted)
├── vibe-tools.mdc (omitted)
├── yo.mdc (omitted)
├── .cursorindexingignore (omitted)
├── .github/
├── instructions/
├── examples-test.instructions.md (omitted)
├── workflows/
├── ci.yml (omitted)
├── .gitignore (omitted)
├── .npmignore (omitted)
├── .prettierrc (omitted)
├── .repomixignore (omitted)
├── LICENSE (omitted)
├── README.md (omitted)
├── TODO.md (omitted)
├── UNIFIED_RULES_CONVENTION.md (omitted)
├── bun.lock (omitted)
├── context.json (omitted)
├── examples/
├── README.md (omitted)
├── end-user-cjs-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── metadata-integration.test.ts (omitted)
├── package.json (omitted)
├── end-user-esm-package/
├── .gitignore (omitted)
├── install.test.ts (omitted)
├── package.json (omitted)
├── library-cjs-package/
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── library-esm-package/
├── README.md (omitted)
├── package.json (omitted)
├── src/
├── llms.ts (omitted)
├── tsconfig.json (omitted)
├── package.json (omitted)
├── reference/
├── README.md (omitted)
├── amp-rules-directory/
├── AGENT.md (omitted)
├── claude-code-directory/
├── CLAUDE.md (omitted)
├── cline-rules-directory/
├── .clinerules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── codex-rules-directory/
├── AGENTS.md (omitted)
├── cursor-rules-directory/
├── .cursor/
├── always-on.mdc (omitted)
├── glob.mdc (omitted)
├── manual.mdc (omitted)
├── model-decision.mdc (omitted)
├── windsurf-rules-directory/
├── .windsurf/
├── rules/
├── always-on.md (omitted)
├── glob.md (omitted)
├── manual.md (omitted)
├── model-decision.md (omitted)
├── zed-rules-directory/
├── .rules (omitted)
├── scripts/
├── build-examples.ts (omitted)
├── test-examples.ts (omitted)
├── src/
├── cli.ts (500 tokens)
├── commands/
├── install.ts (omitted)
├── list.ts (omitted)
├── load.ts (omitted)
├── save.ts (omitted)
├── index.ts (omitted)
├── llms/
├── index.ts (omitted)
├── internal.ts (omitted)
├── llms.txt (omitted)
├── providers/
├── amp-provider.ts (omitted)
├── claude-code-provider.ts (omitted)
├── clinerules-provider.ts (omitted)
├── codex-provider.ts (omitted)
├── cursor-provider.ts (omitted)
├── index.ts (omitted)
├── metadata-application.test.ts (omitted)
├── unified-provider.ts (omitted)
├── vscode-provider.ts (omitted)
├── windsurf-provider.ts (omitted)
├── zed-provider.ts (omitted)
├── schemas.ts (500 tokens)
├── text.d.ts (omitted)
├── types.ts (400 tokens)
├── utils/
├── frontmatter.test.ts (omitted)
├── frontmatter.ts (omitted)
├── path.ts (omitted)
├── rule-formatter.ts (omitted)
├── rule-storage.test.ts (omitted)
├── rule-storage.ts (omitted)
├── similarity.ts (omitted)
├── single-file-helpers.ts (omitted)
├── tsconfig.json (omitted)
├── vibe-tools.config.json (omitted)
```
## /src/cli.ts
```ts path="/src/cli.ts"
#!/usr/bin/env node
import { Command } from "commander";
import chalk from "chalk";
import { installCommandAction } from "./commands/install.js";
import { saveCommandAction } from "./commands/save.js";
import { loadCommandAction } from "./commands/load.js";
import { listCommandAction } from "./commands/list.js";
// Simple debug logger
export let isDebugEnabled = false;
export const debugLog = (message: string, ...optionalParams: any[]) => {
if (isDebugEnabled) {
console.log(chalk.dim(`[Debug] ${message}`), ...optionalParams);
}
};
const program = new Command();
program
.name("vibe-rules")
.description(
"A utility for managing Cursor rules, Windsurf rules, Amp rules, and other AI prompts"
)
.version("0.1.0")
.option("--debug", "Enable debug logging", false);
program.on("option:debug", () => {
isDebugEnabled = program.opts().debug;
debugLog("Debug logging enabled.");
});
program
.command("save")
.description("Save a rule to the local store")
.argument("<n>", "Name of the rule")
.option("-c, --content <content>", "Rule content")
.option("-f, --file <file>", "Load rule content from file")
.option("-d, --description <desc>", "Rule description")
.action(saveCommandAction);
program
.command("list")
.description("List all saved rules from the common store")
.action(listCommandAction);
program
.command("load")
.alias("add")
.description("Apply a saved rule to an editor configuration")
.argument("<n>", "Name of the rule to apply")
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(loadCommandAction);
program
.command("install")
.description(
"Install rules from an NPM package or all dependencies directly into an editor configuration"
)
.argument(
"<editor>",
"Target editor type (cursor, windsurf, claude-code, codex, amp, clinerules, roo, zed, unified, vscode)"
)
.argument("[packageName]", "Optional NPM package name to install rules from")
.option("-g, --global", "Apply to global config path if supported (claude-code, codex)", false)
.option("-t, --target <path>", "Custom target path (overrides default and global)")
.action(installCommandAction);
program.parse(process.argv);
if (process.argv.length <= 2) {
program.help();
}
```
## /src/schemas.ts
```ts path="/src/schemas.ts"
import { z } from "zod";
export const RuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
// Add other fields from RuleConfig if they exist and need validation
});
// Schema for metadata in stored rules
export const RuleGeneratorOptionsSchema = z
.object({
description: z.string().optional(),
isGlobal: z.boolean().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(),
debug: z.boolean().optional(),
})
.optional();
// Schema for the enhanced local storage format with metadata
export const StoredRuleConfigSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
content: z.string().min(1, "Rule content cannot be empty"),
description: z.string().optional(),
metadata: RuleGeneratorOptionsSchema,
});
// Original schema for reference (might still be used elsewhere, e.g., save command)
export const VibeRulesSchema = z.array(RuleConfigSchema);
// --- Schemas for Package Exports (`<packageName>/llms`) ---
// Schema for the flexible rule object format within packages
export const PackageRuleObjectSchema = z.object({
name: z.string().min(1, "Rule name cannot be empty"),
rule: z.string().min(1, "Rule content cannot be empty"), // Renamed from content
description: z.string().optional(),
alwaysApply: z.boolean().optional(),
globs: z.union([z.string(), z.array(z.string())]).optional(), // Allow string or array
});
// Schema for a single item in the package export (either a string or the object)
export const PackageRuleItemSchema = z.union([
z.string().min(1, "Rule string cannot be empty"),
PackageRuleObjectSchema, // Defined above now
]);
// Schema for the default export of package/llms (array of strings or objects)
export const VibePackageRulesSchema = z.array(PackageRuleItemSchema);
// --- Type Helpers ---
// Basic RuleConfig type
export type RuleConfig = z.infer<typeof RuleConfigSchema>;
// Type for the enhanced local storage format
export type StoredRuleConfig = z.infer<typeof StoredRuleConfigSchema>;
// Type for rule generator options
export type RuleGeneratorOptions = z.infer<typeof RuleGeneratorOptionsSchema>;
// Type for the flexible package rule object
export type PackageRuleObject = z.infer<typeof PackageRuleObjectSchema>;
// Type for a single item in the package export array
export type PackageRuleItem = z.infer<typeof PackageRuleItemSchema>;
```
## /src/types.ts
```ts path="/src/types.ts"
/**
* Rule type definitions for the vibe-rules utility
*/
export type * from "./schemas.js";
export interface RuleConfig {
name: string;
content: string;
description?: string;
// Additional properties can be added as needed
}
// Extended interface for storing rules locally with metadata
export interface StoredRuleConfig {
name: string;
content: string;
description?: string;
metadata?: RuleGeneratorOptions;
}
export const RuleType = {
CURSOR: "cursor",
WINDSURF: "windsurf",
CLAUDE_CODE: "claude-code",
CODEX: "codex",
AMP: "amp",
CLINERULES: "clinerules",
ROO: "roo",
ZED: "zed",
UNIFIED: "unified",
VSCODE: "vscode",
CUSTOM: "custom",
} as const;
export type RuleTypeArray = (typeof RuleType)[keyof typeof RuleType][];
export type RuleType = (typeof RuleType)[keyof typeof RuleType];
export interface RuleProvider {
/**
* Creates a new rule file with the given content
*/
saveRule(config: RuleConfig, options?: RuleGeneratorOptions): Promise<string>;
/**
* Loads a rule from storage
*/
loadRule(name: string): Promise<RuleConfig | null>;
/**
* Lists all available rules
*/
listRules(): Promise<string[]>;
/**
* Appends a rule to an existing file
*/
appendRule(name: string, targetPath?: string): Promise<boolean>;
/**
* Formats and appends a rule directly from a RuleConfig object
*/
appendFormattedRule(
config: RuleConfig,
targetPath: string,
isGlobal?: boolean,
options?: RuleGeneratorOptions
): Promise<boolean>;
/**
* Generates formatted rule content with editor-specific formatting
*/
generateRuleContent(config: RuleConfig, options?: RuleGeneratorOptions): string;
}
export interface RuleGeneratorOptions {
description?: string;
isGlobal?: boolean;
alwaysApply?: boolean;
globs?: string | string[];
debug?: boolean;
// Additional options for specific rule types
}
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.