carto-md 1.0.3 → 1.0.5
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/package.json +1 -1
- package/src/agents/formatter.js +19 -1
- package/src/agents/validator.js +1 -1
- package/src/cli/init.js +30 -2
- package/src/detector/files.js +43 -7
- package/src/detector/framework.js +72 -51
- package/src/extractors/filemap.js +9 -0
- package/src/extractors/languages/javascript.js +56 -36
- package/src/extractors/languages/typescript.js +95 -0
- package/src/extractors/stack.js +172 -0
- package/src/sync.js +38 -8
package/package.json
CHANGED
package/src/agents/formatter.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Converts extracted data into markdown sections for AGENTS.md.
|
|
3
5
|
*
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
* 9. Frontend API Calls (auto)
|
|
14
16
|
* 10. Frontend Storage Keys (auto)
|
|
15
17
|
*/
|
|
16
|
-
function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars, importGraph }) {
|
|
18
|
+
function formatSections({ routes, models, frontend, structure, warnings, fileMap, functions, dbTables, envVars, importGraph, stackItems, entryPoints, highImpact }) {
|
|
17
19
|
const sections = [];
|
|
18
20
|
|
|
19
21
|
// 1. Project Structure
|
|
@@ -28,6 +30,22 @@ function formatSections({ routes, models, frontend, structure, warnings, fileMap
|
|
|
28
30
|
sections.push('_No structure data available._');
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
// Stack line — right after structure
|
|
34
|
+
if (stackItems && stackItems.length > 0) {
|
|
35
|
+
sections.push(`\n**Stack:** ${stackItems.join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Entry points
|
|
39
|
+
if (entryPoints && entryPoints.length > 0) {
|
|
40
|
+
sections.push(`**Entry points:** ${entryPoints.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// High impact files
|
|
44
|
+
if (highImpact && highImpact.length > 0) {
|
|
45
|
+
const items = highImpact.map(h => `${h.file} (${h.count} dependents)`);
|
|
46
|
+
sections.push(`**High impact:** ${items.join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
// 2. File Map
|
|
32
50
|
sections.push('\n## File Map (auto)\n');
|
|
33
51
|
if (fileMap && fileMap.length > 0) {
|
package/src/agents/validator.js
CHANGED
|
@@ -14,7 +14,7 @@ function validateExtracted({ routes, models, functions, envVars, dbTables }) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
17
|
+
const VALID_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'ALL']);
|
|
18
18
|
const VALID_HANDLER = /^[a-zA-Z_]\w*$|^\[anonymous\]$/;
|
|
19
19
|
const VALID_CLASS_NAME = /^[A-Z][a-zA-Z0-9]*$/;
|
|
20
20
|
const VALID_FIELD_NAME = /^[a-z_]\w*$/;
|
package/src/cli/init.js
CHANGED
|
@@ -12,7 +12,7 @@ async function run(projectRoot) {
|
|
|
12
12
|
console.log(`[CARTO] Detected: ${detection.framework} (${detection.language})`);
|
|
13
13
|
|
|
14
14
|
const isIgnored = parseCartoIgnore(projectRoot);
|
|
15
|
-
const files = discoverFiles(projectRoot, detection.framework, isIgnored);
|
|
15
|
+
const files = discoverFiles(projectRoot, detection.framework, isIgnored, detection.secondaryFramework);
|
|
16
16
|
|
|
17
17
|
// Count files for reporting
|
|
18
18
|
const pyCount = files.routeFiles.filter(f => f.endsWith('.py')).length;
|
|
@@ -55,11 +55,14 @@ async function run(projectRoot) {
|
|
|
55
55
|
'utf-8'
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
// Install pre-commit hook
|
|
59
|
+
installGitHook(projectRoot);
|
|
60
|
+
|
|
58
61
|
// Run first sync
|
|
59
62
|
const syncConfig = resolveConfig(projectRoot, config);
|
|
60
63
|
await runFullSync(syncConfig);
|
|
61
64
|
|
|
62
|
-
console.log('[CARTO] AGENTS.md generated.
|
|
65
|
+
console.log('[CARTO] AGENTS.md generated. Carto will sync on every git commit.');
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -77,4 +80,29 @@ function resolveConfig(projectRoot, config) {
|
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function installGitHook(projectRoot) {
|
|
84
|
+
const gitDir = path.join(projectRoot, '.git');
|
|
85
|
+
if (!fs.existsSync(gitDir)) return;
|
|
86
|
+
|
|
87
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
88
|
+
if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
|
|
89
|
+
|
|
90
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
91
|
+
const hookLine = 'carto sync\n';
|
|
92
|
+
|
|
93
|
+
if (fs.existsSync(hookPath)) {
|
|
94
|
+
const existing = fs.readFileSync(hookPath, 'utf-8');
|
|
95
|
+
if (existing.includes('carto sync')) {
|
|
96
|
+
console.log('[CARTO] Git hook already installed.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
fs.appendFileSync(hookPath, '\n' + hookLine);
|
|
100
|
+
} else {
|
|
101
|
+
fs.writeFileSync(hookPath, '#!/bin/sh\n' + hookLine);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.chmodSync(hookPath, '755');
|
|
105
|
+
console.log('[CARTO] Git pre-commit hook installed.');
|
|
106
|
+
}
|
|
107
|
+
|
|
80
108
|
module.exports = { run, resolveConfig };
|
package/src/detector/files.js
CHANGED
|
@@ -4,17 +4,38 @@ const path = require('path');
|
|
|
4
4
|
const MAX_FILES_PER_CATEGORY = 50;
|
|
5
5
|
|
|
6
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']);
|
|
7
|
+
const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto', '.next', '.turbo', 'coverage', 'out', '.cache', 'generated', '__generated__', 'storybook-static', 'public', 'static']);
|
|
8
8
|
const HTML_IGNORE = new Set(['node_modules', '.git', '.carto']);
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* discoverFiles(projectRoot, framework, isIgnored) → { routeFiles, modelFiles, frontendFiles }
|
|
11
|
+
* discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) → { routeFiles, modelFiles, frontendFiles }
|
|
12
12
|
*
|
|
13
13
|
* isIgnored is an optional function (filePath) → boolean from the .cartoignore parser.
|
|
14
|
+
* If secondaryFramework is provided, discovers files for both and merges.
|
|
14
15
|
*/
|
|
15
|
-
function discoverFiles(projectRoot, framework, isIgnored) {
|
|
16
|
+
function discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) {
|
|
16
17
|
const ignoreFn = isIgnored || (() => false);
|
|
17
18
|
|
|
19
|
+
const primary = discoverForFramework(projectRoot, framework, ignoreFn);
|
|
20
|
+
|
|
21
|
+
if (secondaryFramework) {
|
|
22
|
+
const secondary = discoverForFramework(projectRoot, secondaryFramework, ignoreFn);
|
|
23
|
+
// Merge and deduplicate
|
|
24
|
+
const routeFiles = [...new Set([...primary.routeFiles, ...secondary.routeFiles])];
|
|
25
|
+
const modelFiles = [...new Set([...primary.modelFiles, ...secondary.modelFiles])];
|
|
26
|
+
const frontendFiles = [...new Set([...primary.frontendFiles, ...secondary.frontendFiles])];
|
|
27
|
+
return {
|
|
28
|
+
routeFiles: cap(routeFiles),
|
|
29
|
+
modelFiles: cap(modelFiles),
|
|
30
|
+
frontendFiles: cap(frontendFiles)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return primary;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function discoverForFramework(projectRoot, framework, ignoreFn) {
|
|
38
|
+
|
|
18
39
|
if (['fastapi', 'django', 'flask', 'python-generic'].includes(framework)) {
|
|
19
40
|
const pyFiles = findFilesRecursive(projectRoot, ['.py'], PYTHON_IGNORE, ignoreFn)
|
|
20
41
|
.filter(f => {
|
|
@@ -81,12 +102,27 @@ function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []
|
|
|
81
102
|
return results;
|
|
82
103
|
}
|
|
83
104
|
|
|
105
|
+
function scoreFile(filePath) {
|
|
106
|
+
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
107
|
+
const base = path.basename(p);
|
|
108
|
+
if (base === 'main.py' || base === 'app.py' || base === 'server.ts' ||
|
|
109
|
+
base === 'server.js' || base === 'app.ts' || base === 'index.ts' ||
|
|
110
|
+
base === 'route.ts' || base === 'routes.ts' || base === 'routes.js') return 10;
|
|
111
|
+
if (p.includes('/api/') || p.includes('/routes/') || p.includes('/route/')) return 9;
|
|
112
|
+
if (p.includes('/models/') || p.includes('/schemas/') || p.includes('/schema/')) return 8;
|
|
113
|
+
if (p.includes('/services/') || p.includes('/controllers/') || p.includes('/handlers/')) return 6;
|
|
114
|
+
if (p.includes('/lib/') || p.includes('/utils/') || p.includes('/helpers/')) return 2;
|
|
115
|
+
return 4;
|
|
116
|
+
}
|
|
117
|
+
|
|
84
118
|
function cap(files) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
const scored = files.map(f => ({ f, score: scoreFile(f) }))
|
|
120
|
+
.sort((a, b) => b.score - a.score);
|
|
121
|
+
if (scored.length > MAX_FILES_PER_CATEGORY) {
|
|
122
|
+
console.warn(`[CARTO] Warning: Found ${scored.length} files, capping at ${MAX_FILES_PER_CATEGORY}`);
|
|
123
|
+
return scored.slice(0, MAX_FILES_PER_CATEGORY).map(x => x.f);
|
|
88
124
|
}
|
|
89
|
-
return
|
|
125
|
+
return scored.map(x => x.f);
|
|
90
126
|
}
|
|
91
127
|
|
|
92
128
|
module.exports = { discoverFiles };
|
|
@@ -3,40 +3,64 @@ const path = require('path');
|
|
|
3
3
|
|
|
4
4
|
const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto']);
|
|
5
5
|
|
|
6
|
+
// Priority order: lower index = higher priority
|
|
7
|
+
const PYTHON_PRIORITY = ['fastapi', 'django', 'flask', 'python-generic'];
|
|
8
|
+
const JS_PRIORITY = ['nextjs', 'express', 'react', 'node-generic'];
|
|
9
|
+
|
|
6
10
|
/**
|
|
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' }
|
|
11
|
+
* detectFramework(projectRoot) → { framework, language, confidence, secondaryFramework?, secondaryLanguage? }
|
|
14
12
|
*
|
|
15
|
-
*
|
|
13
|
+
* Collects all matches from requirements.txt, package.json, pyproject.toml.
|
|
14
|
+
* Picks the most specific Python and JS framework by priority.
|
|
15
|
+
* If both a Python and JS framework are detected, returns both
|
|
16
|
+
* (primary = highest priority overall, secondary = the other language).
|
|
16
17
|
*/
|
|
17
18
|
function detectFramework(projectRoot) {
|
|
18
|
-
// Search for files up to 3 levels deep
|
|
19
19
|
const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml'], 3);
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const pythonDetections = new Set();
|
|
22
|
+
const jsDetections = new Set();
|
|
23
|
+
|
|
24
|
+
// Check requirements.txt
|
|
25
|
+
for (const f of candidates.filter(f => path.basename(f) === 'requirements.txt')) {
|
|
26
|
+
const results = detectAllFromPythonDeps(f);
|
|
27
|
+
for (const r of results) pythonDetections.add(r);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
if (result) return result;
|
|
30
|
+
// Check pyproject.toml
|
|
31
|
+
for (const f of candidates.filter(f => path.basename(f) === 'pyproject.toml')) {
|
|
32
|
+
const results = detectAllFromPythonDeps(f);
|
|
33
|
+
for (const r of results) pythonDetections.add(r);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
36
|
+
// Check package.json
|
|
37
|
+
for (const f of candidates.filter(f => path.basename(f) === 'package.json')) {
|
|
38
|
+
const results = detectAllFromPackageJson(f);
|
|
39
|
+
for (const r of results) jsDetections.add(r);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pick best Python framework by priority
|
|
43
|
+
const bestPython = PYTHON_PRIORITY.find(fw => pythonDetections.has(fw)) || null;
|
|
44
|
+
// Pick best JS framework by priority
|
|
45
|
+
const bestJS = JS_PRIORITY.find(fw => jsDetections.has(fw)) || null;
|
|
46
|
+
|
|
47
|
+
if (bestPython && bestJS) {
|
|
48
|
+
// Both detected — Python is primary (higher priority in the global list)
|
|
49
|
+
return {
|
|
50
|
+
framework: bestPython,
|
|
51
|
+
language: 'python',
|
|
52
|
+
confidence: 'high',
|
|
53
|
+
secondaryFramework: bestJS,
|
|
54
|
+
secondaryLanguage: 'javascript'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (bestPython) {
|
|
59
|
+
return { framework: bestPython, language: 'python', confidence: 'high' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (bestJS) {
|
|
63
|
+
return { framework: bestJS, language: 'javascript', confidence: 'high' };
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
return { framework: 'unknown', language: 'unknown', confidence: 'none' };
|
|
@@ -69,49 +93,46 @@ function findFile(dir, fileNames, maxDepth, currentDepth = 0) {
|
|
|
69
93
|
return results;
|
|
70
94
|
}
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Returns all matching Python frameworks from a deps file.
|
|
98
|
+
*/
|
|
99
|
+
function detectAllFromPythonDeps(filePath) {
|
|
100
|
+
const detected = [];
|
|
73
101
|
let content;
|
|
74
102
|
try {
|
|
75
103
|
content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
|
|
76
104
|
} catch {
|
|
77
|
-
return
|
|
105
|
+
return detected;
|
|
78
106
|
}
|
|
79
107
|
|
|
80
|
-
if (content.includes('fastapi'))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (content.includes('
|
|
84
|
-
|
|
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;
|
|
108
|
+
if (content.includes('fastapi')) detected.push('fastapi');
|
|
109
|
+
if (content.includes('django')) detected.push('django');
|
|
110
|
+
if (content.includes('flask')) detected.push('flask');
|
|
111
|
+
if (content.includes('pydantic') && !detected.length) detected.push('python-generic');
|
|
112
|
+
|
|
113
|
+
return detected;
|
|
93
114
|
}
|
|
94
115
|
|
|
95
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Returns all matching JS frameworks from a package.json.
|
|
118
|
+
*/
|
|
119
|
+
function detectAllFromPackageJson(filePath) {
|
|
120
|
+
const detected = [];
|
|
96
121
|
let pkg;
|
|
97
122
|
try {
|
|
98
123
|
pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
99
124
|
} catch {
|
|
100
|
-
return
|
|
125
|
+
return detected;
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
const deps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
104
129
|
|
|
105
|
-
if (deps['next'])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (deps['react']) {
|
|
112
|
-
return { framework: 'react', language: 'javascript', confidence: 'high' };
|
|
113
|
-
}
|
|
114
|
-
return { framework: 'node-generic', language: 'javascript', confidence: 'medium' };
|
|
130
|
+
if (deps['next']) detected.push('nextjs');
|
|
131
|
+
if (deps['express']) detected.push('express');
|
|
132
|
+
if (deps['react'] && !deps['next']) detected.push('react');
|
|
133
|
+
if (!detected.length) detected.push('node-generic');
|
|
134
|
+
|
|
135
|
+
return detected;
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
module.exports = { detectFramework };
|
|
@@ -9,6 +9,15 @@ const path = require('path');
|
|
|
9
9
|
function inferResponsibility(filename, functionCount, routeCount) {
|
|
10
10
|
const base = path.basename(filename).toLowerCase();
|
|
11
11
|
|
|
12
|
+
// Skip barrel exports — index files with no routes and no functions
|
|
13
|
+
if (
|
|
14
|
+
(base === 'index.ts' || base === 'index.js' || base === 'index.tsx' || base === 'index.jsx') &&
|
|
15
|
+
functionCount === 0 &&
|
|
16
|
+
routeCount === 0
|
|
17
|
+
) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
// Skip __init__.py entirely
|
|
13
22
|
if (base === '__init__.py') return null;
|
|
14
23
|
|
|
@@ -73,13 +73,9 @@ function walk(node, visitor) {
|
|
|
73
73
|
function extractExpressRoutes(ast, filename) {
|
|
74
74
|
const routes = [];
|
|
75
75
|
|
|
76
|
-
// Check for Next.js
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (isNextRoute) {
|
|
80
|
-
const nextRoutes = extractNextJSRoutes(ast, filename);
|
|
81
|
-
routes.push(...nextRoutes);
|
|
82
|
-
}
|
|
76
|
+
// Check for Next.js Pages/App Router patterns (pages/api/... or app/api/...)
|
|
77
|
+
const nextRoutes = extractNextJSPagesRoutes(ast, filename);
|
|
78
|
+
routes.push(...nextRoutes);
|
|
83
79
|
|
|
84
80
|
walk(ast, (node) => {
|
|
85
81
|
if (node.type !== 'CallExpression') return;
|
|
@@ -128,48 +124,72 @@ function extractExpressRoutes(ast, filename) {
|
|
|
128
124
|
});
|
|
129
125
|
});
|
|
130
126
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
// Deduplicate by method + path
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
return routes.filter(r => {
|
|
130
|
+
const key = `${r.method}::${r.path}`;
|
|
131
|
+
if (seen.has(key)) return false;
|
|
132
|
+
seen.add(key);
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Detects Next.js Pages Router pattern:
|
|
139
|
+
* export default function handler(req, res) in files under pages/api/
|
|
140
|
+
* Also handles App Router export default in app/api/ files
|
|
141
|
+
*/
|
|
142
|
+
function extractNextJSPagesRoutes(ast, filename) {
|
|
140
143
|
const routes = [];
|
|
141
|
-
const
|
|
144
|
+
const normalizedPath = ('/' + filename).replace(/\\/g, '/');
|
|
145
|
+
const isApiFile = normalizedPath.includes('/pages/api/') || normalizedPath.includes('/app/api/');
|
|
146
|
+
|
|
147
|
+
if (!isApiFile) return routes;
|
|
148
|
+
if (!ast.program || !ast.program.body) return routes;
|
|
149
|
+
|
|
150
|
+
const HTTP_METHODS_UPPER = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
151
|
+
|
|
152
|
+
// Infer route path from filename
|
|
153
|
+
let routePath = '[inferred]';
|
|
154
|
+
const pagesMatch = normalizedPath.match(/\/pages(\/api\/.+)/);
|
|
155
|
+
const appMatch = normalizedPath.match(/\/app(\/api\/.+)/);
|
|
156
|
+
if (pagesMatch) {
|
|
157
|
+
routePath = pagesMatch[1].replace(/\/[^/]+$/, '').replace(/\/index$/, '');
|
|
158
|
+
if (!routePath || routePath === '/api') routePath = '/api';
|
|
159
|
+
} else if (appMatch) {
|
|
160
|
+
routePath = appMatch[1].replace(/\/[^/]+$/, '').replace(/\/route$/, '');
|
|
161
|
+
if (!routePath || routePath === '/api') routePath = '/api';
|
|
162
|
+
}
|
|
142
163
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
for (const node of ast.program.body) {
|
|
165
|
+
// export default function handler(req, res)
|
|
166
|
+
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
167
|
+
const decl = node.declaration;
|
|
168
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression') {
|
|
169
|
+
const funcName = (decl.id && decl.id.name) || 'handler';
|
|
170
|
+
if (HTTP_METHODS_UPPER.has(funcName.toUpperCase())) {
|
|
171
|
+
routes.push({ method: funcName.toUpperCase(), path: routePath, functionName: funcName });
|
|
172
|
+
} else {
|
|
173
|
+
routes.push({ method: 'ALL', path: routePath, functionName: funcName });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
147
177
|
|
|
148
|
-
|
|
149
|
-
// export async function GET(req) { ... }
|
|
178
|
+
// export function GET/POST/... (named exports in api files)
|
|
150
179
|
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
151
180
|
const decl = node.declaration;
|
|
152
|
-
if (decl.type === 'FunctionDeclaration' && decl.id &&
|
|
153
|
-
routes.push({
|
|
154
|
-
method: decl.id.name,
|
|
155
|
-
path: routePath,
|
|
156
|
-
functionName: decl.id.name
|
|
157
|
-
});
|
|
181
|
+
if (decl.type === 'FunctionDeclaration' && decl.id && HTTP_METHODS_UPPER.has(decl.id.name.toUpperCase())) {
|
|
182
|
+
routes.push({ method: decl.id.name.toUpperCase(), path: routePath, functionName: decl.id.name });
|
|
158
183
|
}
|
|
159
|
-
// export const GET = async (req) => { ... }
|
|
160
184
|
if (decl.type === 'VariableDeclaration') {
|
|
161
|
-
for (const
|
|
162
|
-
if (
|
|
163
|
-
routes.push({
|
|
164
|
-
method: declarator.id.name,
|
|
165
|
-
path: routePath,
|
|
166
|
-
functionName: declarator.id.name
|
|
167
|
-
});
|
|
185
|
+
for (const vDecl of decl.declarations) {
|
|
186
|
+
if (vDecl.id && vDecl.id.name && HTTP_METHODS_UPPER.has(vDecl.id.name.toUpperCase())) {
|
|
187
|
+
routes.push({ method: vDecl.id.name.toUpperCase(), path: routePath, functionName: vDecl.id.name });
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
|
-
}
|
|
192
|
+
}
|
|
173
193
|
|
|
174
194
|
return routes;
|
|
175
195
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const parser = require('@babel/parser');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const jsPlugin = require('./javascript');
|
|
3
4
|
|
|
4
5
|
const TS_PARSE_OPTIONS = {
|
|
@@ -32,6 +33,100 @@ module.exports = {
|
|
|
32
33
|
}
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Next.js Pages Router route detection
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detects Next.js route patterns:
|
|
44
|
+
*
|
|
45
|
+
* Pages Router (pages/api/...):
|
|
46
|
+
* export default function handler(req, res) — treated as ALL methods
|
|
47
|
+
* export default async function handler(req, res)
|
|
48
|
+
*
|
|
49
|
+
* App Router (app/api/.../route.ts):
|
|
50
|
+
* export function GET/POST/PUT/DELETE/PATCH
|
|
51
|
+
* export async function GET/POST/PUT/DELETE/PATCH
|
|
52
|
+
* export const GET/POST = ...
|
|
53
|
+
*/
|
|
54
|
+
function extractNextJSPagesRoutes(ast, filename) {
|
|
55
|
+
const routes = [];
|
|
56
|
+
const normalizedPath = ('/' + filename).replace(/\\/g, '/');
|
|
57
|
+
const isApiFile = normalizedPath.includes('/pages/api/') || normalizedPath.includes('/app/api/');
|
|
58
|
+
|
|
59
|
+
if (!isApiFile) return routes;
|
|
60
|
+
if (!ast.program || !ast.program.body) return routes;
|
|
61
|
+
|
|
62
|
+
// Infer route path from filename
|
|
63
|
+
// pages/api/users/[id].ts → /api/users/[id]
|
|
64
|
+
// app/api/users/route.ts → /api/users
|
|
65
|
+
let routePath = '[inferred]';
|
|
66
|
+
const pagesMatch = normalizedPath.match(/\/pages(\/api\/.+)/);
|
|
67
|
+
const appMatch = normalizedPath.match(/\/app(\/api\/.+)/);
|
|
68
|
+
if (pagesMatch) {
|
|
69
|
+
routePath = pagesMatch[1].replace(/\/[^/]+$/, '').replace(/\/index$/, '');
|
|
70
|
+
if (!routePath || routePath === '/api') routePath = '/api';
|
|
71
|
+
} else if (appMatch) {
|
|
72
|
+
routePath = appMatch[1].replace(/\/[^/]+$/, '').replace(/\/route$/, '');
|
|
73
|
+
if (!routePath || routePath === '/api') routePath = '/api';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const node of ast.program.body) {
|
|
77
|
+
// export default function handler(req, res) — Pages Router pattern
|
|
78
|
+
if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
79
|
+
const decl = node.declaration;
|
|
80
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression') {
|
|
81
|
+
const funcName = (decl.id && decl.id.name) || 'handler';
|
|
82
|
+
|
|
83
|
+
// If the function name is an HTTP method, use it
|
|
84
|
+
if (HTTP_METHODS.has(funcName.toUpperCase())) {
|
|
85
|
+
routes.push({
|
|
86
|
+
method: funcName.toUpperCase(),
|
|
87
|
+
path: routePath,
|
|
88
|
+
functionName: funcName
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
// Pages Router default export — handles all methods
|
|
92
|
+
routes.push({
|
|
93
|
+
method: 'ALL',
|
|
94
|
+
path: routePath,
|
|
95
|
+
functionName: funcName
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// export function GET/POST/... or export const GET = ... — App Router pattern
|
|
102
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
103
|
+
const decl = node.declaration;
|
|
104
|
+
|
|
105
|
+
if (decl.type === 'FunctionDeclaration' && decl.id && HTTP_METHODS.has(decl.id.name.toUpperCase())) {
|
|
106
|
+
routes.push({
|
|
107
|
+
method: decl.id.name.toUpperCase(),
|
|
108
|
+
path: routePath,
|
|
109
|
+
functionName: decl.id.name
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (decl.type === 'VariableDeclaration') {
|
|
114
|
+
for (const vDecl of decl.declarations) {
|
|
115
|
+
if (vDecl.id && vDecl.id.name && HTTP_METHODS.has(vDecl.id.name.toUpperCase())) {
|
|
116
|
+
routes.push({
|
|
117
|
+
method: vDecl.id.name.toUpperCase(),
|
|
118
|
+
path: routePath,
|
|
119
|
+
functionName: vDecl.id.name
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return routes;
|
|
128
|
+
}
|
|
129
|
+
|
|
35
130
|
// ---------------------------------------------------------------------------
|
|
36
131
|
// AST traversal helper
|
|
37
132
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detects the tech stack from file contents and package manifests.
|
|
6
|
+
* Scans only files Carto already watches — no new file scanning.
|
|
7
|
+
* Returns deduplicated array of stack items, max 6.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Python: import/from patterns → stack name
|
|
11
|
+
const PYTHON_STACK = {
|
|
12
|
+
'fastapi': 'FastAPI',
|
|
13
|
+
'django': 'Django',
|
|
14
|
+
'flask': 'Flask',
|
|
15
|
+
'sqlalchemy': 'SQLAlchemy',
|
|
16
|
+
'boto3': 'AWS',
|
|
17
|
+
'openai': 'OpenAI',
|
|
18
|
+
'anthropic': 'Claude API',
|
|
19
|
+
'google.generativeai': 'Gemini',
|
|
20
|
+
'google-generativeai': 'Gemini',
|
|
21
|
+
'redis': 'Redis',
|
|
22
|
+
'celery': 'Celery',
|
|
23
|
+
'pydantic': 'Pydantic',
|
|
24
|
+
'stripe': 'Stripe',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// JS/TS: import/require patterns → stack name
|
|
28
|
+
const JS_STACK = {
|
|
29
|
+
'next': 'Next.js',
|
|
30
|
+
'express': 'Express',
|
|
31
|
+
'fastify': 'Fastify',
|
|
32
|
+
'react': 'React',
|
|
33
|
+
'@prisma/client': 'Prisma',
|
|
34
|
+
'prisma': 'Prisma',
|
|
35
|
+
'mongoose': 'MongoDB',
|
|
36
|
+
'sequelize': 'Sequelize',
|
|
37
|
+
'drizzle-orm': 'Drizzle',
|
|
38
|
+
'drizzle': 'Drizzle',
|
|
39
|
+
'redis': 'Redis',
|
|
40
|
+
'ioredis': 'Redis',
|
|
41
|
+
'openai': 'OpenAI',
|
|
42
|
+
'@anthropic-ai/sdk': 'Claude API',
|
|
43
|
+
'anthropic': 'Claude API',
|
|
44
|
+
'stripe': 'Stripe',
|
|
45
|
+
'firebase': 'Firebase',
|
|
46
|
+
'firebase-admin': 'Firebase',
|
|
47
|
+
'@supabase/supabase-js': 'Supabase',
|
|
48
|
+
'supabase': 'Supabase',
|
|
49
|
+
'@clerk/nextjs': 'Clerk',
|
|
50
|
+
'@clerk/clerk-sdk-node': 'Clerk',
|
|
51
|
+
'clerk': 'Clerk',
|
|
52
|
+
'next-auth': 'NextAuth',
|
|
53
|
+
'nextauth': 'NextAuth',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const MAX_STACK_ITEMS = 6;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* detectStackFromContent(content, filename) → Array<string>
|
|
60
|
+
* Scans a single file's content for known stack imports.
|
|
61
|
+
*/
|
|
62
|
+
function detectStackFromContent(content, filename) {
|
|
63
|
+
const ext = path.extname(filename).toLowerCase();
|
|
64
|
+
const detected = new Set();
|
|
65
|
+
|
|
66
|
+
if (ext === '.py') {
|
|
67
|
+
// Python imports: import X, from X import Y
|
|
68
|
+
for (const pkg of Object.keys(PYTHON_STACK)) {
|
|
69
|
+
// Match: import fastapi, from fastapi import ..., from fastapi.xxx import ...
|
|
70
|
+
const pattern = new RegExp(`(?:^|\\n)\\s*(?:import\\s+${escapeRegex(pkg)}|from\\s+${escapeRegex(pkg)}(?:\\.|\\s))`, 'm');
|
|
71
|
+
if (pattern.test(content)) {
|
|
72
|
+
detected.add(PYTHON_STACK[pkg]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
|
|
76
|
+
// JS/TS imports: import ... from 'pkg', require('pkg')
|
|
77
|
+
for (const pkg of Object.keys(JS_STACK)) {
|
|
78
|
+
const escaped = escapeRegex(pkg);
|
|
79
|
+
// import ... from 'pkg' or import 'pkg'
|
|
80
|
+
const importPattern = new RegExp(`(?:from|import)\\s+['"]${escaped}(?:[/'"])`);
|
|
81
|
+
// require('pkg')
|
|
82
|
+
const requirePattern = new RegExp(`require\\s*\\(\\s*['"]${escaped}(?:[/'"])`);
|
|
83
|
+
if (importPattern.test(content) || requirePattern.test(content)) {
|
|
84
|
+
detected.add(JS_STACK[pkg]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return [...detected];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* detectStackFromPackageJson(projectRoot) → Array<string>
|
|
94
|
+
* Reads package.json dependencies for known stack packages.
|
|
95
|
+
*/
|
|
96
|
+
function detectStackFromPackageJson(projectRoot) {
|
|
97
|
+
const detected = new Set();
|
|
98
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
102
|
+
const allDeps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
103
|
+
|
|
104
|
+
for (const dep of Object.keys(allDeps)) {
|
|
105
|
+
const depLower = dep.toLowerCase();
|
|
106
|
+
if (JS_STACK[depLower]) {
|
|
107
|
+
detected.add(JS_STACK[depLower]);
|
|
108
|
+
}
|
|
109
|
+
// Also check without scope for scoped packages
|
|
110
|
+
if (JS_STACK[dep]) {
|
|
111
|
+
detected.add(JS_STACK[dep]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// No package.json or can't read — that's fine
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [...detected];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* detectStackFromRequirements(projectRoot) → Array<string>
|
|
123
|
+
* Reads requirements.txt for known Python packages.
|
|
124
|
+
*/
|
|
125
|
+
function detectStackFromRequirements(projectRoot) {
|
|
126
|
+
const detected = new Set();
|
|
127
|
+
const reqPath = path.join(projectRoot, 'requirements.txt');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const content = fs.readFileSync(reqPath, 'utf-8').toLowerCase();
|
|
131
|
+
for (const pkg of Object.keys(PYTHON_STACK)) {
|
|
132
|
+
// Match package name at start of line, possibly followed by ==, >=, etc.
|
|
133
|
+
const pattern = new RegExp(`^${escapeRegex(pkg.replace('.', '-'))}\\s*[=><~!\\[]`, 'm');
|
|
134
|
+
const simplePattern = new RegExp(`^${escapeRegex(pkg.replace('.', '-'))}\\s*$`, 'm');
|
|
135
|
+
if (pattern.test(content) || simplePattern.test(content)) {
|
|
136
|
+
detected.add(PYTHON_STACK[pkg]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// No requirements.txt — that's fine
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [...detected];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* buildStackLine(fileContents, projectRoot) → string
|
|
148
|
+
* Aggregates stack detection across all watched files + manifests.
|
|
149
|
+
* Returns a comma-separated string of max 6 items, or empty string.
|
|
150
|
+
*/
|
|
151
|
+
function buildStackLine(fileContents, projectRoot) {
|
|
152
|
+
const allDetected = new Set();
|
|
153
|
+
|
|
154
|
+
// Scan manifests
|
|
155
|
+
for (const item of detectStackFromPackageJson(projectRoot)) allDetected.add(item);
|
|
156
|
+
for (const item of detectStackFromRequirements(projectRoot)) allDetected.add(item);
|
|
157
|
+
|
|
158
|
+
// Scan file contents
|
|
159
|
+
for (const { filePath, content } of fileContents) {
|
|
160
|
+
const items = detectStackFromContent(content, filePath);
|
|
161
|
+
for (const item of items) allDetected.add(item);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sorted = [...allDetected].sort();
|
|
165
|
+
return sorted.slice(0, MAX_STACK_ITEMS);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function escapeRegex(str) {
|
|
169
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { buildStackLine, detectStackFromContent };
|
package/src/sync.js
CHANGED
|
@@ -6,6 +6,7 @@ const { mergeIntoAgentsMd } = require('./agents/merger');
|
|
|
6
6
|
const { inferResponsibility } = require('./extractors/filemap');
|
|
7
7
|
const { validateExtracted } = require('./agents/validator');
|
|
8
8
|
const { buildImportGraph } = require('./extractors/imports');
|
|
9
|
+
const { buildStackLine } = require('./extractors/stack');
|
|
9
10
|
|
|
10
11
|
const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto', 'AGENTS.md']);
|
|
11
12
|
|
|
@@ -71,7 +72,7 @@ async function runFullSync(config) {
|
|
|
71
72
|
// Routes per file for file map
|
|
72
73
|
const routeCountMap = {};
|
|
73
74
|
// Env vars: { varName: Set([filename, ...]) }
|
|
74
|
-
const envVarMap =
|
|
75
|
+
const envVarMap = new Map();
|
|
75
76
|
// DB tables: [{ tableName, modelName, file }]
|
|
76
77
|
const dbTableList = [];
|
|
77
78
|
|
|
@@ -89,6 +90,7 @@ async function runFullSync(config) {
|
|
|
89
90
|
if (!content) continue;
|
|
90
91
|
|
|
91
92
|
const basename = path.basename(filePath);
|
|
93
|
+
const relPath = path.relative(config.projectRoot, filePath);
|
|
92
94
|
const plugin = getPluginForFile(plugins, filePath);
|
|
93
95
|
|
|
94
96
|
if (!plugin) {
|
|
@@ -96,7 +98,7 @@ async function runFullSync(config) {
|
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
const result = plugin.extract(content,
|
|
101
|
+
const result = plugin.extract(content, relPath);
|
|
100
102
|
|
|
101
103
|
// Routes
|
|
102
104
|
allRoutes = allRoutes.concat(result.routes);
|
|
@@ -112,8 +114,8 @@ async function runFullSync(config) {
|
|
|
112
114
|
|
|
113
115
|
// Env vars
|
|
114
116
|
for (const varName of result.envVars) {
|
|
115
|
-
if (!envVarMap
|
|
116
|
-
envVarMap
|
|
117
|
+
if (!envVarMap.has(varName)) envVarMap.set(varName, new Set());
|
|
118
|
+
envVarMap.get(varName).add(basename);
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// DB tables
|
|
@@ -181,22 +183,47 @@ async function runFullSync(config) {
|
|
|
181
183
|
}
|
|
182
184
|
const importGraph = buildImportGraph(fileContentsForImports, config.projectRoot);
|
|
183
185
|
|
|
186
|
+
// Detect tech stack from watched files + manifests
|
|
187
|
+
const stackItems = buildStackLine(fileContentsForImports, config.projectRoot);
|
|
188
|
+
|
|
189
|
+
// Compute entry points and high impact files from import graph
|
|
190
|
+
const allValues = new Set();
|
|
191
|
+
for (const deps of Object.values(importGraph)) {
|
|
192
|
+
for (const dep of deps) allValues.add(dep);
|
|
193
|
+
}
|
|
194
|
+
// Entry points: files that import 3+ others but nothing imports them
|
|
195
|
+
const entryPoints = Object.keys(importGraph)
|
|
196
|
+
.filter(f => !allValues.has(f) && importGraph[f].length >= 3)
|
|
197
|
+
.sort();
|
|
198
|
+
// High impact: files imported by 3+ others, sorted descending by count
|
|
199
|
+
const depCount = {};
|
|
200
|
+
for (const deps of Object.values(importGraph)) {
|
|
201
|
+
for (const dep of deps) {
|
|
202
|
+
depCount[dep] = (depCount[dep] || 0) + 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const highImpact = Object.entries(depCount)
|
|
206
|
+
.filter(([, count]) => count >= 2)
|
|
207
|
+
.sort((a, b) => b[1] - a[1])
|
|
208
|
+
.map(([file, count]) => ({ file, count }));
|
|
209
|
+
|
|
184
210
|
// Build file map
|
|
185
211
|
const fileMap = [];
|
|
186
212
|
for (const filePath of allCodeFiles) {
|
|
187
213
|
const basename = path.basename(filePath);
|
|
214
|
+
const relPath = path.relative(config.projectRoot, filePath);
|
|
188
215
|
const funcCount = (functionsMap[basename] || []).length;
|
|
189
216
|
const routeCount = routeCountMap[filePath] || 0;
|
|
190
217
|
const responsibility = inferResponsibility(basename, funcCount, routeCount);
|
|
191
218
|
if (responsibility && responsibility !== '\u2014') {
|
|
192
|
-
fileMap.push({ file:
|
|
219
|
+
fileMap.push({ file: relPath, responsibility });
|
|
193
220
|
}
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
// Aggregate env vars into sorted array
|
|
197
|
-
const envVars =
|
|
224
|
+
const envVars = [...envVarMap.keys()]
|
|
198
225
|
.sort()
|
|
199
|
-
.map(name => ({ name, files: [...envVarMap
|
|
226
|
+
.map(name => ({ name, files: [...envVarMap.get(name)].sort() }));
|
|
200
227
|
|
|
201
228
|
// Scan project structure
|
|
202
229
|
const structure = await scanStructure(config.projectRoot);
|
|
@@ -220,7 +247,10 @@ async function runFullSync(config) {
|
|
|
220
247
|
functions: validated.functions,
|
|
221
248
|
dbTables: validated.dbTables,
|
|
222
249
|
envVars: validated.envVars,
|
|
223
|
-
importGraph
|
|
250
|
+
importGraph,
|
|
251
|
+
stackItems,
|
|
252
|
+
entryPoints,
|
|
253
|
+
highImpact
|
|
224
254
|
});
|
|
225
255
|
|
|
226
256
|
mergeIntoAgentsMd(config.output, autoContent);
|