UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

296 lines 11.2 kB
import { readFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join, basename } from 'path'; /** * Detects project type and structure by analyzing files in a directory. */ export class ProjectDetector { rootDir; constructor(rootDir) { this.rootDir = rootDir; } /** * Analyze the project and return detection results */ detect() { const evidence = []; let type = 'unknown'; let structure = 'standard'; let confidence = 0; let packageManager; let testFramework; let ciSystem; let projectName; let projectDescription; // Check for package.json (JavaScript/TypeScript) const packageJsonPath = join(this.rootDir, 'package.json'); if (existsSync(packageJsonPath)) { evidence.push('package.json'); try { const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')); projectName = pkg.name; projectDescription = pkg.description; // Detect package manager if (existsSync(join(this.rootDir, 'pnpm-lock.yaml'))) { packageManager = 'pnpm'; evidence.push('pnpm-lock.yaml'); } else if (existsSync(join(this.rootDir, 'yarn.lock'))) { packageManager = 'yarn'; evidence.push('yarn.lock'); } else if (existsSync(join(this.rootDir, 'bun.lockb'))) { packageManager = 'bun'; evidence.push('bun.lockb'); } else if (existsSync(join(this.rootDir, 'package-lock.json'))) { packageManager = 'npm'; evidence.push('package-lock.json'); } // Check for TypeScript if (existsSync(join(this.rootDir, 'tsconfig.json')) || pkg.devDependencies?.['typescript'] || pkg.dependencies?.['typescript']) { type = 'typescript'; confidence = 0.9; if (existsSync(join(this.rootDir, 'tsconfig.json'))) { evidence.push('tsconfig.json'); } } else { type = 'javascript'; confidence = 0.8; } // Detect test framework if (pkg.devDependencies?.['vitest'] || pkg.dependencies?.['vitest']) { testFramework = 'vitest'; } else if (pkg.devDependencies?.['jest'] || pkg.dependencies?.['jest']) { testFramework = 'jest'; } else if (pkg.devDependencies?.['mocha'] || pkg.dependencies?.['mocha']) { testFramework = 'mocha'; } // Detect structure if (pkg.workspaces) { structure = 'monorepo'; evidence.push('package.json workspaces'); } else if (pkg.bin) { structure = 'cli'; } else if (pkg.main || pkg.module || pkg.exports) { structure = 'library'; } } catch { // Invalid JSON, still TypeScript/JavaScript project type = 'javascript'; confidence = 0.5; } } // Check for pyproject.toml or setup.py (Python) if (existsSync(join(this.rootDir, 'pyproject.toml'))) { evidence.push('pyproject.toml'); type = 'python'; confidence = 0.9; packageManager = 'pip'; try { const content = readFileSync(join(this.rootDir, 'pyproject.toml'), 'utf8'); if (content.includes('[tool.poetry]')) { packageManager = 'poetry'; evidence.push('poetry config'); } else if (content.includes('[tool.pdm]')) { packageManager = 'pdm'; } else if (content.includes('[tool.uv]')) { packageManager = 'uv'; } // Extract name const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/); if (nameMatch) { projectName = nameMatch[1]; } // Detect test framework if (content.includes('pytest')) { testFramework = 'pytest'; } } catch { // Continue with default Python detection } } else if (existsSync(join(this.rootDir, 'setup.py'))) { evidence.push('setup.py'); type = 'python'; confidence = 0.8; packageManager = 'pip'; } else if (existsSync(join(this.rootDir, 'requirements.txt'))) { evidence.push('requirements.txt'); type = 'python'; confidence = 0.7; packageManager = 'pip'; } // Check for go.mod (Go) if (existsSync(join(this.rootDir, 'go.mod'))) { evidence.push('go.mod'); type = 'go'; confidence = 0.95; packageManager = 'go mod'; try { const content = readFileSync(join(this.rootDir, 'go.mod'), 'utf8'); const moduleMatch = content.match(/module\s+(\S+)/); if (moduleMatch?.[1]) { projectName = basename(moduleMatch[1]); } } catch { // Continue } // Go test is built-in testFramework = 'go test'; } // Check for Cargo.toml (Rust) if (existsSync(join(this.rootDir, 'Cargo.toml'))) { evidence.push('Cargo.toml'); type = 'rust'; confidence = 0.95; packageManager = 'cargo'; testFramework = 'cargo test'; try { const content = readFileSync(join(this.rootDir, 'Cargo.toml'), 'utf8'); const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/); if (nameMatch) { projectName = nameMatch[1]; } } catch { // Continue } } // Check for pom.xml or build.gradle (Java) if (existsSync(join(this.rootDir, 'pom.xml'))) { evidence.push('pom.xml'); type = 'java'; confidence = 0.9; packageManager = 'maven'; testFramework = 'junit'; } else if (existsSync(join(this.rootDir, 'build.gradle')) || existsSync(join(this.rootDir, 'build.gradle.kts'))) { evidence.push('build.gradle'); type = 'java'; confidence = 0.9; packageManager = 'gradle'; testFramework = 'junit'; } // Check for monorepo patterns if (structure !== 'monorepo') { if (existsSync(join(this.rootDir, 'lerna.json'))) { structure = 'monorepo'; evidence.push('lerna.json'); } else if (existsSync(join(this.rootDir, 'nx.json'))) { structure = 'monorepo'; evidence.push('nx.json'); } else if (existsSync(join(this.rootDir, 'turbo.json'))) { structure = 'monorepo'; evidence.push('turbo.json'); } else if (existsSync(join(this.rootDir, 'pnpm-workspace.yaml'))) { structure = 'monorepo'; evidence.push('pnpm-workspace.yaml'); } else if (this.hasMultiplePackageJsons()) { structure = 'monorepo'; evidence.push('multiple package.json files'); } } // Detect CI system if (existsSync(join(this.rootDir, '.github', 'workflows'))) { ciSystem = 'github-actions'; evidence.push('.github/workflows'); } else if (existsSync(join(this.rootDir, '.gitlab-ci.yml'))) { ciSystem = 'gitlab-ci'; evidence.push('.gitlab-ci.yml'); } else if (existsSync(join(this.rootDir, 'bitbucket-pipelines.yml'))) { ciSystem = 'bitbucket-pipelines'; evidence.push('bitbucket-pipelines.yml'); } else if (existsSync(join(this.rootDir, '.circleci'))) { ciSystem = 'circleci'; evidence.push('.circleci'); } // Detect web project if (structure === 'standard') { if (existsSync(join(this.rootDir, 'next.config.js')) || existsSync(join(this.rootDir, 'next.config.mjs')) || existsSync(join(this.rootDir, 'next.config.ts'))) { structure = 'web'; evidence.push('Next.js config'); } else if (existsSync(join(this.rootDir, 'vite.config.ts')) || existsSync(join(this.rootDir, 'vite.config.js'))) { structure = 'web'; evidence.push('Vite config'); } else if (existsSync(join(this.rootDir, 'angular.json'))) { structure = 'web'; evidence.push('angular.json'); } else if (existsSync(join(this.rootDir, 'vue.config.js'))) { structure = 'web'; evidence.push('vue.config.js'); } } // Detect API project if (structure === 'standard' && type !== 'unknown') { const hasOpenAPI = existsSync(join(this.rootDir, 'openapi.yaml')) || existsSync(join(this.rootDir, 'openapi.json')) || existsSync(join(this.rootDir, 'swagger.yaml')) || existsSync(join(this.rootDir, 'swagger.json')); if (hasOpenAPI) { structure = 'api'; evidence.push('OpenAPI/Swagger spec'); } } return { type, structure, confidence, evidence, packageManager, testFramework, ciSystem, projectName, projectDescription, }; } hasMultiplePackageJsons() { try { const dirs = readdirSync(this.rootDir); let packageCount = 0; for (const dir of dirs) { const fullPath = join(this.rootDir, dir); if (statSync(fullPath).isDirectory()) { if (existsSync(join(fullPath, 'package.json'))) { packageCount++; if (packageCount >= 2) return true; } } } return false; } catch { return false; } } } //# sourceMappingURL=ProjectDetector.js.map