feat: allow goose askai bot to search goose codebase (#7508)
Signed-off-by: The-Best-Codes <106822363+The-Best-Codes@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export interface CodeSearchResult {
|
||||
filePath: string;
|
||||
line: number;
|
||||
content: string;
|
||||
context: string;
|
||||
}
|
||||
|
||||
const SOURCE_EXTENSIONS = new Set([
|
||||
".rs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".json",
|
||||
".toml",
|
||||
".yaml",
|
||||
".yml",
|
||||
".css",
|
||||
".scss",
|
||||
".html",
|
||||
".md",
|
||||
".sql",
|
||||
".sh",
|
||||
".mts",
|
||||
]);
|
||||
|
||||
const IGNORED_DIRS = new Set([
|
||||
"node_modules",
|
||||
"target",
|
||||
"dist",
|
||||
"out",
|
||||
".vite",
|
||||
".git",
|
||||
"build",
|
||||
"coverage",
|
||||
]);
|
||||
|
||||
function getCodebaseDir(): string {
|
||||
return process.env.CODEBASE_PATH || path.join(process.cwd(), "../..");
|
||||
}
|
||||
|
||||
function getSearchableDirs(): { name: string; path: string }[] {
|
||||
const base = path.resolve(getCodebaseDir());
|
||||
return [
|
||||
{ name: "ui", path: path.join(base, "ui") },
|
||||
{ name: "crates", path: path.join(base, "crates") },
|
||||
];
|
||||
}
|
||||
|
||||
function shouldSkipDir(dirName: string): boolean {
|
||||
return IGNORED_DIRS.has(dirName);
|
||||
}
|
||||
|
||||
function isSourceFile(fileName: string): boolean {
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
return SOURCE_EXTENSIONS.has(ext);
|
||||
}
|
||||
|
||||
function getContextLines(
|
||||
lines: string[],
|
||||
matchLine: number,
|
||||
contextSize: number = 2,
|
||||
): string {
|
||||
const start = Math.max(0, matchLine - contextSize);
|
||||
const end = Math.min(lines.length - 1, matchLine + contextSize);
|
||||
const contextLines: string[] = [];
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
const prefix = i === matchLine ? ">" : " ";
|
||||
contextLines.push(`${prefix} ${i + 1}: ${lines[i]}`);
|
||||
}
|
||||
|
||||
return contextLines.join("\n");
|
||||
}
|
||||
|
||||
function searchInFile(
|
||||
filePath: string,
|
||||
pattern: RegExp,
|
||||
baseDir: string,
|
||||
): CodeSearchResult[] {
|
||||
const results: CodeSearchResult[] = [];
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (pattern.test(lines[i])) {
|
||||
const relativePath = path.relative(baseDir, filePath);
|
||||
results.push({
|
||||
filePath: relativePath,
|
||||
line: i + 1,
|
||||
content: lines[i].trim(),
|
||||
context: getContextLines(lines, i),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip files that can't be read (binary, permissions, etc.)
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function walkAndSearch(
|
||||
dir: string,
|
||||
pattern: RegExp,
|
||||
baseDir: string,
|
||||
results: CodeSearchResult[],
|
||||
maxResults: number,
|
||||
): void {
|
||||
if (results.length >= maxResults) return;
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (results.length >= maxResults) return;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (shouldSkipDir(entry.name)) continue;
|
||||
walkAndSearch(
|
||||
path.join(dir, entry.name),
|
||||
pattern,
|
||||
baseDir,
|
||||
results,
|
||||
maxResults,
|
||||
);
|
||||
} else if (isSourceFile(entry.name)) {
|
||||
const fileResults = searchInFile(
|
||||
path.join(dir, entry.name),
|
||||
pattern,
|
||||
baseDir,
|
||||
);
|
||||
for (const result of fileResults) {
|
||||
if (results.length >= maxResults) return;
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error walking directory ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function searchCodebase(
|
||||
query: string,
|
||||
limit: number = 20,
|
||||
scope?: string,
|
||||
): CodeSearchResult[] {
|
||||
const searchDirs = getSearchableDirs();
|
||||
const allResults: CodeSearchResult[] = [];
|
||||
|
||||
let pattern: RegExp;
|
||||
try {
|
||||
pattern = new RegExp(query, "i");
|
||||
} catch {
|
||||
pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
||||
}
|
||||
|
||||
for (const dir of searchDirs) {
|
||||
if (scope && dir.name !== scope) continue;
|
||||
|
||||
if (!fs.existsSync(dir.path)) {
|
||||
logger.warn(`Codebase directory not found: ${dir.path}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
walkAndSearch(
|
||||
dir.path,
|
||||
pattern,
|
||||
path.resolve(getCodebaseDir()),
|
||||
allResults,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
logger.verbose(
|
||||
`Code search for "${query}" returned ${allResults.length} results`,
|
||||
);
|
||||
return allResults;
|
||||
}
|
||||
|
||||
export function listCodebaseFiles(
|
||||
directory: string,
|
||||
): { filePath: string; isDirectory: boolean }[] {
|
||||
const baseDir = path.resolve(getCodebaseDir());
|
||||
const targetDir = path.resolve(path.join(baseDir, directory));
|
||||
|
||||
if (!targetDir.startsWith(baseDir + "/")) {
|
||||
throw new Error("Invalid path - directory traversal not allowed");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
throw new Error(`Directory not found: ${directory}`);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(targetDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Not a directory: ${directory}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => !shouldSkipDir(entry.name))
|
||||
.map((entry) => ({
|
||||
filePath: path.join(directory, entry.name),
|
||||
isDirectory: entry.isDirectory(),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
|
||||
return a.filePath.localeCompare(b.filePath);
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list directory: ${directory}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user