project-detector.js•11.6 kB
/**
* Project Context Detection Utility
* Analyzes the current directory to determine project type, language, and context
*/
const fs = require('fs').promises;
const path = require('path');
const { execSync } = require('child_process');
/**
* Detect programming language from file extensions
*/
async function detectLanguage(directory) {
try {
const files = await fs.readdir(directory, { withFileTypes: true });
const extensions = new Map();
// Count file extensions
for (const file of files) {
if (file.isFile()) {
const ext = path.extname(file.name).toLowerCase();
if (ext) {
extensions.set(ext, (extensions.get(ext) || 0) + 1);
}
}
}
// Language detection rules
const languageMap = {
'.js': 'JavaScript',
'.ts': 'TypeScript',
'.jsx': 'React/JavaScript',
'.tsx': 'React/TypeScript',
'.py': 'Python',
'.rs': 'Rust',
'.go': 'Go',
'.java': 'Java',
'.cpp': 'C++',
'.c': 'C',
'.cs': 'C#',
'.php': 'PHP',
'.rb': 'Ruby',
'.swift': 'Swift',
'.kt': 'Kotlin',
'.scala': 'Scala',
'.sh': 'Shell',
'.md': 'Documentation'
};
// Find most common language extension
let primaryLanguage = 'Unknown';
let maxCount = 0;
for (const [ext, count] of extensions.entries()) {
if (languageMap[ext] && count > maxCount) {
maxCount = count;
primaryLanguage = languageMap[ext];
}
}
return {
primary: primaryLanguage,
extensions: Object.fromEntries(extensions),
confidence: maxCount > 0 ? Math.min(maxCount / 10, 1) : 0
};
} catch (error) {
return { primary: 'Unknown', extensions: {}, confidence: 0 };
}
}
/**
* Detect framework and tools from configuration files
*/
async function detectFramework(directory) {
const frameworks = [];
const tools = [];
try {
const files = await fs.readdir(directory);
// Check for common configuration files
const configFiles = {
'package.json': async () => {
const pkg = JSON.parse(await fs.readFile(path.join(directory, 'package.json'), 'utf8'));
// Check dependencies for frameworks
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
if (deps.react || deps['@types/react']) frameworks.push('React');
if (deps.vue || deps['@vue/cli']) frameworks.push('Vue.js');
if (deps.angular || deps['@angular/core']) frameworks.push('Angular');
if (deps.next || deps['next']) frameworks.push('Next.js');
if (deps.express || deps['express']) frameworks.push('Express.js');
if (deps.fastify) frameworks.push('Fastify');
if (deps.svelte || deps['svelte']) frameworks.push('Svelte');
tools.push('npm');
return pkg.name || 'node-project';
},
'pyproject.toml': async () => {
tools.push('Python');
const content = await fs.readFile(path.join(directory, 'pyproject.toml'), 'utf8');
// Extract project name from pyproject.toml
const nameMatch = content.match(/^name\s*=\s*["']([^"']+)["']/m);
if (content.includes('django')) frameworks.push('Django');
if (content.includes('flask')) frameworks.push('Flask');
if (content.includes('fastapi')) frameworks.push('FastAPI');
if (content.includes('pytest')) tools.push('pytest');
if (content.includes('poetry')) tools.push('Poetry');
return nameMatch ? nameMatch[1] : 'python-project';
},
'Cargo.toml': async () => {
tools.push('Cargo');
const content = await fs.readFile(path.join(directory, 'Cargo.toml'), 'utf8');
const nameMatch = content.match(/^name\s*=\s*["']([^"']+)["']/m);
if (content.includes('actix-web')) frameworks.push('Actix Web');
if (content.includes('rocket')) frameworks.push('Rocket');
if (content.includes('warp')) frameworks.push('Warp');
if (content.includes('tokio')) frameworks.push('Tokio');
return nameMatch ? nameMatch[1] : 'rust-project';
},
'go.mod': async () => {
tools.push('Go Modules');
const content = await fs.readFile(path.join(directory, 'go.mod'), 'utf8');
const moduleMatch = content.match(/^module\s+(.+)$/m);
if (content.includes('gin-gonic/gin')) frameworks.push('Gin');
if (content.includes('gorilla/mux')) frameworks.push('Gorilla Mux');
if (content.includes('fiber')) frameworks.push('Fiber');
return moduleMatch ? path.basename(moduleMatch[1]) : 'go-project';
},
'pom.xml': () => {
tools.push('Maven');
frameworks.push('Java/Maven');
return 'java-maven-project';
},
'build.gradle': () => {
tools.push('Gradle');
frameworks.push('Java/Gradle');
return 'java-gradle-project';
},
'docker-compose.yml': () => {
tools.push('Docker Compose');
return null;
},
'Dockerfile': () => {
tools.push('Docker');
return null;
},
'.env': () => {
tools.push('Environment Config');
return null;
}
};
let projectName = null;
for (const file of files) {
if (configFiles[file]) {
const result = await configFiles[file]();
if (result && !projectName) {
projectName = result;
}
}
}
return {
frameworks,
tools,
projectName
};
} catch (error) {
return { frameworks: [], tools: [], projectName: null };
}
}
/**
* Get Git repository information
*/
function getGitInfo(directory) {
try {
const gitDir = path.join(directory, '.git');
// Check if this is a git repository
const isGitRepo = require('fs').existsSync(gitDir);
if (!isGitRepo) {
return { isRepo: false };
}
// Get repository information
const remoteBranch = execSync('git branch --show-current', { cwd: path.resolve(directory), encoding: 'utf8' }).trim();
const remoteUrl = execSync('git config --get remote.origin.url', { cwd: path.resolve(directory), encoding: 'utf8' }).trim();
const lastCommit = execSync('git log -1 --pretty=format:"%h %s"', { cwd: path.resolve(directory), encoding: 'utf8' }).trim();
// Extract repository name from URL
let repoName = 'unknown-repo';
if (remoteUrl) {
const match = remoteUrl.match(/([^\/]+)(?:\.git)?$/);
if (match) {
repoName = match[1].replace('.git', '');
}
}
return {
isRepo: true,
branch: remoteBranch,
remoteUrl,
repoName,
lastCommit
};
} catch (error) {
return { isRepo: false, error: error.message };
}
}
// ANSI Colors for console output
const COLORS = {
RESET: '\x1b[0m',
BRIGHT: '\x1b[1m',
DIM: '\x1b[2m',
CYAN: '\x1b[36m',
GREEN: '\x1b[32m',
BLUE: '\x1b[34m',
YELLOW: '\x1b[33m',
GRAY: '\x1b[90m',
RED: '\x1b[31m'
};
/**
* Main project context detection function with enhanced visual output
*/
async function detectProjectContext(directory = process.cwd()) {
try {
const directoryName = path.basename(directory);
console.log(`${COLORS.BLUE}📂 Project Detector${COLORS.RESET} ${COLORS.DIM}→${COLORS.RESET} Analyzing ${COLORS.BRIGHT}${directoryName}${COLORS.RESET}`);
// Get basic directory information
// Detect language
const language = await detectLanguage(directory);
// Detect framework and tools
const framework = await detectFramework(directory);
// Get Git information
const git = getGitInfo(directory);
// Determine project name (priority: git repo > config file > directory name)
const projectName = framework.projectName || git.repoName || directoryName;
// Calculate confidence score
let confidence = 0.5; // Base confidence
if (git.isRepo) confidence += 0.3;
if (framework.frameworks.length > 0) confidence += 0.2;
if (language.confidence > 0.5) confidence += language.confidence * 0.3;
const context = {
name: projectName,
directory,
language: language.primary,
languageDetails: language,
frameworks: framework.frameworks,
tools: framework.tools,
git: git,
confidence: Math.min(confidence, 1.0),
metadata: {
detectedAt: new Date().toISOString(),
analyzer: 'claude-hooks-project-detector',
version: '1.1.0'
}
};
// Enhanced output with confidence indication
const confidencePercent = (context.confidence * 100).toFixed(0);
const confidenceColor = context.confidence > 0.8 ? COLORS.GREEN :
context.confidence > 0.6 ? COLORS.YELLOW : COLORS.GRAY;
console.log(`${COLORS.BLUE}📊 Detection Result${COLORS.RESET} ${COLORS.DIM}→${COLORS.RESET} ${COLORS.BRIGHT}${context.name}${COLORS.RESET} ${COLORS.GRAY}(${context.language})${COLORS.RESET} ${COLORS.DIM}•${COLORS.RESET} ${confidenceColor}${confidencePercent}%${COLORS.RESET}`);
return context;
} catch (error) {
console.error(`${COLORS.RED}❌ Project Detector Error${COLORS.RESET} ${COLORS.DIM}→${COLORS.RESET} ${error.message}`);
// Return minimal context on error
return {
name: path.basename(directory),
directory,
language: 'Unknown',
frameworks: [],
tools: [],
confidence: 0.1,
error: error.message
};
}
}
module.exports = {
detectProjectContext,
detectLanguage,
detectFramework,
getGitInfo
};
// Direct execution support for testing
if (require.main === module) {
detectProjectContext(process.cwd())
.then(context => {
console.log('\n=== PROJECT CONTEXT ===');
console.log(JSON.stringify(context, null, 2));
console.log('=== END CONTEXT ===\n');
})
.catch(error => console.error('Detection failed:', error));
}