Files
oh-my-opencode/src/hooks/rules-injector/rule-file-finder.ts
T

140 lines
4.2 KiB
TypeScript
Raw Normal View History

import { existsSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import {
PROJECT_RULE_FILES,
PROJECT_RULE_SUBDIRS,
USER_RULE_DIR,
OPENCODE_USER_RULE_DIRS,
} from "./constants";
import type { RuleFileCandidate } from "./types";
import { findRuleFilesRecursive, safeRealpathSync } from "./rule-file-scanner";
export interface FindRuleFilesOptions {
/**
* When true, skip loading rules from ~/.claude/rules/.
* Use when claude_code integration is disabled to prevent
* Claude Code-specific instructions from leaking into non-Claude agents.
*/
skipClaudeUserRules?: boolean;
}
/**
* Find all rule files for a given context.
* Searches from currentFile upward to projectRoot for rule directories,
* then user-level directory (~/.claude/rules).
*
* IMPORTANT: This searches EVERY directory from file to project root.
* Not just the project root itself.
*
* @param projectRoot - Project root path (or null if outside any project)
* @param homeDir - User home directory
* @param currentFile - Current file being edited (for distance calculation)
* @returns Array of rule file candidates sorted by distance
*/
export function findRuleFiles(
projectRoot: string | null,
homeDir: string,
currentFile: string,
options?: FindRuleFilesOptions,
): RuleFileCandidate[] {
const candidates: RuleFileCandidate[] = [];
const seenRealPaths = new Set<string>();
// Search from current file's directory up to project root
let currentDir = dirname(currentFile);
let distance = 0;
while (true) {
// Search rule directories in current directory
for (const [parent, subdir] of PROJECT_RULE_SUBDIRS) {
const ruleDir = join(currentDir, parent, subdir);
const files: string[] = [];
findRuleFilesRecursive(ruleDir, files);
for (const filePath of files) {
const realPath = safeRealpathSync(filePath);
if (seenRealPaths.has(realPath)) continue;
seenRealPaths.add(realPath);
candidates.push({
path: filePath,
realPath,
isGlobal: false,
distance,
});
}
}
// Stop at project root or filesystem root
if (projectRoot && currentDir === projectRoot) break;
const parentDir = dirname(currentDir);
if (parentDir === currentDir) break;
currentDir = parentDir;
distance++;
}
// Check for single-file rules at project root (e.g., .github/copilot-instructions.md)
if (projectRoot) {
for (const ruleFile of PROJECT_RULE_FILES) {
const filePath = join(projectRoot, ruleFile);
if (existsSync(filePath)) {
try {
const stat = statSync(filePath);
if (stat.isFile()) {
const realPath = safeRealpathSync(filePath);
if (!seenRealPaths.has(realPath)) {
seenRealPaths.add(realPath);
candidates.push({
path: filePath,
realPath,
isGlobal: false,
distance: 0,
isSingleFile: true,
});
}
}
} catch {
// Skip if file can't be read
}
}
}
}
// Search user-level rule directories
// Always search OpenCode-native dirs (~/.sisyphus/rules, ~/.opencode/rules)
const userRuleDirs: string[] = OPENCODE_USER_RULE_DIRS.map((dir) => join(homeDir, dir));
// Only search ~/.claude/rules when claude_code integration is not disabled
if (!options?.skipClaudeUserRules) {
userRuleDirs.push(join(homeDir, USER_RULE_DIR));
}
for (const userRuleDir of userRuleDirs) {
const userFiles: string[] = [];
findRuleFilesRecursive(userRuleDir, userFiles);
for (const filePath of userFiles) {
const realPath = safeRealpathSync(filePath);
if (seenRealPaths.has(realPath)) continue;
seenRealPaths.add(realPath);
candidates.push({
path: filePath,
realPath,
isGlobal: true,
distance: 9999, // Global rules always have max distance
});
}
}
// Sort by distance (closest first, then global rules last)
candidates.sort((a, b) => {
if (a.isGlobal !== b.isGlobal) {
return a.isGlobal ? 1 : -1;
}
return a.distance - b.distance;
});
return candidates;
}