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 +16 -0
- package/package.json +35 -0
- package/src/agents/formatter.js +150 -0
- package/src/agents/merger.js +70 -0
- package/src/cli/index.js +43 -0
- package/src/cli/init.js +80 -0
- package/src/cli/sync.js +27 -0
- package/src/cli/watch.js +52 -0
- package/src/detector/files.js +92 -0
- package/src/detector/framework.js +117 -0
- package/src/extractors/dbtables.js +80 -0
- package/src/extractors/envvars.js +40 -0
- package/src/extractors/filemap.js +36 -0
- package/src/extractors/frontend.js +43 -0
- package/src/extractors/functions.js +69 -0
- package/src/extractors/languages/html.js +15 -0
- package/src/extractors/languages/javascript.js +376 -0
- package/src/extractors/languages/prisma.js +80 -0
- package/src/extractors/languages/python.js +26 -0
- package/src/extractors/languages/typescript.js +235 -0
- package/src/extractors/loader.js +67 -0
- package/src/extractors/models.js +40 -0
- package/src/extractors/routes.js +66 -0
- package/src/security/ignore.js +66 -0
- package/src/sync.js +180 -0
- package/src/watcher/watch.js +46 -0
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 };
|
package/src/cli/index.js
ADDED
|
@@ -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
|
+
}
|
package/src/cli/init.js
ADDED
|
@@ -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 };
|
package/src/cli/sync.js
ADDED
|
@@ -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 };
|
package/src/cli/watch.js
ADDED
|
@@ -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 };
|