YeonGyu-Kim fcdfcd3186 feat(hooks): add rules-injector hook for .cursor/rules and .claude/rules support
Implements adaptive rule injection similar to Claude Code's rule system:
- Searches .cursor/rules and .claude/rules directories recursively
- Supports YAML frontmatter with globs, paths, alwaysApply, description
- Adaptive project root detection (finds markers even outside ctx.directory)
- Symlink duplicate detection via realpath comparison
- Content hash deduplication (SHA-256) to avoid re-injecting same rules
- picomatch-based glob pattern matching for file-specific rules

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 00:35:34 +09:00

64 lines
1.6 KiB
TypeScript

import { createHash } from "crypto"
import { relative } from "node:path"
import picomatch from "picomatch"
import type { RuleMetadata } from "./types"
export interface MatchResult {
applies: boolean
reason?: string
}
/**
* Check if a rule should apply to the current file based on metadata
*/
export function shouldApplyRule(
metadata: RuleMetadata,
currentFilePath: string,
projectRoot: string | null
): MatchResult {
if (metadata.alwaysApply === true) {
return { applies: true, reason: "alwaysApply" }
}
const globs = metadata.globs
if (!globs) {
return { applies: false }
}
const patterns = Array.isArray(globs) ? globs : [globs]
if (patterns.length === 0) {
return { applies: false }
}
const relativePath = projectRoot ? relative(projectRoot, currentFilePath) : currentFilePath
for (const pattern of patterns) {
if (picomatch.isMatch(relativePath, pattern, { dot: true, bash: true })) {
return { applies: true, reason: `glob: ${pattern}` }
}
}
return { applies: false }
}
/**
* Check if realPath already exists in cache (symlink deduplication)
*/
export function isDuplicateByRealPath(realPath: string, cache: Set<string>): boolean {
return cache.has(realPath)
}
/**
* Create SHA-256 hash of content, truncated to 16 chars
*/
export function createContentHash(content: string): string {
return createHash("sha256").update(content).digest("hex").slice(0, 16)
}
/**
* Check if content hash already exists in cache
*/
export function isDuplicateByContentHash(hash: string, cache: Set<string>): boolean {
return cache.has(hash)
}