autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
184 lines (183 loc) • 6 kB
JavaScript
/**
* @module GenericDiscoverer
* @description 通用兜底项目结构发现器
*
* 始终匹配,confidence 0.1。
* 按语言统计最多的扩展名确定主语言。
* 按顶层目录分 Target。
*/
import { existsSync, readdirSync } from 'node:fs';
import { basename, extname, join, relative } from 'node:path';
import { LanguageService } from '../../shared/LanguageService.js';
import { ProjectDiscoverer, } from './ProjectDiscoverer.js';
const EXCLUDE_DIRS = new Set([
'node_modules',
'.git',
'.cursor',
'dist',
'build',
'out',
'.build',
'target',
'Pods',
'Carthage',
'DerivedData',
'__pycache__',
'.venv',
'venv',
'.gradle',
'.idea',
'vendor',
'coverage',
'.cache',
]);
const SOURCE_EXTENSIONS = LanguageService.sourceExts;
export class GenericDiscoverer extends ProjectDiscoverer {
#projectRoot = null;
#targets = [];
#primaryLang = 'unknown';
get id() {
return 'generic';
}
get displayName() {
return 'Generic (directory scan)';
}
async detect(projectRoot) {
// 始终匹配
return { match: true, confidence: 0.1, reason: 'Generic fallback discoverer' };
}
async load(projectRoot) {
this.#projectRoot = projectRoot;
this.#targets = [];
// 统计语言分布
const langStats = {};
this.#scanLangStats(projectRoot, langStats, 0);
// 找到主语言
let maxCount = 0;
for (const [lang, count] of Object.entries(langStats)) {
if (count > maxCount) {
maxCount = count;
this.#primaryLang = lang;
}
}
// 按顶层约定目录分 Target
const targetDirs = ['src', 'lib', 'app', 'pkg', 'cmd', 'internal', 'test', 'tests'];
let foundTargets = false;
try {
const entries = readdirSync(projectRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
if (entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) {
continue;
}
if (targetDirs.includes(entry.name.toLowerCase())) {
const isTest = /^tests?$/.test(entry.name);
this.#targets.push({
name: entry.name,
path: join(projectRoot, entry.name),
type: isTest ? 'test' : 'library',
language: this.#primaryLang,
});
foundTargets = true;
}
}
}
catch {
/* skip */
}
// 没有约定目录则整个项目为一个 Target
if (!foundTargets) {
this.#targets.push({
name: basename(projectRoot),
path: projectRoot,
type: 'library',
language: this.#primaryLang,
});
}
}
async listTargets() {
return this.#targets;
}
async getTargetFiles(target) {
const targetPath = typeof target === 'string'
? this.#targets.find((t) => t.name === target)?.path || this.#projectRoot
: target.path;
if (!targetPath || !existsSync(targetPath)) {
return [];
}
const files = [];
this.#collectFiles(targetPath, targetPath, files);
return files;
}
async getDependencyGraph() {
// GenericDiscoverer 无法推断依赖图
return { nodes: this.#targets.map((t) => t.name), edges: [] };
}
// ── 内部实现 ──
#scanLangStats(dir, stats, depth) {
if (depth > 5) {
return; // 限制深度, 只采样
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
if (entry.isDirectory()) {
this.#scanLangStats(join(dir, entry.name), stats, depth + 1);
}
else if (entry.isFile()) {
const ext = extname(entry.name);
if (SOURCE_EXTENSIONS.has(ext)) {
const lang = LanguageService.inferLang(entry.name) || 'unknown';
stats[lang] = (stats[lang] || 0) + 1;
}
}
}
}
catch {
/* skip */
}
}
#collectFiles(dir, rootDir, files, depth = 0) {
if (depth > 15) {
return;
}
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
if (EXCLUDE_DIRS.has(entry.name)) {
continue;
}
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
this.#collectFiles(fullPath, rootDir, files, depth + 1);
}
else if (entry.isFile()) {
const ext = extname(entry.name);
if (SOURCE_EXTENSIONS.has(ext)) {
const lang = LanguageService.inferLang(entry.name) || 'unknown';
files.push({
name: entry.name,
path: fullPath,
relativePath: relative(rootDir, fullPath),
language: lang,
});
}
}
}
}
catch {
/* skip */
}
}
}