@optave/codegraph 1.1.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/LICENSE +190 -0
- package/README.md +311 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +69 -0
- package/src/builder.js +547 -0
- package/src/cli.js +224 -0
- package/src/config.js +55 -0
- package/src/constants.js +28 -0
- package/src/cycles.js +104 -0
- package/src/db.js +117 -0
- package/src/embedder.js +330 -0
- package/src/export.js +138 -0
- package/src/index.js +39 -0
- package/src/logger.js +20 -0
- package/src/mcp.js +139 -0
- package/src/parser.js +573 -0
- package/src/queries.js +616 -0
- package/src/watcher.js +213 -0
package/src/watcher.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { openDb, initSchema } from './db.js';
|
|
5
|
+
import { createParsers, getParser, extractSymbols, extractHCLSymbols, extractPythonSymbols } from './parser.js';
|
|
6
|
+
import { IGNORE_DIRS, EXTENSIONS, normalizePath } from './constants.js';
|
|
7
|
+
import { resolveImportPath } from './builder.js';
|
|
8
|
+
import { warn, debug, info } from './logger.js';
|
|
9
|
+
|
|
10
|
+
function shouldIgnore(filePath) {
|
|
11
|
+
const parts = filePath.split(path.sep);
|
|
12
|
+
return parts.some(p => IGNORE_DIRS.has(p));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isTrackedExt(filePath) {
|
|
16
|
+
return EXTENSIONS.has(path.extname(filePath));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a single file and update the database incrementally.
|
|
21
|
+
*/
|
|
22
|
+
function updateFile(db, rootDir, filePath, parsers, stmts) {
|
|
23
|
+
const relPath = normalizePath(path.relative(rootDir, filePath));
|
|
24
|
+
|
|
25
|
+
const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
|
|
26
|
+
const oldEdges = stmts.countEdgesForFile.get(relPath)?.c || 0;
|
|
27
|
+
|
|
28
|
+
stmts.deleteEdgesForFile.run(relPath);
|
|
29
|
+
stmts.deleteNodes.run(relPath);
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return { file: relPath, nodesAdded: 0, nodesRemoved: oldNodes, edgesAdded: 0, deleted: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parser = getParser(parsers, filePath);
|
|
36
|
+
if (!parser) return null;
|
|
37
|
+
|
|
38
|
+
let code;
|
|
39
|
+
try { code = fs.readFileSync(filePath, 'utf-8'); }
|
|
40
|
+
catch (err) {
|
|
41
|
+
warn(`Cannot read ${relPath}: ${err.message}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let tree;
|
|
46
|
+
try { tree = parser.parse(code); }
|
|
47
|
+
catch (err) {
|
|
48
|
+
warn(`Parse error in ${relPath}: ${err.message}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isHCL = filePath.endsWith('.tf') || filePath.endsWith('.hcl');
|
|
53
|
+
const isPython = filePath.endsWith('.py');
|
|
54
|
+
const symbols = isHCL ? extractHCLSymbols(tree, filePath)
|
|
55
|
+
: isPython ? extractPythonSymbols(tree, filePath)
|
|
56
|
+
: extractSymbols(tree, filePath);
|
|
57
|
+
|
|
58
|
+
stmts.insertNode.run(relPath, 'file', relPath, 0, null);
|
|
59
|
+
|
|
60
|
+
for (const def of symbols.definitions) {
|
|
61
|
+
stmts.insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
|
|
62
|
+
}
|
|
63
|
+
for (const exp of symbols.exports) {
|
|
64
|
+
stmts.insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const newNodes = stmts.countNodes.get(relPath)?.c || 0;
|
|
68
|
+
|
|
69
|
+
let edgesAdded = 0;
|
|
70
|
+
const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
|
|
71
|
+
if (!fileNodeRow) return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
|
|
72
|
+
const fileNodeId = fileNodeRow.id;
|
|
73
|
+
|
|
74
|
+
// Load aliases for full import resolution
|
|
75
|
+
const aliases = { baseUrl: null, paths: {} };
|
|
76
|
+
|
|
77
|
+
for (const imp of symbols.imports) {
|
|
78
|
+
const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
|
|
79
|
+
const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
|
|
80
|
+
if (targetRow) {
|
|
81
|
+
const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
|
|
82
|
+
stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
|
|
83
|
+
edgesAdded++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const importedNames = new Map();
|
|
88
|
+
for (const imp of symbols.imports) {
|
|
89
|
+
const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
|
|
90
|
+
for (const name of imp.names) {
|
|
91
|
+
importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const call of symbols.calls) {
|
|
96
|
+
let caller = null;
|
|
97
|
+
for (const def of symbols.definitions) {
|
|
98
|
+
if (def.line <= call.line) {
|
|
99
|
+
const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
|
|
100
|
+
if (row) caller = row;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!caller) caller = fileNodeRow;
|
|
104
|
+
|
|
105
|
+
const importedFrom = importedNames.get(call.name);
|
|
106
|
+
let targets;
|
|
107
|
+
if (importedFrom) {
|
|
108
|
+
targets = stmts.findNodeInFile.all(call.name, importedFrom);
|
|
109
|
+
}
|
|
110
|
+
if (!targets || targets.length === 0) {
|
|
111
|
+
targets = stmts.findNodeInFile.all(call.name, relPath);
|
|
112
|
+
if (targets.length === 0) {
|
|
113
|
+
targets = stmts.findNodeByName.all(call.name);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const t of targets) {
|
|
118
|
+
if (t.id !== caller.id) {
|
|
119
|
+
stmts.insertEdge.run(caller.id, t.id, 'calls', importedFrom ? 1.0 : 0.5, call.dynamic ? 1 : 0);
|
|
120
|
+
edgesAdded++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
file: relPath,
|
|
127
|
+
nodesAdded: newNodes,
|
|
128
|
+
nodesRemoved: oldNodes,
|
|
129
|
+
edgesAdded,
|
|
130
|
+
deleted: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function watchProject(rootDir) {
|
|
135
|
+
const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
|
|
136
|
+
if (!fs.existsSync(dbPath)) {
|
|
137
|
+
console.error('No graph.db found. Run `codegraph build` first.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const db = openDb(dbPath);
|
|
142
|
+
initSchema(db);
|
|
143
|
+
const parsers = await createParsers();
|
|
144
|
+
|
|
145
|
+
const stmts = {
|
|
146
|
+
insertNode: db.prepare('INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)'),
|
|
147
|
+
getNodeId: db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?'),
|
|
148
|
+
insertEdge: db.prepare('INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)'),
|
|
149
|
+
deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
|
|
150
|
+
deleteEdgesForFile: null,
|
|
151
|
+
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
|
|
152
|
+
countEdgesForFile: null,
|
|
153
|
+
findNodeInFile: db.prepare('SELECT id, file FROM nodes WHERE name = ? AND kind IN (\'function\', \'method\', \'class\', \'interface\') AND file = ?'),
|
|
154
|
+
findNodeByName: db.prepare('SELECT id, file FROM nodes WHERE name = ? AND kind IN (\'function\', \'method\', \'class\', \'interface\')'),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Use named params for statements needing the same value twice
|
|
158
|
+
const origDeleteEdges = db.prepare(`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
|
|
159
|
+
const origCountEdges = db.prepare(`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
|
|
160
|
+
stmts.deleteEdgesForFile = { run: (f) => origDeleteEdges.run({ f }) };
|
|
161
|
+
stmts.countEdgesForFile = { get: (f) => origCountEdges.get({ f }) };
|
|
162
|
+
|
|
163
|
+
const pending = new Set();
|
|
164
|
+
let timer = null;
|
|
165
|
+
const DEBOUNCE_MS = 300;
|
|
166
|
+
|
|
167
|
+
function processPending() {
|
|
168
|
+
const files = [...pending];
|
|
169
|
+
pending.clear();
|
|
170
|
+
|
|
171
|
+
const updates = db.transaction(() => {
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const filePath of files) {
|
|
174
|
+
const result = updateFile(db, rootDir, filePath, parsers, stmts);
|
|
175
|
+
if (result) results.push(result);
|
|
176
|
+
}
|
|
177
|
+
return results;
|
|
178
|
+
})();
|
|
179
|
+
|
|
180
|
+
for (const r of updates) {
|
|
181
|
+
const nodeDelta = r.nodesAdded - r.nodesRemoved;
|
|
182
|
+
const nodeStr = nodeDelta >= 0 ? `+${nodeDelta}` : `${nodeDelta}`;
|
|
183
|
+
if (r.deleted) {
|
|
184
|
+
info(`Removed: ${r.file} (-${r.nodesRemoved} nodes)`);
|
|
185
|
+
} else {
|
|
186
|
+
info(`Updated: ${r.file} (${nodeStr} nodes, +${r.edgesAdded} edges)`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`Watching ${rootDir} for changes...`);
|
|
192
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
193
|
+
|
|
194
|
+
const watcher = fs.watch(rootDir, { recursive: true }, (eventType, filename) => {
|
|
195
|
+
if (!filename) return;
|
|
196
|
+
if (shouldIgnore(filename)) return;
|
|
197
|
+
if (!isTrackedExt(filename)) return;
|
|
198
|
+
|
|
199
|
+
const fullPath = path.join(rootDir, filename);
|
|
200
|
+
pending.add(fullPath);
|
|
201
|
+
|
|
202
|
+
if (timer) clearTimeout(timer);
|
|
203
|
+
timer = setTimeout(processPending, DEBOUNCE_MS);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
process.on('SIGINT', () => {
|
|
207
|
+
console.log('\nStopping watcher...');
|
|
208
|
+
watcher.close();
|
|
209
|
+
db.close();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|