172 lines
5.6 KiB
JavaScript
172 lines
5.6 KiB
JavaScript
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Repo Profiler
|
||
|
|
* Analyzes repository files and dependencies to determine its Archetype deterministically.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const ARCHETYPES = {
|
||
|
|
INFRASTRUCTURE: 'Infrastructure',
|
||
|
|
FRONTEND: 'Frontend SPA',
|
||
|
|
BACKEND: 'Backend API',
|
||
|
|
PIPELINE: 'Data Pipeline',
|
||
|
|
LIBRARY: 'Library',
|
||
|
|
MONOREPO: 'Monorepo',
|
||
|
|
UNKNOWN: 'Unknown'
|
||
|
|
};
|
||
|
|
|
||
|
|
function readJsonFile(filePath) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||
|
|
} catch (e) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function analyzePackageJson(dir) {
|
||
|
|
const pkg = readJsonFile(path.join(dir, 'package.json'));
|
||
|
|
if (!pkg) return null;
|
||
|
|
|
||
|
|
const signals = [];
|
||
|
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
||
|
|
const depsKeys = Object.keys(deps);
|
||
|
|
|
||
|
|
// Frontend
|
||
|
|
if (depsKeys.includes('react') || depsKeys.includes('vue') || depsKeys.includes('angular') || depsKeys.includes('next') || depsKeys.includes('vite') || depsKeys.includes('webpack')) {
|
||
|
|
signals.push('frontend_framework');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Backend
|
||
|
|
if (depsKeys.includes('express') || depsKeys.includes('fastify') || depsKeys.includes('nestjs') || depsKeys.includes('koa')) {
|
||
|
|
signals.push('backend_framework');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Library
|
||
|
|
if (!pkg.scripts?.start && (pkg.main || pkg.exports) && !depsKeys.includes('express') && !depsKeys.includes('react') && !depsKeys.includes('vue') && !depsKeys.includes('angular')) {
|
||
|
|
signals.push('library_exports');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Monorepo workspaces
|
||
|
|
if (pkg.workspaces) {
|
||
|
|
signals.push('workspaces');
|
||
|
|
}
|
||
|
|
|
||
|
|
return signals;
|
||
|
|
}
|
||
|
|
|
||
|
|
function analyzeFiles(dir, maxDepth = 3) {
|
||
|
|
const rootFiles = [];
|
||
|
|
try {
|
||
|
|
rootFiles.push(...fs.readdirSync(dir));
|
||
|
|
} catch (e) {
|
||
|
|
// directory doesn't exist
|
||
|
|
}
|
||
|
|
|
||
|
|
const signals = [];
|
||
|
|
|
||
|
|
// Check root level first
|
||
|
|
if (rootFiles.includes('Chart.yaml') || rootFiles.some(f => f.endsWith('.tf')) || rootFiles.includes('terraform')) {
|
||
|
|
signals.push('infra_files');
|
||
|
|
}
|
||
|
|
if (rootFiles.some(f => f.endsWith('.hcl') || f === 'helm' || f === 'kubernetes' || f === 'k8s')) {
|
||
|
|
signals.push('infra_files');
|
||
|
|
}
|
||
|
|
if (rootFiles.includes('go.mod')) signals.push('go_backend');
|
||
|
|
if (rootFiles.includes('Cargo.toml')) signals.push('rust_app');
|
||
|
|
if (rootFiles.includes('requirements.txt') || rootFiles.includes('Pipfile') || rootFiles.includes('pyproject.toml')) signals.push('python_app');
|
||
|
|
if (rootFiles.includes('lerna.json') || rootFiles.includes('turbo.json') || rootFiles.includes('nx.json')) signals.push('monorepo_tools');
|
||
|
|
|
||
|
|
// Recurse into subdirectories to find infra patterns (Helm charts, TF files)
|
||
|
|
if (!signals.includes('infra_files')) {
|
||
|
|
const infraFound = findInfraRecursive(dir, maxDepth, 0);
|
||
|
|
if (infraFound) signals.push('infra_files');
|
||
|
|
}
|
||
|
|
|
||
|
|
return signals;
|
||
|
|
}
|
||
|
|
|
||
|
|
function findInfraRecursive(dir, maxDepth, currentDepth) {
|
||
|
|
if (currentDepth >= maxDepth) return false;
|
||
|
|
try {
|
||
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||
|
|
for (const entry of entries) {
|
||
|
|
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.terraform') continue;
|
||
|
|
if (entry.isFile()) {
|
||
|
|
if (entry.name === 'Chart.yaml' || entry.name.endsWith('.tf') || entry.name === 'Dockerfile' || entry.name === 'crossplane.yaml') {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} else if (entry.isDirectory()) {
|
||
|
|
if (['charts', 'helm', 'terraform', 'modules', 'k8s', 'kubernetes'].includes(entry.name)) return true;
|
||
|
|
if (findInfraRecursive(path.join(dir, entry.name), maxDepth, currentDepth + 1)) return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) { /* skip unreadable dirs */ }
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function profileRepo(repoPath, graph = null) {
|
||
|
|
const signals = new Set();
|
||
|
|
|
||
|
|
const fileSignals = analyzeFiles(repoPath);
|
||
|
|
fileSignals.forEach(s => signals.add(s));
|
||
|
|
|
||
|
|
const pkgSignals = analyzePackageJson(repoPath);
|
||
|
|
if (pkgSignals) {
|
||
|
|
pkgSignals.forEach(s => signals.add(s));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (graph && graph.nodes) {
|
||
|
|
let hasRoutes = false;
|
||
|
|
let hasComponents = false;
|
||
|
|
let hasInfraNodes = false;
|
||
|
|
|
||
|
|
for (const [id, node] of Object.entries(graph.nodes)) {
|
||
|
|
if (node.type === 'route' || node.type === 'controller') hasRoutes = true;
|
||
|
|
if (node.type === 'component') hasComponents = true;
|
||
|
|
if (node.type === 'resource' || node.type === 'chart' || node.type === 'module') hasInfraNodes = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasRoutes) signals.add('graph_routes');
|
||
|
|
if (hasComponents) signals.add('graph_components');
|
||
|
|
if (hasInfraNodes) signals.add('graph_infra');
|
||
|
|
}
|
||
|
|
|
||
|
|
let archetype = ARCHETYPES.UNKNOWN;
|
||
|
|
let confidence = 0.0;
|
||
|
|
|
||
|
|
if (signals.has('workspaces') || signals.has('monorepo_tools')) {
|
||
|
|
archetype = ARCHETYPES.MONOREPO;
|
||
|
|
confidence = 0.9;
|
||
|
|
} else if (signals.has('infra_files') || signals.has('graph_infra')) {
|
||
|
|
archetype = ARCHETYPES.INFRASTRUCTURE;
|
||
|
|
confidence = 0.9;
|
||
|
|
} else if (signals.has('frontend_framework') || signals.has('graph_components')) {
|
||
|
|
archetype = ARCHETYPES.FRONTEND;
|
||
|
|
confidence = 0.85;
|
||
|
|
} else if (signals.has('backend_framework') || signals.has('graph_routes') || signals.has('go_backend')) {
|
||
|
|
archetype = ARCHETYPES.BACKEND;
|
||
|
|
confidence = 0.85;
|
||
|
|
} else if (signals.has('library_exports')) {
|
||
|
|
archetype = ARCHETYPES.LIBRARY;
|
||
|
|
confidence = 0.7;
|
||
|
|
} else if (signals.has('python_app') || signals.has('rust_app')) {
|
||
|
|
archetype = ARCHETYPES.BACKEND;
|
||
|
|
confidence = 0.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
archetype,
|
||
|
|
confidence,
|
||
|
|
signals: Array.from(signals)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { profileRepo, ARCHETYPES };
|
||
|
|
|
||
|
|
if (require.main === module) {
|
||
|
|
const targetDir = process.argv[2] || process.cwd();
|
||
|
|
const profile = profileRepo(targetDir);
|
||
|
|
console.log(JSON.stringify(profile, null, 2));
|
||
|
|
}
|