264 lines
6.5 KiB
TypeScript
264 lines
6.5 KiB
TypeScript
/**
|
|
* Context Gathering Service
|
|
*
|
|
* Automatically gathers project context for ask mode.
|
|
* Provides codebase overview without requiring manual file references.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
import { join } from "path";
|
|
|
|
export interface ProjectContext {
|
|
projectType: string;
|
|
name?: string;
|
|
description?: string;
|
|
mainLanguage: string;
|
|
frameworks: string[];
|
|
structure: string;
|
|
keyFiles: string[];
|
|
dependencies?: string[];
|
|
}
|
|
|
|
interface ProjectConfig {
|
|
name?: string;
|
|
description?: string;
|
|
dependencies?: Record<string, string>;
|
|
devDependencies?: Record<string, string>;
|
|
scripts?: Record<string, string>;
|
|
}
|
|
|
|
const PROJECT_MARKERS: Record<string, { type: string; language: string }> = {
|
|
"package.json": { type: "Node.js", language: "JavaScript/TypeScript" },
|
|
"tsconfig.json": { type: "TypeScript", language: "TypeScript" },
|
|
"Cargo.toml": { type: "Rust", language: "Rust" },
|
|
"go.mod": { type: "Go", language: "Go" },
|
|
"pom.xml": { type: "Maven/Java", language: "Java" },
|
|
"build.gradle": { type: "Gradle/Java", language: "Java" },
|
|
"pyproject.toml": { type: "Python", language: "Python" },
|
|
"setup.py": { type: "Python", language: "Python" },
|
|
"requirements.txt": { type: "Python", language: "Python" },
|
|
"Gemfile": { type: "Ruby", language: "Ruby" },
|
|
"composer.json": { type: "PHP", language: "PHP" },
|
|
".csproj": { type: ".NET", language: "C#" },
|
|
};
|
|
|
|
const FRAMEWORK_MARKERS: Record<string, string[]> = {
|
|
react: ["react", "react-dom", "next", "gatsby"],
|
|
vue: ["vue", "nuxt"],
|
|
angular: ["@angular/core"],
|
|
svelte: ["svelte", "sveltekit"],
|
|
express: ["express"],
|
|
fastify: ["fastify"],
|
|
nestjs: ["@nestjs/core"],
|
|
django: ["django"],
|
|
flask: ["flask"],
|
|
rails: ["rails"],
|
|
spring: ["spring-boot"],
|
|
};
|
|
|
|
const IGNORED_DIRS = new Set([
|
|
"node_modules",
|
|
".git",
|
|
"dist",
|
|
"build",
|
|
".next",
|
|
".nuxt",
|
|
"target",
|
|
"__pycache__",
|
|
".venv",
|
|
"venv",
|
|
"vendor",
|
|
".idea",
|
|
".vscode",
|
|
"coverage",
|
|
]);
|
|
|
|
const detectProjectType = (workingDir: string): { type: string; language: string } => {
|
|
for (const [marker, info] of Object.entries(PROJECT_MARKERS)) {
|
|
if (existsSync(join(workingDir, marker))) {
|
|
return info;
|
|
}
|
|
}
|
|
return { type: "Unknown", language: "Unknown" };
|
|
};
|
|
|
|
const detectFrameworks = (deps: Record<string, string>): string[] => {
|
|
const frameworks: string[] = [];
|
|
|
|
for (const [framework, markers] of Object.entries(FRAMEWORK_MARKERS)) {
|
|
for (const marker of markers) {
|
|
if (deps[marker]) {
|
|
frameworks.push(framework);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return frameworks;
|
|
};
|
|
|
|
const readPackageJson = (workingDir: string): ProjectConfig | null => {
|
|
const packagePath = join(workingDir, "package.json");
|
|
if (!existsSync(packagePath)) return null;
|
|
|
|
try {
|
|
const content = readFileSync(packagePath, "utf-8");
|
|
return JSON.parse(content) as ProjectConfig;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getDirectoryStructure = (
|
|
dir: string,
|
|
baseDir: string,
|
|
depth = 0,
|
|
maxDepth = 3,
|
|
): string[] => {
|
|
if (depth >= maxDepth) return [];
|
|
|
|
const entries: string[] = [];
|
|
try {
|
|
const items = readdirSync(dir);
|
|
|
|
for (const item of items) {
|
|
if (IGNORED_DIRS.has(item) || item.startsWith(".")) continue;
|
|
|
|
const fullPath = join(dir, item);
|
|
|
|
try {
|
|
const stat = statSync(fullPath);
|
|
const indent = " ".repeat(depth);
|
|
|
|
if (stat.isDirectory()) {
|
|
entries.push(`${indent}${item}/`);
|
|
const subEntries = getDirectoryStructure(fullPath, baseDir, depth + 1, maxDepth);
|
|
entries.push(...subEntries);
|
|
} else if (depth < 2) {
|
|
entries.push(`${indent}${item}`);
|
|
}
|
|
} catch {
|
|
// Skip inaccessible files
|
|
}
|
|
}
|
|
} catch {
|
|
// Skip inaccessible directories
|
|
}
|
|
|
|
return entries;
|
|
};
|
|
|
|
const getKeyFiles = (workingDir: string): string[] => {
|
|
const keyPatterns = [
|
|
"README.md",
|
|
"readme.md",
|
|
"README",
|
|
"package.json",
|
|
"tsconfig.json",
|
|
"Cargo.toml",
|
|
"go.mod",
|
|
"pyproject.toml",
|
|
".env.example",
|
|
"docker-compose.yml",
|
|
"Dockerfile",
|
|
"Makefile",
|
|
];
|
|
|
|
const found: string[] = [];
|
|
for (const pattern of keyPatterns) {
|
|
if (existsSync(join(workingDir, pattern))) {
|
|
found.push(pattern);
|
|
}
|
|
}
|
|
|
|
return found;
|
|
};
|
|
|
|
const getMainDependencies = (pkg: ProjectConfig): string[] => {
|
|
const allDeps = {
|
|
...pkg.dependencies,
|
|
...pkg.devDependencies,
|
|
};
|
|
|
|
const importantDeps = Object.keys(allDeps).filter(
|
|
(dep) =>
|
|
!dep.startsWith("@types/") &&
|
|
!dep.startsWith("eslint") &&
|
|
!dep.startsWith("prettier") &&
|
|
!dep.includes("lint"),
|
|
);
|
|
|
|
return importantDeps.slice(0, 15);
|
|
};
|
|
|
|
/**
|
|
* Gather comprehensive project context
|
|
*/
|
|
export const gatherProjectContext = (workingDir: string): ProjectContext => {
|
|
const { type, language } = detectProjectType(workingDir);
|
|
const pkg = readPackageJson(workingDir);
|
|
const structure = getDirectoryStructure(workingDir, workingDir);
|
|
const keyFiles = getKeyFiles(workingDir);
|
|
|
|
const frameworks = pkg
|
|
? detectFrameworks({ ...pkg.dependencies, ...pkg.devDependencies })
|
|
: [];
|
|
|
|
const dependencies = pkg ? getMainDependencies(pkg) : undefined;
|
|
|
|
return {
|
|
projectType: type,
|
|
name: pkg?.name,
|
|
description: pkg?.description,
|
|
mainLanguage: language,
|
|
frameworks,
|
|
structure: structure.slice(0, 50).join("\n"),
|
|
keyFiles,
|
|
dependencies,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Build a formatted context string for injection into prompts
|
|
*/
|
|
export const buildProjectContextString = (context: ProjectContext): string => {
|
|
const sections: string[] = [];
|
|
|
|
const header = context.name
|
|
? `Project: ${context.name}`
|
|
: `Project Type: ${context.projectType}`;
|
|
sections.push(header);
|
|
|
|
if (context.description) {
|
|
sections.push(`Description: ${context.description}`);
|
|
}
|
|
|
|
sections.push(`Language: ${context.mainLanguage}`);
|
|
|
|
if (context.frameworks.length > 0) {
|
|
sections.push(`Frameworks: ${context.frameworks.join(", ")}`);
|
|
}
|
|
|
|
if (context.keyFiles.length > 0) {
|
|
sections.push(`Key Files: ${context.keyFiles.join(", ")}`);
|
|
}
|
|
|
|
if (context.dependencies && context.dependencies.length > 0) {
|
|
sections.push(`Main Dependencies: ${context.dependencies.join(", ")}`);
|
|
}
|
|
|
|
if (context.structure) {
|
|
sections.push(`\nProject Structure:\n\`\`\`\n${context.structure}\n\`\`\``);
|
|
}
|
|
|
|
return sections.join("\n");
|
|
};
|
|
|
|
/**
|
|
* Get project context for ask mode prompts
|
|
*/
|
|
export const getProjectContextForAskMode = (workingDir: string): string => {
|
|
const context = gatherProjectContext(workingDir);
|
|
return buildProjectContextString(context);
|
|
};
|