mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
feat: automatic project type and framework detection (#293)
Add SessionStart hook integration that auto-detects project languages and frameworks by inspecting marker files and dependency manifests. Supports 12 languages (Python, TypeScript, Go, Rust, Ruby, Java, C#, Swift, Kotlin, Elixir, PHP, JavaScript) and 25+ frameworks (Next.js, React, Django, FastAPI, Rails, Laravel, Spring, etc.). Detection output is injected into Claude's context as JSON, enabling context-aware recommendations without loading irrelevant rules. - New: scripts/lib/project-detect.js (cross-platform detection library) - Modified: scripts/hooks/session-start.js (integration) - New: tests/lib/project-detect.test.js (28 tests, all passing) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ const {
|
||||
} = require('../lib/utils');
|
||||
const { getPackageManager, getSelectionPrompt } = require('../lib/package-manager');
|
||||
const { listAliases } = require('../lib/session-aliases');
|
||||
const { detectProjectType } = require('../lib/project-detect');
|
||||
|
||||
async function main() {
|
||||
const sessionsDir = getSessionsDir();
|
||||
@@ -71,6 +72,22 @@ async function main() {
|
||||
log(getSelectionPrompt());
|
||||
}
|
||||
|
||||
// Detect project type and frameworks (#293)
|
||||
const projectInfo = detectProjectType();
|
||||
if (projectInfo.languages.length > 0 || projectInfo.frameworks.length > 0) {
|
||||
const parts = [];
|
||||
if (projectInfo.languages.length > 0) {
|
||||
parts.push(`languages: ${projectInfo.languages.join(', ')}`);
|
||||
}
|
||||
if (projectInfo.frameworks.length > 0) {
|
||||
parts.push(`frameworks: ${projectInfo.frameworks.join(', ')}`);
|
||||
}
|
||||
log(`[SessionStart] Project detected — ${parts.join('; ')}`);
|
||||
output(`Project type: ${JSON.stringify(projectInfo)}`);
|
||||
} else {
|
||||
log('[SessionStart] No specific project type detected');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
413
scripts/lib/project-detect.js
Normal file
413
scripts/lib/project-detect.js
Normal file
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Project type and framework detection
|
||||
*
|
||||
* Cross-platform (Windows, macOS, Linux) project type detection
|
||||
* by inspecting files in the working directory.
|
||||
*
|
||||
* Resolves: https://github.com/affaan-m/everything-claude-code/issues/293
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Language detection rules.
|
||||
* Each rule checks for marker files or glob patterns in the project root.
|
||||
*/
|
||||
const LANGUAGE_RULES = [
|
||||
{
|
||||
type: 'python',
|
||||
markers: ['requirements.txt', 'pyproject.toml', 'setup.py', 'setup.cfg', 'Pipfile', 'poetry.lock'],
|
||||
extensions: ['.py']
|
||||
},
|
||||
{
|
||||
type: 'typescript',
|
||||
markers: ['tsconfig.json', 'tsconfig.build.json'],
|
||||
extensions: ['.ts', '.tsx']
|
||||
},
|
||||
{
|
||||
type: 'javascript',
|
||||
markers: ['package.json', 'jsconfig.json'],
|
||||
extensions: ['.js', '.jsx', '.mjs']
|
||||
},
|
||||
{
|
||||
type: 'golang',
|
||||
markers: ['go.mod', 'go.sum'],
|
||||
extensions: ['.go']
|
||||
},
|
||||
{
|
||||
type: 'rust',
|
||||
markers: ['Cargo.toml', 'Cargo.lock'],
|
||||
extensions: ['.rs']
|
||||
},
|
||||
{
|
||||
type: 'ruby',
|
||||
markers: ['Gemfile', 'Gemfile.lock', 'Rakefile'],
|
||||
extensions: ['.rb']
|
||||
},
|
||||
{
|
||||
type: 'java',
|
||||
markers: ['pom.xml', 'build.gradle', 'build.gradle.kts'],
|
||||
extensions: ['.java']
|
||||
},
|
||||
{
|
||||
type: 'csharp',
|
||||
markers: [],
|
||||
extensions: ['.cs', '.csproj', '.sln']
|
||||
},
|
||||
{
|
||||
type: 'swift',
|
||||
markers: ['Package.swift'],
|
||||
extensions: ['.swift']
|
||||
},
|
||||
{
|
||||
type: 'kotlin',
|
||||
markers: [],
|
||||
extensions: ['.kt', '.kts']
|
||||
},
|
||||
{
|
||||
type: 'elixir',
|
||||
markers: ['mix.exs'],
|
||||
extensions: ['.ex', '.exs']
|
||||
},
|
||||
{
|
||||
type: 'php',
|
||||
markers: ['composer.json', 'composer.lock'],
|
||||
extensions: ['.php']
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Framework detection rules.
|
||||
* Checked after language detection for more specific identification.
|
||||
*/
|
||||
const FRAMEWORK_RULES = [
|
||||
// Python frameworks
|
||||
{ framework: 'django', language: 'python', markers: ['manage.py'], packageKeys: ['django'] },
|
||||
{ framework: 'fastapi', language: 'python', markers: [], packageKeys: ['fastapi'] },
|
||||
{ framework: 'flask', language: 'python', markers: [], packageKeys: ['flask'] },
|
||||
|
||||
// JavaScript/TypeScript frameworks
|
||||
{ framework: 'nextjs', language: 'typescript', markers: ['next.config.js', 'next.config.mjs', 'next.config.ts'], packageKeys: ['next'] },
|
||||
{ framework: 'react', language: 'typescript', markers: [], packageKeys: ['react'] },
|
||||
{ framework: 'vue', language: 'typescript', markers: ['vue.config.js'], packageKeys: ['vue'] },
|
||||
{ framework: 'angular', language: 'typescript', markers: ['angular.json'], packageKeys: ['@angular/core'] },
|
||||
{ framework: 'svelte', language: 'typescript', markers: ['svelte.config.js'], packageKeys: ['svelte'] },
|
||||
{ framework: 'express', language: 'javascript', markers: [], packageKeys: ['express'] },
|
||||
{ framework: 'nestjs', language: 'typescript', markers: ['nest-cli.json'], packageKeys: ['@nestjs/core'] },
|
||||
{ framework: 'remix', language: 'typescript', markers: [], packageKeys: ['@remix-run/node', '@remix-run/react'] },
|
||||
{ framework: 'astro', language: 'typescript', markers: ['astro.config.mjs', 'astro.config.ts'], packageKeys: ['astro'] },
|
||||
{ framework: 'nuxt', language: 'typescript', markers: ['nuxt.config.js', 'nuxt.config.ts'], packageKeys: ['nuxt'] },
|
||||
{ framework: 'electron', language: 'typescript', markers: [], packageKeys: ['electron'] },
|
||||
|
||||
// Ruby frameworks
|
||||
{ framework: 'rails', language: 'ruby', markers: ['config/routes.rb', 'bin/rails'], packageKeys: [] },
|
||||
|
||||
// Go frameworks
|
||||
{ framework: 'gin', language: 'golang', markers: [], packageKeys: ['github.com/gin-gonic/gin'] },
|
||||
{ framework: 'echo', language: 'golang', markers: [], packageKeys: ['github.com/labstack/echo'] },
|
||||
|
||||
// Rust frameworks
|
||||
{ framework: 'actix', language: 'rust', markers: [], packageKeys: ['actix-web'] },
|
||||
{ framework: 'axum', language: 'rust', markers: [], packageKeys: ['axum'] },
|
||||
|
||||
// Java frameworks
|
||||
{ framework: 'spring', language: 'java', markers: [], packageKeys: ['spring-boot', 'org.springframework'] },
|
||||
|
||||
// PHP frameworks
|
||||
{ framework: 'laravel', language: 'php', markers: ['artisan'], packageKeys: ['laravel/framework'] },
|
||||
{ framework: 'symfony', language: 'php', markers: ['symfony.lock'], packageKeys: ['symfony/framework-bundle'] },
|
||||
|
||||
// Elixir frameworks
|
||||
{ framework: 'phoenix', language: 'elixir', markers: [], packageKeys: ['phoenix'] }
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a file exists relative to the project directory
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @param {string} filePath - Relative file path
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function fileExists(projectDir, filePath) {
|
||||
try {
|
||||
return fs.existsSync(path.join(projectDir, filePath));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any file with given extension exists in the project root (non-recursive, top-level only)
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @param {string[]} extensions - File extensions to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasFileWithExtension(projectDir, extensions) {
|
||||
try {
|
||||
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
||||
return entries.some(entry => {
|
||||
if (!entry.isFile()) return false;
|
||||
const ext = path.extname(entry.name);
|
||||
return extensions.includes(ext);
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse package.json dependencies
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of dependency names
|
||||
*/
|
||||
function getPackageJsonDeps(projectDir) {
|
||||
try {
|
||||
const pkgPath = path.join(projectDir, 'package.json');
|
||||
if (!fs.existsSync(pkgPath)) return [];
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
return [
|
||||
...Object.keys(pkg.dependencies || {}),
|
||||
...Object.keys(pkg.devDependencies || {})
|
||||
];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read requirements.txt or pyproject.toml for Python package names
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of dependency names (lowercase)
|
||||
*/
|
||||
function getPythonDeps(projectDir) {
|
||||
const deps = [];
|
||||
|
||||
// requirements.txt
|
||||
try {
|
||||
const reqPath = path.join(projectDir, 'requirements.txt');
|
||||
if (fs.existsSync(reqPath)) {
|
||||
const content = fs.readFileSync(reqPath, 'utf8');
|
||||
content.split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
|
||||
const name = trimmed.split(/[>=<!\[;]/)[0].trim().toLowerCase();
|
||||
if (name) deps.push(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// pyproject.toml — simple extraction of dependency names
|
||||
try {
|
||||
const tomlPath = path.join(projectDir, 'pyproject.toml');
|
||||
if (fs.existsSync(tomlPath)) {
|
||||
const content = fs.readFileSync(tomlPath, 'utf8');
|
||||
const depMatches = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
|
||||
if (depMatches) {
|
||||
const block = depMatches[1];
|
||||
block.match(/"([^"]+)"/g)?.forEach(m => {
|
||||
const name = m.replace(/"/g, '').split(/[>=<!\[;]/)[0].trim().toLowerCase();
|
||||
if (name) deps.push(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
return deps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read go.mod for Go module dependencies
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of module paths
|
||||
*/
|
||||
function getGoDeps(projectDir) {
|
||||
try {
|
||||
const modPath = path.join(projectDir, 'go.mod');
|
||||
if (!fs.existsSync(modPath)) return [];
|
||||
const content = fs.readFileSync(modPath, 'utf8');
|
||||
const deps = [];
|
||||
const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
|
||||
if (requireBlock) {
|
||||
requireBlock[1].split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('//')) {
|
||||
const parts = trimmed.split(/\s+/);
|
||||
if (parts[0]) deps.push(parts[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return deps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Cargo.toml for Rust crate dependencies
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of crate names
|
||||
*/
|
||||
function getRustDeps(projectDir) {
|
||||
try {
|
||||
const cargoPath = path.join(projectDir, 'Cargo.toml');
|
||||
if (!fs.existsSync(cargoPath)) return [];
|
||||
const content = fs.readFileSync(cargoPath, 'utf8');
|
||||
const deps = [];
|
||||
// Match [dependencies] and [dev-dependencies] sections
|
||||
const sections = content.match(/\[(dev-)?dependencies\]([\s\S]*?)(?=\n\[|$)/g);
|
||||
if (sections) {
|
||||
sections.forEach(section => {
|
||||
section.split('\n').forEach(line => {
|
||||
const match = line.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
||||
if (match && !line.startsWith('[')) {
|
||||
deps.push(match[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return deps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read composer.json for PHP package dependencies
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of package names
|
||||
*/
|
||||
function getComposerDeps(projectDir) {
|
||||
try {
|
||||
const composerPath = path.join(projectDir, 'composer.json');
|
||||
if (!fs.existsSync(composerPath)) return [];
|
||||
const composer = JSON.parse(fs.readFileSync(composerPath, 'utf8'));
|
||||
return [
|
||||
...Object.keys(composer.require || {}),
|
||||
...Object.keys(composer['require-dev'] || {})
|
||||
];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read mix.exs for Elixir dependencies (simple pattern match)
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string[]} Array of dependency atom names
|
||||
*/
|
||||
function getElixirDeps(projectDir) {
|
||||
try {
|
||||
const mixPath = path.join(projectDir, 'mix.exs');
|
||||
if (!fs.existsSync(mixPath)) return [];
|
||||
const content = fs.readFileSync(mixPath, 'utf8');
|
||||
const deps = [];
|
||||
const matches = content.match(/\{:(\w+)/g);
|
||||
if (matches) {
|
||||
matches.forEach(m => deps.push(m.replace('{:', '')));
|
||||
}
|
||||
return deps;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect project languages and frameworks
|
||||
* @param {string} [projectDir] - Project directory (defaults to cwd)
|
||||
* @returns {{ languages: string[], frameworks: string[], primary: string, projectDir: string }}
|
||||
*/
|
||||
function detectProjectType(projectDir) {
|
||||
projectDir = projectDir || process.cwd();
|
||||
const languages = [];
|
||||
const frameworks = [];
|
||||
|
||||
// Step 1: Detect languages
|
||||
for (const rule of LANGUAGE_RULES) {
|
||||
const hasMarker = rule.markers.some(m => fileExists(projectDir, m));
|
||||
const hasExt = rule.extensions.length > 0 && hasFileWithExtension(projectDir, rule.extensions);
|
||||
|
||||
if (hasMarker || hasExt) {
|
||||
languages.push(rule.type);
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate: if both typescript and javascript detected, keep typescript
|
||||
if (languages.includes('typescript') && languages.includes('javascript')) {
|
||||
const idx = languages.indexOf('javascript');
|
||||
if (idx !== -1) languages.splice(idx, 1);
|
||||
}
|
||||
|
||||
// Step 2: Detect frameworks based on markers and dependencies
|
||||
const npmDeps = getPackageJsonDeps(projectDir);
|
||||
const pyDeps = getPythonDeps(projectDir);
|
||||
const goDeps = getGoDeps(projectDir);
|
||||
const rustDeps = getRustDeps(projectDir);
|
||||
const composerDeps = getComposerDeps(projectDir);
|
||||
const elixirDeps = getElixirDeps(projectDir);
|
||||
|
||||
for (const rule of FRAMEWORK_RULES) {
|
||||
// Check marker files
|
||||
const hasMarker = rule.markers.some(m => fileExists(projectDir, m));
|
||||
|
||||
// Check package dependencies
|
||||
let hasDep = false;
|
||||
if (rule.packageKeys.length > 0) {
|
||||
let depList = [];
|
||||
switch (rule.language) {
|
||||
case 'python': depList = pyDeps; break;
|
||||
case 'typescript':
|
||||
case 'javascript': depList = npmDeps; break;
|
||||
case 'golang': depList = goDeps; break;
|
||||
case 'rust': depList = rustDeps; break;
|
||||
case 'php': depList = composerDeps; break;
|
||||
case 'elixir': depList = elixirDeps; break;
|
||||
}
|
||||
hasDep = rule.packageKeys.some(key =>
|
||||
depList.some(dep => dep.toLowerCase().includes(key.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMarker || hasDep) {
|
||||
frameworks.push(rule.framework);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Determine primary type
|
||||
let primary = 'unknown';
|
||||
if (frameworks.length > 0) {
|
||||
primary = frameworks[0];
|
||||
} else if (languages.length > 0) {
|
||||
primary = languages[0];
|
||||
}
|
||||
|
||||
// Determine if fullstack (both frontend and backend languages)
|
||||
const frontendSignals = ['react', 'vue', 'angular', 'svelte', 'nextjs', 'nuxt', 'astro', 'remix'];
|
||||
const backendSignals = ['django', 'fastapi', 'flask', 'express', 'nestjs', 'rails', 'spring', 'laravel', 'phoenix', 'gin', 'echo', 'actix', 'axum'];
|
||||
const hasFrontend = frameworks.some(f => frontendSignals.includes(f));
|
||||
const hasBackend = frameworks.some(f => backendSignals.includes(f));
|
||||
|
||||
if (hasFrontend && hasBackend) {
|
||||
primary = 'fullstack';
|
||||
}
|
||||
|
||||
return {
|
||||
languages,
|
||||
frameworks,
|
||||
primary,
|
||||
projectDir
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectProjectType,
|
||||
LANGUAGE_RULES,
|
||||
FRAMEWORK_RULES,
|
||||
// Exported for testing
|
||||
getPackageJsonDeps,
|
||||
getPythonDeps,
|
||||
getGoDeps,
|
||||
getRustDeps,
|
||||
getComposerDeps,
|
||||
getElixirDeps
|
||||
};
|
||||
418
tests/lib/project-detect.test.js
Normal file
418
tests/lib/project-detect.test.js
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Tests for scripts/lib/project-detect.js
|
||||
*
|
||||
* Run with: node tests/lib/project-detect.test.js
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
const {
|
||||
detectProjectType,
|
||||
LANGUAGE_RULES,
|
||||
FRAMEWORK_RULES,
|
||||
getPackageJsonDeps,
|
||||
getPythonDeps,
|
||||
getGoDeps,
|
||||
getRustDeps,
|
||||
getComposerDeps,
|
||||
getElixirDeps
|
||||
} = require('../../scripts/lib/project-detect');
|
||||
|
||||
// Test helper
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(` \u2717 ${name}`);
|
||||
console.log(` Error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary directory for testing
|
||||
function createTempDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-test-'));
|
||||
}
|
||||
|
||||
// Clean up temp directory
|
||||
function cleanupDir(dir) {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Write a file in the temp directory
|
||||
function writeTestFile(dir, filePath, content = '') {
|
||||
const fullPath = path.join(dir, filePath);
|
||||
const dirName = path.dirname(fullPath);
|
||||
fs.mkdirSync(dirName, { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('\n=== Testing project-detect.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Rule definitions tests
|
||||
console.log('Rule Definitions:');
|
||||
|
||||
if (test('LANGUAGE_RULES is non-empty array', () => {
|
||||
assert.ok(Array.isArray(LANGUAGE_RULES));
|
||||
assert.ok(LANGUAGE_RULES.length > 0);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('FRAMEWORK_RULES is non-empty array', () => {
|
||||
assert.ok(Array.isArray(FRAMEWORK_RULES));
|
||||
assert.ok(FRAMEWORK_RULES.length > 0);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('each language rule has type, markers, and extensions', () => {
|
||||
for (const rule of LANGUAGE_RULES) {
|
||||
assert.ok(typeof rule.type === 'string', `Missing type`);
|
||||
assert.ok(Array.isArray(rule.markers), `Missing markers for ${rule.type}`);
|
||||
assert.ok(Array.isArray(rule.extensions), `Missing extensions for ${rule.type}`);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('each framework rule has framework, language, markers, packageKeys', () => {
|
||||
for (const rule of FRAMEWORK_RULES) {
|
||||
assert.ok(typeof rule.framework === 'string', `Missing framework`);
|
||||
assert.ok(typeof rule.language === 'string', `Missing language for ${rule.framework}`);
|
||||
assert.ok(Array.isArray(rule.markers), `Missing markers for ${rule.framework}`);
|
||||
assert.ok(Array.isArray(rule.packageKeys), `Missing packageKeys for ${rule.framework}`);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Empty directory detection
|
||||
console.log('\nEmpty Directory:');
|
||||
|
||||
if (test('empty directory returns unknown primary', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = detectProjectType(dir);
|
||||
assert.strictEqual(result.primary, 'unknown');
|
||||
assert.deepStrictEqual(result.languages, []);
|
||||
assert.deepStrictEqual(result.frameworks, []);
|
||||
assert.strictEqual(result.projectDir, dir);
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Python detection
|
||||
console.log('\nPython Detection:');
|
||||
|
||||
if (test('detects python from requirements.txt', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'requirements.txt', 'flask==3.0.0\nrequests>=2.31');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('python'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects python from pyproject.toml', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'pyproject.toml', '[project]\nname = "test"');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('python'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects flask framework from requirements.txt', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'requirements.txt', 'flask==3.0.0\nrequests>=2.31');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('flask'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects django framework from manage.py', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'manage.py', '#!/usr/bin/env python');
|
||||
writeTestFile(dir, 'requirements.txt', 'django>=4.2');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('django'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects fastapi from pyproject.toml dependencies', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'pyproject.toml', '[project]\nname = "test"\ndependencies = [\n "fastapi>=0.100",\n "uvicorn"\n]');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('fastapi'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// TypeScript/JavaScript detection
|
||||
console.log('\nTypeScript/JavaScript Detection:');
|
||||
|
||||
if (test('detects typescript from tsconfig.json', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'tsconfig.json', '{}');
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{}}');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('typescript'));
|
||||
// Should NOT also include javascript when TS is detected
|
||||
assert.ok(!result.languages.includes('javascript'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects nextjs from next.config.mjs', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'tsconfig.json', '{}');
|
||||
writeTestFile(dir, 'next.config.mjs', 'export default {}');
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{"next":"14.0.0","react":"18.0.0"}}');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('nextjs'));
|
||||
assert.ok(result.frameworks.includes('react'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects react from package.json', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0","react-dom":"18.0.0"}}');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('react'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('detects angular from angular.json', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'angular.json', '{}');
|
||||
writeTestFile(dir, 'tsconfig.json', '{}');
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{"@angular/core":"17.0.0"}}');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('angular'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Go detection
|
||||
console.log('\nGo Detection:');
|
||||
|
||||
if (test('detects golang from go.mod', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'go.mod', 'module github.com/test/app\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n)');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('golang'));
|
||||
assert.ok(result.frameworks.includes('gin'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Rust detection
|
||||
console.log('\nRust Detection:');
|
||||
|
||||
if (test('detects rust from Cargo.toml', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'Cargo.toml', '[package]\nname = "test"\n\n[dependencies]\naxum = "0.7"');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('rust'));
|
||||
assert.ok(result.frameworks.includes('axum'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Ruby detection
|
||||
console.log('\nRuby Detection:');
|
||||
|
||||
if (test('detects ruby and rails', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'Gemfile', 'source "https://rubygems.org"\ngem "rails"');
|
||||
writeTestFile(dir, 'config/routes.rb', 'Rails.application.routes.draw do\nend');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('ruby'));
|
||||
assert.ok(result.frameworks.includes('rails'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// PHP detection
|
||||
console.log('\nPHP Detection:');
|
||||
|
||||
if (test('detects php and laravel', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'composer.json', '{"require":{"laravel/framework":"^10.0"}}');
|
||||
writeTestFile(dir, 'artisan', '#!/usr/bin/env php');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('php'));
|
||||
assert.ok(result.frameworks.includes('laravel'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Fullstack detection
|
||||
console.log('\nFullstack Detection:');
|
||||
|
||||
if (test('detects fullstack when frontend + backend frameworks present', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0","express":"4.18.0"}}');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.frameworks.includes('react'));
|
||||
assert.ok(result.frameworks.includes('express'));
|
||||
assert.strictEqual(result.primary, 'fullstack');
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Dependency reader tests
|
||||
console.log('\nDependency Readers:');
|
||||
|
||||
if (test('getPackageJsonDeps reads deps and devDeps', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'package.json', '{"dependencies":{"react":"18.0.0"},"devDependencies":{"typescript":"5.0.0"}}');
|
||||
const deps = getPackageJsonDeps(dir);
|
||||
assert.ok(deps.includes('react'));
|
||||
assert.ok(deps.includes('typescript'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getPythonDeps reads requirements.txt', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'requirements.txt', 'flask>=3.0\n# comment\nrequests==2.31\n-r other.txt');
|
||||
const deps = getPythonDeps(dir);
|
||||
assert.ok(deps.includes('flask'));
|
||||
assert.ok(deps.includes('requests'));
|
||||
assert.ok(!deps.includes('-r'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getGoDeps reads go.mod require block', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'go.mod', 'module test\n\ngo 1.22\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/lib/pq v1.10.9\n)');
|
||||
const deps = getGoDeps(dir);
|
||||
assert.ok(deps.some(d => d.includes('gin-gonic/gin')));
|
||||
assert.ok(deps.some(d => d.includes('lib/pq')));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getRustDeps reads Cargo.toml', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'Cargo.toml', '[package]\nname = "test"\n\n[dependencies]\nserde = "1.0"\ntokio = { version = "1.0", features = ["full"] }');
|
||||
const deps = getRustDeps(dir);
|
||||
assert.ok(deps.includes('serde'));
|
||||
assert.ok(deps.includes('tokio'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('returns empty arrays for missing files', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
assert.deepStrictEqual(getPackageJsonDeps(dir), []);
|
||||
assert.deepStrictEqual(getPythonDeps(dir), []);
|
||||
assert.deepStrictEqual(getGoDeps(dir), []);
|
||||
assert.deepStrictEqual(getRustDeps(dir), []);
|
||||
assert.deepStrictEqual(getComposerDeps(dir), []);
|
||||
assert.deepStrictEqual(getElixirDeps(dir), []);
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Elixir detection
|
||||
console.log('\nElixir Detection:');
|
||||
|
||||
if (test('detects elixir from mix.exs', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'mix.exs', 'defmodule Test.MixProject do\n defp deps do\n [{:phoenix, "~> 1.7"},\n {:ecto, "~> 3.0"}]\n end\nend');
|
||||
const result = detectProjectType(dir);
|
||||
assert.ok(result.languages.includes('elixir'));
|
||||
assert.ok(result.frameworks.includes('phoenix'));
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Edge cases
|
||||
console.log('\nEdge Cases:');
|
||||
|
||||
if (test('handles non-existent directory gracefully', () => {
|
||||
const result = detectProjectType('/tmp/nonexistent-dir-' + Date.now());
|
||||
assert.strictEqual(result.primary, 'unknown');
|
||||
assert.deepStrictEqual(result.languages, []);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('handles malformed package.json', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'package.json', 'not valid json{{{');
|
||||
const deps = getPackageJsonDeps(dir);
|
||||
assert.deepStrictEqual(deps, []);
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('handles malformed composer.json', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
writeTestFile(dir, 'composer.json', '{invalid');
|
||||
const deps = getComposerDeps(dir);
|
||||
assert.deepStrictEqual(deps, []);
|
||||
} finally {
|
||||
cleanupDir(dir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests();
|
||||
@@ -15,6 +15,7 @@ const testFiles = [
|
||||
'lib/package-manager.test.js',
|
||||
'lib/session-manager.test.js',
|
||||
'lib/session-aliases.test.js',
|
||||
'lib/project-detect.test.js',
|
||||
'hooks/hooks.test.js',
|
||||
'hooks/evaluate-session.test.js',
|
||||
'hooks/suggest-compact.test.js',
|
||||
|
||||
Reference in New Issue
Block a user