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/src/sync.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadLanguagePlugins, getPluginForFile } = require('./extractors/loader');
|
|
4
|
+
const { formatSections } = require('./agents/formatter');
|
|
5
|
+
const { mergeIntoAgentsMd } = require('./agents/merger');
|
|
6
|
+
const { inferResponsibility } = require('./extractors/filemap');
|
|
7
|
+
|
|
8
|
+
const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.carto']);
|
|
9
|
+
|
|
10
|
+
// Load plugins once at module load
|
|
11
|
+
const plugins = loadLanguagePlugins();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe file read — returns null on error and pushes a warning.
|
|
15
|
+
*/
|
|
16
|
+
async function safeReadFile(filePath, warnings) {
|
|
17
|
+
try {
|
|
18
|
+
return await fs.promises.readFile(filePath, 'utf-8');
|
|
19
|
+
} catch (err) {
|
|
20
|
+
warnings.push(`Could not read ${filePath} — ${err.code || err.message}`);
|
|
21
|
+
console.warn(`[CARTO] Warning: Could not read ${filePath} — skipping`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scans top-level folder structure (1 level deep).
|
|
28
|
+
* Ignores node_modules, .git, __pycache__, etc.
|
|
29
|
+
*/
|
|
30
|
+
async function scanStructure(basePath) {
|
|
31
|
+
const entries = [];
|
|
32
|
+
try {
|
|
33
|
+
const items = await fs.promises.readdir(basePath, { withFileTypes: true });
|
|
34
|
+
for (const item of items) {
|
|
35
|
+
if (IGNORE_DIRS.has(item.name)) continue;
|
|
36
|
+
entries.push({
|
|
37
|
+
name: item.name,
|
|
38
|
+
type: item.isDirectory() ? 'dir' : 'file'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
entries.sort((a, b) => {
|
|
42
|
+
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
|
|
43
|
+
return a.name.localeCompare(b.name);
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`[CARTO] Warning: Could not scan ${basePath} — skipping structure`);
|
|
47
|
+
}
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* runFullSync(config) — reads all source files, extracts data, writes AGENTS.md.
|
|
53
|
+
*/
|
|
54
|
+
async function runFullSync(config) {
|
|
55
|
+
const warnings = [];
|
|
56
|
+
|
|
57
|
+
const allRouteFiles = config.watch.routeFiles || [];
|
|
58
|
+
const allModelFiles = config.watch.modelFiles || [];
|
|
59
|
+
const allFrontendFiles = config.watch.frontendFiles || [];
|
|
60
|
+
|
|
61
|
+
// Aggregate data
|
|
62
|
+
let allRoutes = [];
|
|
63
|
+
let allModels = [];
|
|
64
|
+
let allFetches = [];
|
|
65
|
+
let allStorageKeys = [];
|
|
66
|
+
|
|
67
|
+
// Functions: { filename: [{ name, params, returnType }] }
|
|
68
|
+
const functionsMap = {};
|
|
69
|
+
// Routes per file for file map
|
|
70
|
+
const routeCountMap = {};
|
|
71
|
+
// Env vars: { varName: Set([filename, ...]) }
|
|
72
|
+
const envVarMap = {};
|
|
73
|
+
// DB tables: [{ tableName, modelName, file }]
|
|
74
|
+
const dbTableList = [];
|
|
75
|
+
|
|
76
|
+
// Deduplicate files
|
|
77
|
+
const processedFiles = new Set();
|
|
78
|
+
|
|
79
|
+
// Process all code files (route + model files, deduplicated)
|
|
80
|
+
const allCodeFiles = [...new Set([...allRouteFiles, ...allModelFiles])];
|
|
81
|
+
|
|
82
|
+
for (const filePath of allCodeFiles) {
|
|
83
|
+
if (processedFiles.has(filePath)) continue;
|
|
84
|
+
processedFiles.add(filePath);
|
|
85
|
+
|
|
86
|
+
const content = await safeReadFile(filePath, warnings);
|
|
87
|
+
if (!content) continue;
|
|
88
|
+
|
|
89
|
+
const basename = path.basename(filePath);
|
|
90
|
+
const plugin = getPluginForFile(plugins, filePath);
|
|
91
|
+
|
|
92
|
+
if (!plugin) {
|
|
93
|
+
// No plugin for this file type — skip silently
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = plugin.extract(content, basename);
|
|
98
|
+
|
|
99
|
+
// Routes
|
|
100
|
+
allRoutes = allRoutes.concat(result.routes);
|
|
101
|
+
routeCountMap[filePath] = result.routes.length;
|
|
102
|
+
|
|
103
|
+
// Models
|
|
104
|
+
allModels = allModels.concat(result.models);
|
|
105
|
+
|
|
106
|
+
// Functions
|
|
107
|
+
if (result.functions.length > 0 && basename !== '__init__.py') {
|
|
108
|
+
functionsMap[basename] = result.functions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Env vars
|
|
112
|
+
for (const varName of result.envVars) {
|
|
113
|
+
if (!envVarMap[varName]) envVarMap[varName] = new Set();
|
|
114
|
+
envVarMap[varName].add(basename);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// DB tables
|
|
118
|
+
for (const t of result.dbTables) {
|
|
119
|
+
dbTableList.push({ tableName: t.tableName, modelName: t.modelName, file: basename });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Fetches and storage keys (from JS/HTML plugins)
|
|
123
|
+
allFetches = allFetches.concat(result.fetches);
|
|
124
|
+
allStorageKeys = allStorageKeys.concat(result.storageKeys);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Process frontend files separately (may overlap with code files)
|
|
128
|
+
for (const filePath of allFrontendFiles) {
|
|
129
|
+
if (processedFiles.has(filePath)) continue;
|
|
130
|
+
processedFiles.add(filePath);
|
|
131
|
+
|
|
132
|
+
const content = await safeReadFile(filePath, warnings);
|
|
133
|
+
if (!content) continue;
|
|
134
|
+
|
|
135
|
+
const basename = path.basename(filePath);
|
|
136
|
+
const plugin = getPluginForFile(plugins, filePath);
|
|
137
|
+
|
|
138
|
+
if (!plugin) continue;
|
|
139
|
+
|
|
140
|
+
const result = plugin.extract(content, basename);
|
|
141
|
+
allFetches = allFetches.concat(result.fetches);
|
|
142
|
+
allStorageKeys = allStorageKeys.concat(result.storageKeys);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build file map
|
|
146
|
+
const fileMap = [];
|
|
147
|
+
for (const filePath of allCodeFiles) {
|
|
148
|
+
const basename = path.basename(filePath);
|
|
149
|
+
const funcCount = (functionsMap[basename] || []).length;
|
|
150
|
+
const routeCount = routeCountMap[filePath] || 0;
|
|
151
|
+
const responsibility = inferResponsibility(basename, funcCount, routeCount);
|
|
152
|
+
if (responsibility && responsibility !== '\u2014') {
|
|
153
|
+
fileMap.push({ file: basename, responsibility });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Aggregate env vars into sorted array
|
|
158
|
+
const envVars = Object.keys(envVarMap)
|
|
159
|
+
.sort()
|
|
160
|
+
.map(name => ({ name, files: [...envVarMap[name]].sort() }));
|
|
161
|
+
|
|
162
|
+
// Scan project structure
|
|
163
|
+
const structure = await scanStructure(config.projectRoot);
|
|
164
|
+
|
|
165
|
+
const autoContent = formatSections({
|
|
166
|
+
routes: allRoutes,
|
|
167
|
+
models: allModels,
|
|
168
|
+
frontend: { fetches: allFetches, storageKeys: allStorageKeys },
|
|
169
|
+
structure,
|
|
170
|
+
warnings,
|
|
171
|
+
fileMap,
|
|
172
|
+
functions: functionsMap,
|
|
173
|
+
dbTables: dbTableList,
|
|
174
|
+
envVars
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
mergeIntoAgentsMd(config.output, autoContent);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { runFullSync, safeReadFile, scanStructure };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const chokidar = require('chokidar');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts a file watcher with 300ms debounce.
|
|
5
|
+
* On change, calls onChange(filePath).
|
|
6
|
+
* On error, restarts after 5 seconds.
|
|
7
|
+
*/
|
|
8
|
+
function startWatcher(filePaths, onChange) {
|
|
9
|
+
let debounceTimer = null;
|
|
10
|
+
let lastChangedFile = null;
|
|
11
|
+
|
|
12
|
+
const watcher = chokidar.watch(filePaths, {
|
|
13
|
+
persistent: true,
|
|
14
|
+
ignoreInitial: true,
|
|
15
|
+
awaitWriteFinish: { stabilityThreshold: 100 }
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
watcher.on('change', (filePath) => {
|
|
19
|
+
lastChangedFile = filePath;
|
|
20
|
+
|
|
21
|
+
if (debounceTimer) {
|
|
22
|
+
clearTimeout(debounceTimer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
debounceTimer = setTimeout(async () => {
|
|
26
|
+
debounceTimer = null;
|
|
27
|
+
try {
|
|
28
|
+
await onChange(lastChangedFile);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(`[CARTO] Sync error: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
}, 300);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
watcher.on('error', (error) => {
|
|
36
|
+
console.error(`[CARTO] Watcher error: ${error.message}`);
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
watcher.close();
|
|
39
|
+
startWatcher(filePaths, onChange);
|
|
40
|
+
}, 5000);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return watcher;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { startWatcher };
|