vibe-rules
Version:
A utility for managing Cursor rules, Windsurf rules, and other AI prompts
209 lines • 8.98 kB
JavaScript
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 GEMINI_HOME_DIR = path.join(os.homedir(), ".gemini");
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() {
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, ruleName) {
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, ruleName, // ruleName might not be relevant for some types like Claude/Codex global
isGlobal = false, projectRoot = process.cwd()) {
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.GEMINI:
// Gemini rules are GEMINI.md, either global or local
return isGlobal
? path.join(GEMINI_HOME_DIR, "GEMINI.md")
: path.join(projectRoot, "GEMINI.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, isGlobalHint = false // Hint for providers like Claude/Codex
) {
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.GEMINI:
// Default target depends on global hint
return isGlobalHint
? GEMINI_HOME_DIR // Directory
: process.cwd(); // Project root (for local GEMINI.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) {
try {
fs.ensureDirSync(dirPath);
debugLog(`Ensured directory exists: ${dirPath}`);
}
catch (err) {
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, isGlobal, projectRoot = process.cwd()) {
let checkPath;
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.GEMINI:
checkPath = isGlobal
? path.join(GEMINI_HOME_DIR, "GEMINI.md")
: path.join(projectRoot, "GEMINI.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) {
return name
.toLowerCase()
.replace(/[^a-z0-9_]+/g, "-")
.replace(/^-|-$/g, "");
}
//# sourceMappingURL=path.js.map