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/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 };