carto-md 2.0.5 → 2.0.7
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/CONTRIBUTING.md +5 -5
- package/README.md +174 -138
- package/docs/screenshots/claude-code-supabase.png +0 -0
- package/index.js +17 -7
- package/package.json +2 -2
- package/src/acp/agent.js +6 -13
- package/src/acp/providers/index.js +4 -12
- package/src/acp/session.js +9 -11
- package/src/cli/impact.js +11 -6
- package/src/cli/init.js +16 -19
- package/src/cli/serve.js +11 -12
- package/src/cli/sync.js +2 -14
- package/src/extractors/imports.js +5 -1
- package/src/mcp/server-v2.js +51 -5
- package/src/security/ignore.js +56 -1
- package/src/store/path-utils.js +50 -0
- package/src/store/sqlite-store.js +66 -11
- package/src/store/store-adapter.js +169 -0
- package/src/store/sync-v2.js +37 -4
- package/src/cache/file-hash.js +0 -84
- package/src/cache/graph-cache.js +0 -77
- package/src/detector/files.js +0 -289
- package/src/engine/carto.js +0 -606
- package/src/engine/incremental.js +0 -149
- package/src/mcp/server.js +0 -431
- package/src/sync.js +0 -317
package/src/store/sync-v2.js
CHANGED
|
@@ -24,7 +24,10 @@ const IGNORE_DIRS = new Set([
|
|
|
24
24
|
'dist', '.next', '.turbo', 'build', 'coverage', '.carto',
|
|
25
25
|
'out', '.cache', 'generated', '__generated__',
|
|
26
26
|
'storybook-static', 'public', 'static',
|
|
27
|
-
'tmp-bench', 'vendor', 'third_party', '.yarn'
|
|
27
|
+
'tmp-bench', 'vendor', 'third_party', '.yarn',
|
|
28
|
+
// Test directories — ported from V1 detector/files.js
|
|
29
|
+
'test', 'tests', '__tests__', 'e2e', 'playwright',
|
|
30
|
+
'cypress', 'fixtures', 'mocks', '__mocks__'
|
|
28
31
|
]);
|
|
29
32
|
|
|
30
33
|
const CODE_EXTS = new Set([
|
|
@@ -34,6 +37,36 @@ const CODE_EXTS = new Set([
|
|
|
34
37
|
'.swift', '.kt'
|
|
35
38
|
]);
|
|
36
39
|
|
|
40
|
+
const JS_LIKE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* isTestFile(relPath) → true if the file is a test/spec/stories file
|
|
44
|
+
* Ported from V1 detector/files.js exclusion patterns.
|
|
45
|
+
* R: test_*, test-*, *_test.r (case-insensitive)
|
|
46
|
+
* Python: test_*.py, *_test.py
|
|
47
|
+
* JS/TS: *.test.*, *.spec.*, *.stories.*
|
|
48
|
+
* Path-based directory exclusions (test/, tests/, __tests__/, etc.)
|
|
49
|
+
* are handled by IGNORE_DIRS during the walk — see above.
|
|
50
|
+
*/
|
|
51
|
+
function isTestFile(relPath) {
|
|
52
|
+
const base = path.basename(relPath);
|
|
53
|
+
const lbase = base.toLowerCase();
|
|
54
|
+
const ext = path.extname(base).toLowerCase();
|
|
55
|
+
|
|
56
|
+
if (ext === '.r') {
|
|
57
|
+
return lbase.startsWith('test_') || lbase.startsWith('test-') || lbase.endsWith('_test.r');
|
|
58
|
+
}
|
|
59
|
+
if (ext === '.py') {
|
|
60
|
+
return lbase.startsWith('test_') || lbase.endsWith('_test.py');
|
|
61
|
+
}
|
|
62
|
+
if (JS_LIKE_EXTS.has(ext)) {
|
|
63
|
+
if (lbase.includes('.test.') || lbase.includes('.spec.') || lbase.includes('.stories.')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
/**
|
|
38
71
|
* discoverFiles(projectRoot)
|
|
39
72
|
* Recursively walks the project tree. No file cap. Respects ignore dirs + .cartoignore.
|
|
@@ -55,7 +88,7 @@ function discoverFiles(projectRoot) {
|
|
|
55
88
|
walk(full);
|
|
56
89
|
} else if (entry.isFile()) {
|
|
57
90
|
const ext = path.extname(entry.name).toLowerCase();
|
|
58
|
-
if (CODE_EXTS.has(ext) && !isIgnored(rel)) {
|
|
91
|
+
if (CODE_EXTS.has(ext) && !isIgnored(rel) && !isTestFile(rel)) {
|
|
59
92
|
results.push(rel);
|
|
60
93
|
}
|
|
61
94
|
}
|
|
@@ -419,8 +452,8 @@ async function runSyncV2(config) {
|
|
|
419
452
|
}
|
|
420
453
|
}
|
|
421
454
|
|
|
422
|
-
// 8. Generate outputs (only if files changed)
|
|
423
|
-
if (toProcess.length > 0) {
|
|
455
|
+
// 8. Generate outputs (only if files changed AND output not suppressed)
|
|
456
|
+
if (toProcess.length > 0 && config.output !== null && config.output !== false) {
|
|
424
457
|
await generateOutputs(store, config, projectRoot, store.getImportGraph());
|
|
425
458
|
}
|
|
426
459
|
|
package/src/cache/file-hash.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
|
|
7
|
-
function getHashPath(projectRoot) {
|
|
8
|
-
return path.join(projectRoot, '.carto', 'hashes.json');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function loadHashes(projectRoot) {
|
|
12
|
-
try {
|
|
13
|
-
const raw = fs.readFileSync(getHashPath(projectRoot), 'utf-8');
|
|
14
|
-
return JSON.parse(raw);
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function saveHashes(projectRoot, hashes) {
|
|
21
|
-
const hashPath = getHashPath(projectRoot);
|
|
22
|
-
const tmp = hashPath + '.tmp';
|
|
23
|
-
try {
|
|
24
|
-
fs.writeFileSync(tmp, JSON.stringify(hashes, null, 2), 'utf-8');
|
|
25
|
-
fs.renameSync(tmp, hashPath);
|
|
26
|
-
} catch {}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function hashContent(content) {
|
|
30
|
-
return crypto.createHash('sha1').update(content).digest('hex');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* computeChangedFiles(filePaths, storedHashes, projectRoot)
|
|
35
|
-
* Returns { changed: string[], unchanged: string[], hashes: object }
|
|
36
|
-
* changed = files whose content hash differs from stored
|
|
37
|
-
* unchanged = files whose hash matches — can skip re-parsing
|
|
38
|
-
*/
|
|
39
|
-
function computeChangedFiles(filePaths, storedHashes, projectRoot) {
|
|
40
|
-
const changed = [];
|
|
41
|
-
const unchanged = [];
|
|
42
|
-
const newHashes = { ...storedHashes };
|
|
43
|
-
|
|
44
|
-
for (const filePath of filePaths) {
|
|
45
|
-
const relPath = path.relative(projectRoot, filePath);
|
|
46
|
-
let content;
|
|
47
|
-
try {
|
|
48
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
49
|
-
} catch {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const hash = hashContent(content);
|
|
53
|
-
if (storedHashes[relPath] === hash) {
|
|
54
|
-
unchanged.push(filePath);
|
|
55
|
-
} else {
|
|
56
|
-
changed.push(filePath);
|
|
57
|
-
newHashes[relPath] = hash;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { changed, unchanged, hashes: newHashes };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* updateFileHash(projectRoot, relPath, content)
|
|
66
|
-
* Updates the hash for a single file after incremental re-index.
|
|
67
|
-
*/
|
|
68
|
-
function updateFileHash(projectRoot, relPath, content) {
|
|
69
|
-
const hashes = loadHashes(projectRoot);
|
|
70
|
-
hashes[relPath] = hashContent(content);
|
|
71
|
-
saveHashes(projectRoot, hashes);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* removeFileHash(projectRoot, relPath)
|
|
76
|
-
* Removes hash entry when a file is deleted.
|
|
77
|
-
*/
|
|
78
|
-
function removeFileHash(projectRoot, relPath) {
|
|
79
|
-
const hashes = loadHashes(projectRoot);
|
|
80
|
-
delete hashes[relPath];
|
|
81
|
-
saveHashes(projectRoot, hashes);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
module.exports = { loadHashes, saveHashes, hashContent, computeChangedFiles, updateFileHash, removeFileHash };
|
package/src/cache/graph-cache.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
function getCachePath(projectRoot) {
|
|
7
|
-
return path.join(projectRoot, '.carto', 'graph-cache.json');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* loadGraphCache(projectRoot)
|
|
12
|
-
* Returns the full persisted graph or null if not found / corrupt.
|
|
13
|
-
*
|
|
14
|
-
* Cache shape:
|
|
15
|
-
* {
|
|
16
|
-
* version: '2',
|
|
17
|
-
* generated: ISO string,
|
|
18
|
-
* fileData: {
|
|
19
|
-
* [relPath]: { routes, models, functions, envVars, dbTables, fetches, storageKeys, imports }
|
|
20
|
-
* },
|
|
21
|
-
* importGraph: { [relPath]: [relPath, ...] },
|
|
22
|
-
* routesByFile: { [relPath]: ['METHOD /path', ...] },
|
|
23
|
-
* domains: { [domain]: { files, routes, models, functions, envVars, dbTables } },
|
|
24
|
-
* highImpact: [{ file, dependents }],
|
|
25
|
-
* entryPoints: [relPath, ...],
|
|
26
|
-
* stack: [...],
|
|
27
|
-
* meta: { totalFiles, totalRoutes, totalImportEdges, lastIndexed, indexDuration }
|
|
28
|
-
* }
|
|
29
|
-
*/
|
|
30
|
-
function loadGraphCache(projectRoot) {
|
|
31
|
-
try {
|
|
32
|
-
const raw = fs.readFileSync(getCachePath(projectRoot), 'utf-8');
|
|
33
|
-
const parsed = JSON.parse(raw);
|
|
34
|
-
if (parsed.version !== '2') return null;
|
|
35
|
-
return parsed;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function saveGraphCache(projectRoot, cache) {
|
|
42
|
-
const cachePath = getCachePath(projectRoot);
|
|
43
|
-
const tmp = cachePath + '.tmp';
|
|
44
|
-
try {
|
|
45
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
46
|
-
fs.writeFileSync(tmp, JSON.stringify(cache, null, 2) + '\n', 'utf-8');
|
|
47
|
-
fs.renameSync(tmp, cachePath);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.warn(`[CARTO] Warning: Could not save graph cache — ${err.message}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* buildEmptyCache() — starting point for a fresh index
|
|
55
|
-
*/
|
|
56
|
-
function buildEmptyCache() {
|
|
57
|
-
return {
|
|
58
|
-
version: '2',
|
|
59
|
-
generated: new Date().toISOString(),
|
|
60
|
-
fileData: {},
|
|
61
|
-
importGraph: {},
|
|
62
|
-
routesByFile: {},
|
|
63
|
-
domains: {},
|
|
64
|
-
highImpact: [],
|
|
65
|
-
entryPoints: [],
|
|
66
|
-
stack: [],
|
|
67
|
-
meta: {
|
|
68
|
-
totalFiles: 0,
|
|
69
|
-
totalRoutes: 0,
|
|
70
|
-
totalImportEdges: 0,
|
|
71
|
-
lastIndexed: null,
|
|
72
|
-
indexDuration: 0
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
module.exports = { loadGraphCache, saveGraphCache, buildEmptyCache, getCachePath };
|
package/src/detector/files.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
const MAX_ROUTE_FILES = 150; // all route files up to this — routes are never crowded out
|
|
5
|
-
const MAX_MODEL_FILES = 50; // all model/schema files up to this
|
|
6
|
-
const MAX_UTILITY_FILES = 100; // top N utilities by import count
|
|
7
|
-
|
|
8
|
-
const PYTHON_IGNORE = new Set(['__pycache__', '.venv', 'venv', 'migrations', 'node_modules', '.git', '.carto']);
|
|
9
|
-
const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto', '.next', '.turbo', 'coverage', 'out', '.cache', 'generated', '__generated__', 'storybook-static', 'public', 'static', 'playwright', 'e2e', '__tests__', 'fixtures', 'mocks', '__mocks__', 'cypress']);
|
|
10
|
-
const HTML_IGNORE = new Set(['node_modules', '.git', '.carto']);
|
|
11
|
-
const R_IGNORE = new Set(['.Rhistory', '.RData', 'packrat', 'renv', 'node_modules', '.git', '__pycache__', '.carto']);
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) → { routeFiles, modelFiles, frontendFiles }
|
|
15
|
-
*/
|
|
16
|
-
function discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) {
|
|
17
|
-
const ignoreFn = isIgnored || (() => false);
|
|
18
|
-
|
|
19
|
-
const primary = discoverForFramework(projectRoot, framework, ignoreFn);
|
|
20
|
-
|
|
21
|
-
if (secondaryFramework) {
|
|
22
|
-
const secondary = discoverForFramework(projectRoot, secondaryFramework, ignoreFn);
|
|
23
|
-
const routeFiles = [...new Set([...primary.routeFiles, ...secondary.routeFiles])];
|
|
24
|
-
const modelFiles = [...new Set([...primary.modelFiles, ...secondary.modelFiles])];
|
|
25
|
-
const frontendFiles = [...new Set([...primary.frontendFiles, ...secondary.frontendFiles])];
|
|
26
|
-
return { routeFiles, modelFiles, frontendFiles };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return primary;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function discoverForFramework(projectRoot, framework, ignoreFn) {
|
|
33
|
-
if (['fastapi', 'django', 'flask', 'python-generic'].includes(framework)) {
|
|
34
|
-
const pyFiles = findFilesRecursive(projectRoot, ['.py'], PYTHON_IGNORE, ignoreFn)
|
|
35
|
-
.filter(f => {
|
|
36
|
-
const base = path.basename(f);
|
|
37
|
-
return !base.startsWith('test_') && !base.endsWith('_test.py');
|
|
38
|
-
});
|
|
39
|
-
const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
|
|
40
|
-
|
|
41
|
-
if (pyFiles.length <= MAX_ROUTE_FILES + MAX_MODEL_FILES + MAX_UTILITY_FILES) {
|
|
42
|
-
return { routeFiles: pyFiles, modelFiles: pyFiles, frontendFiles: htmlFiles };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return smartSelect(pyFiles, htmlFiles, projectRoot);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (['express', 'nextjs', 'react', 'node-generic'].includes(framework)) {
|
|
49
|
-
const jsFiles = findFilesRecursive(projectRoot, ['.js', '.ts', '.jsx', '.tsx', '.prisma'], JS_IGNORE, ignoreFn)
|
|
50
|
-
.filter(f => {
|
|
51
|
-
const base = path.basename(f);
|
|
52
|
-
const rel = f.toLowerCase().replace(/\\/g, '/');
|
|
53
|
-
return !base.includes('.test.') &&
|
|
54
|
-
!base.includes('.spec.') &&
|
|
55
|
-
!base.includes('.stories.') &&
|
|
56
|
-
!rel.includes('/test/') &&
|
|
57
|
-
!rel.includes('/tests/') &&
|
|
58
|
-
!rel.includes('/e2e/') &&
|
|
59
|
-
!rel.includes('/playwright/') &&
|
|
60
|
-
!rel.includes('/__tests__/') &&
|
|
61
|
-
!rel.includes('/fixtures/');
|
|
62
|
-
});
|
|
63
|
-
const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
|
|
64
|
-
|
|
65
|
-
if (jsFiles.length <= MAX_ROUTE_FILES + MAX_MODEL_FILES + MAX_UTILITY_FILES) {
|
|
66
|
-
return { routeFiles: jsFiles, modelFiles: jsFiles, frontendFiles: htmlFiles };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return smartSelect(jsFiles, htmlFiles, projectRoot);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (['plumber', 'shiny', 'r-generic'].includes(framework)) {
|
|
73
|
-
const rFiles = findFilesRecursive(projectRoot, ['.r'], R_IGNORE, ignoreFn)
|
|
74
|
-
.filter(f => {
|
|
75
|
-
const lbase = path.basename(f).toLowerCase();
|
|
76
|
-
return !lbase.startsWith('test_') && !lbase.startsWith('test-') && !lbase.endsWith('_test.r');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (rFiles.length <= MAX_ROUTE_FILES + MAX_MODEL_FILES + MAX_UTILITY_FILES) {
|
|
80
|
-
return { routeFiles: rFiles, modelFiles: rFiles, frontendFiles: [] };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return smartSelect(rFiles, [], projectRoot);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Unknown framework
|
|
87
|
-
const allCode = findFilesRecursive(projectRoot, ['.py', '.js', '.ts'], new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.carto']), ignoreFn);
|
|
88
|
-
const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
|
|
89
|
-
|
|
90
|
-
if (allCode.length <= MAX_ROUTE_FILES + MAX_MODEL_FILES + MAX_UTILITY_FILES) {
|
|
91
|
-
return { routeFiles: allCode, modelFiles: allCode, frontendFiles: htmlFiles };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return smartSelect(allCode, htmlFiles, projectRoot);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function smartSelect(allFiles, htmlFiles, projectRoot) {
|
|
98
|
-
console.warn(`[CARTO] Warning: Found ${allFiles.length} files, selecting by tier`);
|
|
99
|
-
|
|
100
|
-
const routeCandidates = [];
|
|
101
|
-
const modelCandidates = [];
|
|
102
|
-
const otherFiles = [];
|
|
103
|
-
|
|
104
|
-
for (const f of allFiles) {
|
|
105
|
-
if (isRouteFile(f)) {
|
|
106
|
-
routeCandidates.push(f);
|
|
107
|
-
} else if (isModelFile(f)) {
|
|
108
|
-
modelCandidates.push(f);
|
|
109
|
-
} else {
|
|
110
|
-
otherFiles.push(f);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Tier 1 — routes: all of them up to MAX_ROUTE_FILES
|
|
115
|
-
// Split tRPC routers and REST routes, tRPC gets up to 50 slots
|
|
116
|
-
const trpcRouters = routeCandidates.filter(f => f.includes('/routers/'));
|
|
117
|
-
const restRoutes = routeCandidates.filter(f => !f.includes('/routers/'));
|
|
118
|
-
|
|
119
|
-
trpcRouters.sort((a, b) => scoreRoute(b) - scoreRoute(a));
|
|
120
|
-
restRoutes.sort((a, b) => scoreRoute(b) - scoreRoute(a));
|
|
121
|
-
|
|
122
|
-
const trpcSelected = trpcRouters.slice(0, Math.min(trpcRouters.length, 50));
|
|
123
|
-
const restSelected = restRoutes.slice(0, Math.min(restRoutes.length, MAX_ROUTE_FILES - trpcSelected.length));
|
|
124
|
-
const selectedRoutes = [...trpcSelected, ...restSelected];
|
|
125
|
-
|
|
126
|
-
// Tier 2 — models: all of them up to MAX_MODEL_FILES
|
|
127
|
-
modelCandidates.sort((a, b) => scoreModel(b) - scoreModel(a));
|
|
128
|
-
const selectedModels = modelCandidates.slice(0, MAX_MODEL_FILES);
|
|
129
|
-
|
|
130
|
-
// Tier 3 — utilities: top N by import count
|
|
131
|
-
const importCounts = countImportReferences(allFiles, projectRoot);
|
|
132
|
-
otherFiles.sort((a, b) => {
|
|
133
|
-
const relA = path.relative(projectRoot, a);
|
|
134
|
-
const relB = path.relative(projectRoot, b);
|
|
135
|
-
return (importCounts[relB] || 0) - (importCounts[relA] || 0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const alreadySelected = new Set([...selectedRoutes, ...selectedModels]);
|
|
139
|
-
const selectedUtilities = otherFiles
|
|
140
|
-
.filter(f => !alreadySelected.has(f))
|
|
141
|
-
.slice(0, MAX_UTILITY_FILES);
|
|
142
|
-
|
|
143
|
-
const allSelected = [...new Set([...selectedRoutes, ...selectedModels, ...selectedUtilities])];
|
|
144
|
-
|
|
145
|
-
console.warn(`[CARTO] Selected: ${selectedRoutes.length} route files, ${selectedModels.length} model files, ${selectedUtilities.length} utility files`);
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
routeFiles: allSelected,
|
|
149
|
-
modelFiles: allSelected,
|
|
150
|
-
frontendFiles: htmlFiles.slice(0, 10)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function isRouteFile(filePath) {
|
|
155
|
-
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
156
|
-
const base = path.basename(p);
|
|
157
|
-
if (base === 'route.ts' || base === 'route.js' || base === 'routes.ts' || base === 'routes.js') return true;
|
|
158
|
-
if (p.includes('/api/') || p.includes('/routes/') || p.includes('/route/')) return true;
|
|
159
|
-
if (p.includes('/routers/')) return true;
|
|
160
|
-
if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return true;
|
|
161
|
-
if (p.includes('/pages/api/')) return true;
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isModelFile(filePath) {
|
|
166
|
-
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
167
|
-
const base = path.basename(p);
|
|
168
|
-
if (base === 'schema.prisma' || base.endsWith('.prisma')) return true;
|
|
169
|
-
if (base.startsWith('models.') || base.includes('.model.') || base.includes('.models.')) return true;
|
|
170
|
-
if (p.includes('/models/') || p.includes('/schemas/') || p.includes('/schema/')) return true;
|
|
171
|
-
if (base.includes('entity') || base.includes('schema')) return true;
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function scoreRoute(filePath) {
|
|
176
|
-
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
177
|
-
const base = path.basename(p);
|
|
178
|
-
if (base === 'main.py' || base === 'app.py' || base === 'server.ts' || base === 'server.js') return 10;
|
|
179
|
-
if (p.includes('/app/api/')) return 9;
|
|
180
|
-
if (p.includes('/pages/api/')) return 8;
|
|
181
|
-
if (p.includes('/routers/')) return 9;
|
|
182
|
-
if (p.includes('/routes/') || p.includes('/api/')) return 7;
|
|
183
|
-
return 5;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function scoreModel(filePath) {
|
|
187
|
-
const p = filePath.toLowerCase().replace(/\\/g, '/');
|
|
188
|
-
const base = path.basename(p);
|
|
189
|
-
if (base === 'schema.prisma') return 10;
|
|
190
|
-
if (base.startsWith('models.')) return 9;
|
|
191
|
-
if (p.includes('/models/')) return 8;
|
|
192
|
-
if (base.includes('.model.')) return 7;
|
|
193
|
-
return 5;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Quick scan of all files for import/require statements.
|
|
198
|
-
* Returns { 'relative/path': count } of how many files import each path.
|
|
199
|
-
*/
|
|
200
|
-
function countImportReferences(allFiles, projectRoot) {
|
|
201
|
-
const counts = {};
|
|
202
|
-
|
|
203
|
-
// Build a set of known relative paths for matching
|
|
204
|
-
const knownPaths = new Set();
|
|
205
|
-
for (const f of allFiles) {
|
|
206
|
-
knownPaths.add(path.relative(projectRoot, f));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Quick regex scan — don't parse AST, just count references
|
|
210
|
-
const importPattern = /(?:from|import)\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
211
|
-
|
|
212
|
-
for (const filePath of allFiles) {
|
|
213
|
-
let content;
|
|
214
|
-
try {
|
|
215
|
-
content = fs.readFileSync(filePath, 'utf-8');
|
|
216
|
-
} catch {
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const fileDir = path.dirname(filePath);
|
|
221
|
-
let match;
|
|
222
|
-
importPattern.lastIndex = 0;
|
|
223
|
-
while ((match = importPattern.exec(content)) !== null) {
|
|
224
|
-
const importPath = match[1] || match[2];
|
|
225
|
-
if (!importPath || !importPath.startsWith('.')) continue;
|
|
226
|
-
|
|
227
|
-
// Try to resolve to a known file
|
|
228
|
-
const resolved = tryResolve(importPath, fileDir, projectRoot, knownPaths);
|
|
229
|
-
if (resolved) {
|
|
230
|
-
counts[resolved] = (counts[resolved] || 0) + 1;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return counts;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Try to resolve a relative import to a known file path.
|
|
240
|
-
*/
|
|
241
|
-
function tryResolve(importPath, fileDir, projectRoot, knownPaths) {
|
|
242
|
-
const base = path.resolve(fileDir, importPath);
|
|
243
|
-
const rel = path.relative(projectRoot, base);
|
|
244
|
-
|
|
245
|
-
// Try exact
|
|
246
|
-
if (knownPaths.has(rel)) return rel;
|
|
247
|
-
|
|
248
|
-
// Try extensions
|
|
249
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py'];
|
|
250
|
-
for (const ext of extensions) {
|
|
251
|
-
const withExt = rel + ext;
|
|
252
|
-
if (knownPaths.has(withExt)) return withExt;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Try index files
|
|
256
|
-
for (const ext of extensions) {
|
|
257
|
-
const indexFile = path.join(rel, 'index' + ext);
|
|
258
|
-
if (knownPaths.has(indexFile)) return indexFile;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function findFilesRecursive(dir, extensions, ignoreDirs, isIgnored, results = []) {
|
|
265
|
-
let items;
|
|
266
|
-
try {
|
|
267
|
-
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
268
|
-
} catch {
|
|
269
|
-
return results;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
for (const item of items) {
|
|
273
|
-
if (ignoreDirs.has(item.name)) continue;
|
|
274
|
-
|
|
275
|
-
const fullPath = path.join(dir, item.name);
|
|
276
|
-
|
|
277
|
-
if (item.isDirectory()) {
|
|
278
|
-
findFilesRecursive(fullPath, extensions, ignoreDirs, isIgnored, results);
|
|
279
|
-
} else if (item.isFile()) {
|
|
280
|
-
const ext = path.extname(item.name).toLowerCase();
|
|
281
|
-
if (extensions.includes(ext) && !isIgnored(fullPath)) {
|
|
282
|
-
results.push(fullPath);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return results;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
module.exports = { discoverFiles };
|