carto-md 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.cartoignore ADDED
@@ -0,0 +1,16 @@
1
+ # Carto ignore rules — files matching these patterns are excluded from scanning
2
+ # Default rules are always active even without this file
3
+
4
+ # Secrets and credentials
5
+ .env
6
+ .env.*
7
+ *secret*
8
+ *SECRET*
9
+ *password*
10
+ *PASSWORD*
11
+ *credential*
12
+ *CREDENTIAL*
13
+ *private_key*
14
+ *PRIVATE_KEY*
15
+ *.pem
16
+ *.key
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "carto-md",
3
+ "version": "1.0.0",
4
+ "description": "The context layer for AI-native development.",
5
+ "bin": {
6
+ "carto": "src/cli/index.js"
7
+ },
8
+ "main": "./src/sync.js",
9
+ "scripts": {
10
+ "test": "node test/test.js"
11
+ },
12
+ "dependencies": {
13
+ "@babel/parser": "^7.29.3",
14
+ "chokidar": "^3.6.0"
15
+ },
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "author": {
20
+ "name": "Ansh Sonkar",
21
+ "url": "https://github.com/theanshsonkar",
22
+ "twitter": "https://x.com/theanshsonkar"
23
+ },
24
+ "license": "MIT",
25
+ "keywords": [
26
+ "agents",
27
+ "AGENTS.md",
28
+ "AI",
29
+ "context",
30
+ "codebase",
31
+ "developer-tools",
32
+ "cursor",
33
+ "copilot"
34
+ ]
35
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Converts extracted data into markdown sections for AGENTS.md.
3
+ *
4
+ * Section order:
5
+ * 1. Project Structure (auto)
6
+ * 2. File Map (auto)
7
+ * 3. API Routes (auto)
8
+ * 4. Models (auto)
9
+ * 5. Functions (auto)
10
+ * 6. Database Tables (auto)
11
+ * 7. Environment Variables (auto)
12
+ * 8. Frontend API Calls (auto)
13
+ * 9. Frontend Storage Keys (auto)
14
+ */
15
+ function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars }) {
16
+ const sections = [];
17
+
18
+ // 1. Project Structure
19
+ sections.push('## Project Structure (auto)\n');
20
+ if (structure.length > 0) {
21
+ for (const entry of structure) {
22
+ const icon = entry.type === 'dir' ? '📁' : '📄';
23
+ const suffix = entry.type === 'dir' ? '/' : '';
24
+ sections.push(`- ${icon} ${entry.name}${suffix}`);
25
+ }
26
+ } else {
27
+ sections.push('_No structure data available._');
28
+ }
29
+
30
+ // 2. File Map
31
+ sections.push('\n## File Map (auto)\n');
32
+ if (fileMap && fileMap.length > 0) {
33
+ sections.push('| File | Responsibility |');
34
+ sections.push('|------|----------------|');
35
+ for (const entry of fileMap) {
36
+ sections.push(`| ${entry.file} | ${entry.responsibility} |`);
37
+ }
38
+ } else {
39
+ sections.push('_No file map data available._');
40
+ }
41
+
42
+ // 3. API Routes
43
+ sections.push('\n## API Routes (auto)\n');
44
+ if (routes.length > 0) {
45
+ sections.push('| Method | Path | Handler |');
46
+ sections.push('|--------|------|---------|');
47
+ for (const r of routes) {
48
+ sections.push(`| ${r.method} | ${r.path} | ${r.functionName} |`);
49
+ }
50
+ } else {
51
+ sections.push('_No routes found._');
52
+ }
53
+
54
+ // 4. Models
55
+ sections.push('\n## Models (auto)\n');
56
+ if (models.length > 0) {
57
+ for (const m of models) {
58
+ sections.push(`### ${m.className}`);
59
+ if (m.fields.length > 0) {
60
+ sections.push('| Field | Type |');
61
+ sections.push('|-------|------|');
62
+ for (const f of m.fields) {
63
+ sections.push(`| ${f.name} | ${f.type} |`);
64
+ }
65
+ } else {
66
+ sections.push('_No fields._');
67
+ }
68
+ sections.push('');
69
+ }
70
+ } else {
71
+ sections.push('_No models found._');
72
+ }
73
+
74
+ // 5. Functions
75
+ sections.push('## Functions (auto)\n');
76
+ if (functions && Object.keys(functions).length > 0) {
77
+ const sortedFiles = Object.keys(functions).sort();
78
+ for (const filename of sortedFiles) {
79
+ const funcs = functions[filename];
80
+ if (funcs.length === 0) continue;
81
+ sections.push(`### ${filename}`);
82
+ sections.push('| Function | Params | Returns |');
83
+ sections.push('|----------|--------|---------|');
84
+ for (const f of funcs) {
85
+ sections.push(`| ${f.name} | ${f.params} | ${f.returnType} |`);
86
+ }
87
+ sections.push('');
88
+ }
89
+ } else {
90
+ sections.push('_No functions found._');
91
+ }
92
+
93
+ // 6. Database Tables
94
+ sections.push('## Database Tables (auto)\n');
95
+ if (dbTables && dbTables.length > 0) {
96
+ sections.push('| Table | Model | File |');
97
+ sections.push('|-------|-------|------|');
98
+ for (const t of dbTables) {
99
+ sections.push(`| ${t.tableName} | ${t.modelName} | ${t.file} |`);
100
+ }
101
+ } else {
102
+ sections.push('_No database tables detected._');
103
+ }
104
+
105
+ // 7. Environment Variables
106
+ sections.push('\n## Environment Variables (auto)\n');
107
+ if (envVars && envVars.length > 0) {
108
+ sections.push('| Variable | Used In |');
109
+ sections.push('|----------|---------|');
110
+ for (const v of envVars) {
111
+ sections.push(`| ${v.name} | ${v.files.join(', ')} |`);
112
+ }
113
+ } else {
114
+ sections.push('_No environment variables detected._');
115
+ }
116
+
117
+ // 8. Frontend API Calls
118
+ sections.push('\n## Frontend API Calls (auto)\n');
119
+ if (frontend.fetches.length > 0) {
120
+ sections.push('| Method | URL |');
121
+ sections.push('|--------|-----|');
122
+ for (const f of frontend.fetches) {
123
+ sections.push(`| ${f.method} | ${f.url} |`);
124
+ }
125
+ } else {
126
+ sections.push('_No fetch calls found._');
127
+ }
128
+
129
+ // 9. Frontend Storage Keys
130
+ sections.push('\n## Frontend Storage Keys (auto)\n');
131
+ if (frontend.storageKeys.length > 0) {
132
+ sections.push('| Operation | Key |');
133
+ sections.push('|-----------|-----|');
134
+ for (const s of frontend.storageKeys) {
135
+ sections.push(`| ${s.operation} | ${s.key} |`);
136
+ }
137
+ } else {
138
+ sections.push('_No sessionStorage usage found._');
139
+ }
140
+
141
+ // Warnings (if any)
142
+ if (warnings.length > 0) {
143
+ sections.push('\n---');
144
+ sections.push('_Some sources could not be read. Sections above may be incomplete._');
145
+ }
146
+
147
+ return sections.join('\n');
148
+ }
149
+
150
+ module.exports = { formatSections };
@@ -0,0 +1,70 @@
1
+ const fs = require('fs');
2
+
3
+ const START_MARKER = '<!-- CARTO:AUTO:START -->';
4
+ const END_MARKER = '<!-- CARTO:AUTO:END -->';
5
+
6
+ /**
7
+ * Safely writes auto-generated content into AGENTS.md between markers.
8
+ * Never touches anything outside the markers.
9
+ * Uses atomic write (write to .tmp, then rename) to prevent corruption.
10
+ *
11
+ * Cases:
12
+ * 1. File does not exist → create with markers + content
13
+ * 2. File exists, no markers → append markers + content at end
14
+ * 3. File exists, markers reversed (END before START) → treat as corrupted, append
15
+ * 4. File exists, valid markers → replace ONLY between markers
16
+ */
17
+ function mergeIntoAgentsMd(agentsPath, autoContent) {
18
+ const markerBlock = `${START_MARKER}\n${autoContent}\n${END_MARKER}`;
19
+
20
+ // Case 1: File does not exist
21
+ if (!fs.existsSync(agentsPath)) {
22
+ const tmpPath = agentsPath + '.tmp';
23
+ fs.writeFileSync(tmpPath, markerBlock + '\n', 'utf-8');
24
+ fs.renameSync(tmpPath, agentsPath);
25
+ return;
26
+ }
27
+
28
+ let existing;
29
+ try {
30
+ existing = fs.readFileSync(agentsPath, 'utf-8');
31
+ } catch (err) {
32
+ console.error(`[CARTO] Error reading AGENTS.md: ${err.message}`);
33
+ return;
34
+ }
35
+
36
+ const startIdx = existing.indexOf(START_MARKER);
37
+ const endIdx = existing.indexOf(END_MARKER);
38
+
39
+ // Case 2: No markers found
40
+ if (startIdx === -1 || endIdx === -1) {
41
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
42
+ const tmpPath = agentsPath + '.tmp';
43
+ fs.writeFileSync(tmpPath, existing + separator + markerBlock + '\n', 'utf-8');
44
+ fs.renameSync(tmpPath, agentsPath);
45
+ return;
46
+ }
47
+
48
+ // Case 3: Markers reversed or overlapping — treat as corrupted
49
+ if (startIdx >= endIdx) {
50
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
51
+ const tmpPath = agentsPath + '.tmp';
52
+ fs.writeFileSync(tmpPath, existing + separator + markerBlock + '\n', 'utf-8');
53
+ fs.renameSync(tmpPath, agentsPath);
54
+ console.warn('[CARTO] Warning: markers were reversed/corrupted — appended fresh marker block');
55
+ return;
56
+ }
57
+
58
+ // Case 4: Valid markers — replace between them
59
+ const before = existing.substring(0, startIdx);
60
+ const after = existing.substring(endIdx + END_MARKER.length);
61
+ try {
62
+ const tmpPath = agentsPath + '.tmp';
63
+ fs.writeFileSync(tmpPath, before + markerBlock + after, 'utf-8');
64
+ fs.renameSync(tmpPath, agentsPath);
65
+ } catch (err) {
66
+ console.error(`[CARTO] Error writing AGENTS.md: ${err.message}`);
67
+ }
68
+ }
69
+
70
+ module.exports = { mergeIntoAgentsMd, START_MARKER, END_MARKER };
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ const command = process.argv[2];
4
+
5
+ function printUsage() {
6
+ console.log(`
7
+ Usage: carto <command>
8
+
9
+ Commands:
10
+ init Detect project, write .carto/config.json, run first sync
11
+ watch Read .carto/config.json, start file watcher
12
+ sync Read .carto/config.json, run one sync, exit
13
+
14
+ Options:
15
+ --help, -h Show this help message
16
+ `);
17
+ }
18
+
19
+ if (!command || command === '--help' || command === '-h') {
20
+ printUsage();
21
+ process.exit(0);
22
+ }
23
+
24
+ if (command === 'init') {
25
+ require('./init').run(process.cwd()).catch(err => {
26
+ console.error(`[CARTO] Fatal error: ${err.message}`);
27
+ process.exit(1);
28
+ });
29
+ } else if (command === 'watch') {
30
+ require('./watch').run(process.cwd()).catch(err => {
31
+ console.error(`[CARTO] Fatal error: ${err.message}`);
32
+ process.exit(1);
33
+ });
34
+ } else if (command === 'sync') {
35
+ require('./sync').run(process.cwd()).catch(err => {
36
+ console.error(`[CARTO] Fatal error: ${err.message}`);
37
+ process.exit(1);
38
+ });
39
+ } else {
40
+ console.error(`[CARTO] Unknown command: ${command}`);
41
+ printUsage();
42
+ process.exit(1);
43
+ }
@@ -0,0 +1,80 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { detectFramework } = require('../detector/framework');
4
+ const { discoverFiles } = require('../detector/files');
5
+ const { parseCartoIgnore } = require('../security/ignore');
6
+ const { runFullSync } = require('../sync');
7
+
8
+ async function run(projectRoot) {
9
+ console.log('[CARTO] Detecting project...');
10
+
11
+ const detection = detectFramework(projectRoot);
12
+ console.log(`[CARTO] Detected: ${detection.framework} (${detection.language})`);
13
+
14
+ const isIgnored = parseCartoIgnore(projectRoot);
15
+ const files = discoverFiles(projectRoot, detection.framework, isIgnored);
16
+
17
+ // Count files for reporting
18
+ const pyCount = files.routeFiles.filter(f => f.endsWith('.py')).length;
19
+ const jsCount = files.routeFiles.filter(f => /\.(js|ts|jsx|tsx)$/.test(f)).length;
20
+ const htmlCount = files.frontendFiles.length;
21
+
22
+ const parts = [];
23
+ if (pyCount > 0) parts.push(`${pyCount} Python files`);
24
+ if (jsCount > 0) parts.push(`${jsCount} JS/TS files`);
25
+ if (htmlCount > 0) parts.push(`${htmlCount} HTML files`);
26
+ console.log(`[CARTO] Found ${parts.join(', ') || '0 files'}`);
27
+
28
+ // Make paths relative for config storage
29
+ const relRouteFiles = files.routeFiles.map(f => path.relative(projectRoot, f));
30
+ const relModelFiles = files.modelFiles.map(f => path.relative(projectRoot, f));
31
+ const relFrontendFiles = files.frontendFiles.map(f => path.relative(projectRoot, f));
32
+
33
+ // Write .carto/config.json
34
+ const cartoDir = path.join(projectRoot, '.carto');
35
+ if (!fs.existsSync(cartoDir)) {
36
+ fs.mkdirSync(cartoDir, { recursive: true });
37
+ }
38
+
39
+ const config = {
40
+ version: '1',
41
+ framework: detection.framework,
42
+ language: detection.language,
43
+ watch: {
44
+ routeFiles: relRouteFiles,
45
+ modelFiles: relModelFiles,
46
+ frontendFiles: relFrontendFiles
47
+ },
48
+ output: 'AGENTS.md',
49
+ generated: new Date().toISOString()
50
+ };
51
+
52
+ fs.writeFileSync(
53
+ path.join(cartoDir, 'config.json'),
54
+ JSON.stringify(config, null, 2) + '\n',
55
+ 'utf-8'
56
+ );
57
+
58
+ // Run first sync
59
+ const syncConfig = resolveConfig(projectRoot, config);
60
+ await runFullSync(syncConfig);
61
+
62
+ console.log('[CARTO] AGENTS.md generated. Run "carto watch" to keep it live.');
63
+ }
64
+
65
+ /**
66
+ * Resolves relative paths in config to absolute paths.
67
+ */
68
+ function resolveConfig(projectRoot, config) {
69
+ return {
70
+ watch: {
71
+ routeFiles: (config.watch.routeFiles || []).map(f => path.resolve(projectRoot, f)),
72
+ modelFiles: (config.watch.modelFiles || []).map(f => path.resolve(projectRoot, f)),
73
+ frontendFiles: (config.watch.frontendFiles || []).map(f => path.resolve(projectRoot, f))
74
+ },
75
+ output: path.resolve(projectRoot, config.output),
76
+ projectRoot
77
+ };
78
+ }
79
+
80
+ module.exports = { run, resolveConfig };
@@ -0,0 +1,27 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { runFullSync } = require('../sync');
4
+ const { resolveConfig } = require('./init');
5
+
6
+ async function run(projectRoot) {
7
+ const configPath = path.join(projectRoot, '.carto', 'config.json');
8
+
9
+ if (!fs.existsSync(configPath)) {
10
+ console.error('[CARTO] Run "carto init" first.');
11
+ process.exit(1);
12
+ }
13
+
14
+ let config;
15
+ try {
16
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
17
+ } catch (err) {
18
+ console.error(`[CARTO] Error reading .carto/config.json: ${err.message}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const resolved = resolveConfig(projectRoot, config);
23
+ await runFullSync(resolved);
24
+ console.log('[CARTO] Sync complete.');
25
+ }
26
+
27
+ module.exports = { run };
@@ -0,0 +1,52 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { startWatcher } = require('../watcher/watch');
4
+ const { runFullSync } = require('../sync');
5
+ const { resolveConfig } = require('./init');
6
+
7
+ async function run(projectRoot) {
8
+ const configPath = path.join(projectRoot, '.carto', 'config.json');
9
+
10
+ if (!fs.existsSync(configPath)) {
11
+ console.error('[CARTO] Run "carto init" first.');
12
+ process.exit(1);
13
+ }
14
+
15
+ let config;
16
+ try {
17
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
18
+ } catch (err) {
19
+ console.error(`[CARTO] Error reading .carto/config.json: ${err.message}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const resolved = resolveConfig(projectRoot, config);
24
+
25
+ // Run full sync once on startup
26
+ console.log('[CARTO] Starting initial sync...');
27
+ await runFullSync(resolved);
28
+ console.log('[CARTO] Initial sync complete');
29
+
30
+ // Collect all watch paths (deduplicated)
31
+ const allFiles = new Set([
32
+ ...resolved.watch.routeFiles,
33
+ ...resolved.watch.modelFiles,
34
+ ...resolved.watch.frontendFiles
35
+ ]);
36
+ const watchPaths = [...allFiles];
37
+
38
+ startWatcher(watchPaths, async (changedFile) => {
39
+ await runFullSync(resolved);
40
+ const timestamp = new Date().toISOString();
41
+ const filename = path.basename(changedFile);
42
+ console.log(`[CARTO] ${filename} updated → AGENTS.md synced — ${timestamp}`);
43
+ });
44
+
45
+ console.log('[CARTO] Watching files...');
46
+ for (const p of watchPaths) {
47
+ console.log(` → ${p}`);
48
+ }
49
+ console.log(` → Output: ${resolved.output}`);
50
+ }
51
+
52
+ module.exports = { run };
@@ -0,0 +1,92 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const MAX_FILES_PER_CATEGORY = 50;
5
+
6
+ const PYTHON_IGNORE = new Set(['__pycache__', '.venv', 'venv', 'migrations', 'node_modules', '.git', '.carto']);
7
+ const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto']);
8
+ const HTML_IGNORE = new Set(['node_modules', '.git', '.carto']);
9
+
10
+ /**
11
+ * discoverFiles(projectRoot, framework, isIgnored) → { routeFiles, modelFiles, frontendFiles }
12
+ *
13
+ * isIgnored is an optional function (filePath) → boolean from the .cartoignore parser.
14
+ */
15
+ function discoverFiles(projectRoot, framework, isIgnored) {
16
+ const ignoreFn = isIgnored || (() => false);
17
+
18
+ if (['fastapi', 'django', 'flask', 'python-generic'].includes(framework)) {
19
+ const pyFiles = findFilesRecursive(projectRoot, ['.py'], PYTHON_IGNORE, ignoreFn)
20
+ .filter(f => {
21
+ const base = path.basename(f);
22
+ return !base.startsWith('test_') && !base.endsWith('_test.py');
23
+ });
24
+ const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
25
+
26
+ return {
27
+ routeFiles: cap(pyFiles),
28
+ modelFiles: cap(pyFiles),
29
+ frontendFiles: cap(htmlFiles)
30
+ };
31
+ }
32
+
33
+ if (['express', 'nextjs', 'react', 'node-generic'].includes(framework)) {
34
+ const jsFiles = findFilesRecursive(projectRoot, ['.js', '.ts', '.jsx', '.tsx'], JS_IGNORE, ignoreFn)
35
+ .filter(f => {
36
+ const base = path.basename(f);
37
+ return !base.includes('.test.') && !base.includes('.spec.');
38
+ });
39
+ const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
40
+
41
+ return {
42
+ routeFiles: cap(jsFiles),
43
+ modelFiles: cap(jsFiles),
44
+ frontendFiles: cap(htmlFiles)
45
+ };
46
+ }
47
+
48
+ // Unknown framework — best effort
49
+ const allCode = findFilesRecursive(projectRoot, ['.py', '.js', '.ts'], new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.carto']), ignoreFn);
50
+ const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
51
+
52
+ return {
53
+ routeFiles: cap(allCode),
54
+ modelFiles: cap(allCode),
55
+ frontendFiles: cap(htmlFiles)
56
+ };
57
+ }
58
+
59
+ function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []) {
60
+ let items;
61
+ try {
62
+ items = fs.readdirSync(dir, { withFileTypes: true });
63
+ } catch {
64
+ return results;
65
+ }
66
+
67
+ for (const item of items) {
68
+ if (ignoreDirs.has(item.name)) continue;
69
+
70
+ const fullPath = path.join(dir, item.name);
71
+
72
+ if (item.isDirectory()) {
73
+ findFilesRecursive(fullPath, extensions, ignoreDirs, isIgnored, results);
74
+ } else if (item.isFile()) {
75
+ const ext = path.extname(item.name).toLowerCase();
76
+ if (extensions.includes(ext) && !isIgnored(fullPath)) {
77
+ results.push(fullPath);
78
+ }
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+
84
+ function cap(files) {
85
+ if (files.length > MAX_FILES_PER_CATEGORY) {
86
+ console.warn(`[CARTO] Warning: Found ${files.length} files, capping at ${MAX_FILES_PER_CATEGORY}`);
87
+ return files.slice(0, MAX_FILES_PER_CATEGORY);
88
+ }
89
+ return files;
90
+ }
91
+
92
+ module.exports = { discoverFiles };
@@ -0,0 +1,117 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto']);
5
+
6
+ /**
7
+ * detectFramework(projectRoot) → { framework, language, confidence }
8
+ *
9
+ * Search order (recursive up to 3 levels deep):
10
+ * 1. requirements.txt → fastapi / django / flask / python-generic
11
+ * 2. package.json → nextjs / express / react / node-generic
12
+ * 3. pyproject.toml → same logic as requirements.txt
13
+ * 4. Nothing found → { framework: 'unknown', language: 'unknown' }
14
+ *
15
+ * Returns first match found. Does not combine multiple detections.
16
+ */
17
+ function detectFramework(projectRoot) {
18
+ // Search for files up to 3 levels deep
19
+ const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml'], 3);
20
+
21
+ // 1. Check requirements.txt
22
+ const reqFile = candidates.find(f => path.basename(f) === 'requirements.txt');
23
+ if (reqFile) {
24
+ const result = detectFromPythonDeps(reqFile);
25
+ if (result) return result;
26
+ }
27
+
28
+ // 2. Check package.json
29
+ const pkgFile = candidates.find(f => path.basename(f) === 'package.json');
30
+ if (pkgFile) {
31
+ const result = detectFromPackageJson(pkgFile);
32
+ if (result) return result;
33
+ }
34
+
35
+ // 3. Check pyproject.toml
36
+ const pyprojectFile = candidates.find(f => path.basename(f) === 'pyproject.toml');
37
+ if (pyprojectFile) {
38
+ const result = detectFromPythonDeps(pyprojectFile);
39
+ if (result) return result;
40
+ }
41
+
42
+ return { framework: 'unknown', language: 'unknown', confidence: 'none' };
43
+ }
44
+
45
+ /**
46
+ * Recursively find files by name up to maxDepth levels.
47
+ */
48
+ function findFile(dir, fileNames, maxDepth, currentDepth = 0) {
49
+ const results = [];
50
+ if (currentDepth > maxDepth) return results;
51
+
52
+ let items;
53
+ try {
54
+ items = fs.readdirSync(dir, { withFileTypes: true });
55
+ } catch {
56
+ return results;
57
+ }
58
+
59
+ for (const item of items) {
60
+ if (IGNORE_DIRS.has(item.name)) continue;
61
+
62
+ const fullPath = path.join(dir, item.name);
63
+ if (item.isFile() && fileNames.includes(item.name)) {
64
+ results.push(fullPath);
65
+ } else if (item.isDirectory() && currentDepth < maxDepth) {
66
+ results.push(...findFile(fullPath, fileNames, maxDepth, currentDepth + 1));
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+
72
+ function detectFromPythonDeps(filePath) {
73
+ let content;
74
+ try {
75
+ content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
76
+ } catch {
77
+ return null;
78
+ }
79
+
80
+ if (content.includes('fastapi')) {
81
+ return { framework: 'fastapi', language: 'python', confidence: 'high' };
82
+ }
83
+ if (content.includes('django')) {
84
+ return { framework: 'django', language: 'python', confidence: 'high' };
85
+ }
86
+ if (content.includes('flask')) {
87
+ return { framework: 'flask', language: 'python', confidence: 'high' };
88
+ }
89
+ if (content.includes('pydantic')) {
90
+ return { framework: 'python-generic', language: 'python', confidence: 'medium' };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function detectFromPackageJson(filePath) {
96
+ let pkg;
97
+ try {
98
+ pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
99
+ } catch {
100
+ return null;
101
+ }
102
+
103
+ const deps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
104
+
105
+ if (deps['next']) {
106
+ return { framework: 'nextjs', language: 'javascript', confidence: 'high' };
107
+ }
108
+ if (deps['express']) {
109
+ return { framework: 'express', language: 'javascript', confidence: 'high' };
110
+ }
111
+ if (deps['react']) {
112
+ return { framework: 'react', language: 'javascript', confidence: 'high' };
113
+ }
114
+ return { framework: 'node-generic', language: 'javascript', confidence: 'medium' };
115
+ }
116
+
117
+ module.exports = { detectFramework };