codeatlas-mcp-server 2.20.2
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 +21 -0
- package/README.md +338 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/dist/src/analyzer/parser.js +1072 -0
- package/dist/src/analyzer/parser.js.map +1 -0
- package/dist/src/analyzer/parser.test.js +73 -0
- package/dist/src/analyzer/parser.test.js.map +1 -0
- package/dist/src/analyzer/phpParser.js +147 -0
- package/dist/src/analyzer/phpParser.js.map +1 -0
- package/dist/src/analyzer/pythonParser.js +185 -0
- package/dist/src/analyzer/pythonParser.js.map +1 -0
- package/dist/src/analyzer/types.js +2 -0
- package/dist/src/analyzer/types.js.map +1 -0
- package/dist/src/context.js +3 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/memoryGenerator.js +293 -0
- package/dist/src/memoryGenerator.js.map +1 -0
- package/dist/src/oracleDatabase.js +298 -0
- package/dist/src/oracleDatabase.js.map +1 -0
- package/dist/src/presentation/httpServer.js +306 -0
- package/dist/src/presentation/httpServer.js.map +1 -0
- package/dist/src/presentation/mcpServer.js +1487 -0
- package/dist/src/presentation/mcpServer.js.map +1 -0
- package/dist/src/repositories.js +144 -0
- package/dist/src/repositories.js.map +1 -0
- package/dist/src/securityScanner.js +69 -0
- package/dist/src/securityScanner.js.map +1 -0
- package/dist/src/services/authService.js +24 -0
- package/dist/src/services/authService.js.map +1 -0
- package/dist/src/services/dreamingService.js +119 -0
- package/dist/src/services/dreamingService.js.map +1 -0
- package/dist/src/services/dreamingService.test.js +179 -0
- package/dist/src/services/dreamingService.test.js.map +1 -0
- package/dist/src/services/projectService.js +1068 -0
- package/dist/src/services/projectService.js.map +1 -0
- package/dist/src/services/projectService.test.js +217 -0
- package/dist/src/services/projectService.test.js.map +1 -0
- package/dist/src/services/watcherService.js +164 -0
- package/dist/src/services/watcherService.js.map +1 -0
- package/dist/src/services/watcherService.test.js +65 -0
- package/dist/src/services/watcherService.test.js.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as https from "https";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { CodeAnalyzer } from "../analyzer/parser.js";
|
|
6
|
+
import { authStorage } from "../context.js";
|
|
7
|
+
export const fsWrapper = {
|
|
8
|
+
existsSync: (p) => fs.existsSync(p),
|
|
9
|
+
readFileSync: (p, encoding) => fs.readFileSync(p, encoding),
|
|
10
|
+
readdirSync: (p) => fs.readdirSync(p)
|
|
11
|
+
};
|
|
12
|
+
/** Unified stats helper */
|
|
13
|
+
export function getStats(analysis) {
|
|
14
|
+
const ec = analysis.entityCounts;
|
|
15
|
+
const st = analysis.stats;
|
|
16
|
+
return {
|
|
17
|
+
files: st?.files ?? analysis.totalFilesAnalyzed ?? ec?.modules ?? 0,
|
|
18
|
+
modules: ec?.modules ?? st?.files ?? analysis.totalFilesAnalyzed ?? 0,
|
|
19
|
+
functions: ec?.functions ?? st?.functions ?? 0,
|
|
20
|
+
classes: ec?.classes ?? st?.classes ?? 0,
|
|
21
|
+
dependencies: ec?.dependencies ?? st?.dependencies ?? 0,
|
|
22
|
+
circularDeps: ec?.circularDeps ?? st?.circularDeps ?? 0,
|
|
23
|
+
deadCode: ec?.deadCode ?? st?.deadCode ?? 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function findDirMatchingNormalized(normalized) {
|
|
27
|
+
if (!/^[a-zA-Z0-9_]+$/.test(normalized)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const directPath = "/" + normalized.replace(/_/g, "/");
|
|
31
|
+
if (fsWrapper.existsSync(directPath)) {
|
|
32
|
+
return directPath;
|
|
33
|
+
}
|
|
34
|
+
const parts = normalized.split("_").filter(Boolean);
|
|
35
|
+
if (parts.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
let currentPath = "/";
|
|
38
|
+
for (let i = 0; i < parts.length; i++) {
|
|
39
|
+
if (!fsWrapper.existsSync(currentPath))
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const files = fsWrapper.readdirSync(currentPath);
|
|
43
|
+
let matchedEntry = "";
|
|
44
|
+
let matchedPartCount = 0;
|
|
45
|
+
let matchedIsExactCase = false;
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const normFile = file.replace(/[^a-zA-Z0-9]/g, "_");
|
|
48
|
+
const normFileParts = normFile.split("_").filter(Boolean);
|
|
49
|
+
if (normFileParts.length === 0)
|
|
50
|
+
continue;
|
|
51
|
+
let match = true;
|
|
52
|
+
let isExactCase = true;
|
|
53
|
+
for (let j = 0; j < normFileParts.length; j++) {
|
|
54
|
+
if (i + j >= parts.length) {
|
|
55
|
+
match = false;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const partA = parts[i + j];
|
|
59
|
+
const partB = normFileParts[j];
|
|
60
|
+
if (partA.toLowerCase() !== partB.toLowerCase()) {
|
|
61
|
+
match = false;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (partA !== partB) {
|
|
65
|
+
isExactCase = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (match) {
|
|
69
|
+
if (normFileParts.length > matchedPartCount || (normFileParts.length === matchedPartCount && isExactCase && !matchedIsExactCase)) {
|
|
70
|
+
matchedEntry = file;
|
|
71
|
+
matchedPartCount = normFileParts.length;
|
|
72
|
+
matchedIsExactCase = isExactCase;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (matchedEntry) {
|
|
77
|
+
currentPath = path.join(currentPath, matchedEntry);
|
|
78
|
+
i += matchedPartCount - 1;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
currentPath = path.join(currentPath, parts[i]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (fsWrapper.existsSync(currentPath)) {
|
|
89
|
+
return currentPath;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Discovers workspace directory by traversing ancestor processes via /proc.
|
|
95
|
+
* @param startPid - Process ID to start traversal from (defaults to current process)
|
|
96
|
+
* @returns Resolved workspace directory path, or null if not found or on unsupported platforms
|
|
97
|
+
* @platform Linux only - requires /proc filesystem
|
|
98
|
+
*/
|
|
99
|
+
export function getWorkspaceFromAncestors(startPid = process.pid) {
|
|
100
|
+
try {
|
|
101
|
+
// 1. Build a map of PPID -> children PIDs by scanning /proc once.
|
|
102
|
+
// This allows us to inspect siblings of processes in the ancestor chain.
|
|
103
|
+
const ppidToChildren = new Map();
|
|
104
|
+
if (fsWrapper.existsSync('/proc')) {
|
|
105
|
+
const files = fsWrapper.readdirSync('/proc');
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
if (/^\d+$/.test(file)) {
|
|
108
|
+
const pid = parseInt(file, 10);
|
|
109
|
+
const statusPath = `/proc/${pid}/status`;
|
|
110
|
+
try {
|
|
111
|
+
if (fsWrapper.existsSync(statusPath)) {
|
|
112
|
+
const statusContent = fsWrapper.readFileSync(statusPath, 'utf8');
|
|
113
|
+
const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
|
|
114
|
+
if (ppidMatch) {
|
|
115
|
+
const ppid = parseInt(ppidMatch[1], 10);
|
|
116
|
+
if (!ppidToChildren.has(ppid)) {
|
|
117
|
+
ppidToChildren.set(ppid, []);
|
|
118
|
+
}
|
|
119
|
+
ppidToChildren.get(ppid).push(pid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore access errors on individual processes
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 2. Traverse up the ancestor chain
|
|
130
|
+
let currentPid = startPid;
|
|
131
|
+
let iterations = 0;
|
|
132
|
+
while (currentPid > 1 && iterations < 100) {
|
|
133
|
+
iterations++;
|
|
134
|
+
const statusPath = `/proc/${currentPid}/status`;
|
|
135
|
+
if (!fsWrapper.existsSync(statusPath))
|
|
136
|
+
break;
|
|
137
|
+
const statusContent = fsWrapper.readFileSync(statusPath, 'utf8');
|
|
138
|
+
const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
|
|
139
|
+
if (!ppidMatch)
|
|
140
|
+
break;
|
|
141
|
+
const ppid = parseInt(ppidMatch[1], 10);
|
|
142
|
+
if (ppid <= 1 || ppid === currentPid)
|
|
143
|
+
break;
|
|
144
|
+
// First check: does the parent process itself have the workspace ID?
|
|
145
|
+
const parentCmdlinePath = `/proc/${ppid}/cmdline`;
|
|
146
|
+
if (fsWrapper.existsSync(parentCmdlinePath)) {
|
|
147
|
+
const cmdline = fsWrapper.readFileSync(parentCmdlinePath, 'utf8');
|
|
148
|
+
const args = cmdline.split('\0');
|
|
149
|
+
const workspaceIdIndex = args.indexOf('--workspace_id');
|
|
150
|
+
if (workspaceIdIndex !== -1 && workspaceIdIndex + 1 < args.length) {
|
|
151
|
+
const workspaceId = args[workspaceIdIndex + 1];
|
|
152
|
+
if (workspaceId.startsWith('file_')) {
|
|
153
|
+
const normalized = workspaceId.substring(5);
|
|
154
|
+
const dir = findDirMatchingNormalized(normalized);
|
|
155
|
+
if (dir)
|
|
156
|
+
return dir;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Second check: check all siblings (other children of ppid)
|
|
161
|
+
const siblings = ppidToChildren.get(ppid) || [];
|
|
162
|
+
for (const siblingPid of siblings) {
|
|
163
|
+
if (siblingPid === currentPid)
|
|
164
|
+
continue; // Skip self
|
|
165
|
+
const cmdlinePath = `/proc/${siblingPid}/cmdline`;
|
|
166
|
+
if (fsWrapper.existsSync(cmdlinePath)) {
|
|
167
|
+
const cmdline = fsWrapper.readFileSync(cmdlinePath, 'utf8');
|
|
168
|
+
const args = cmdline.split('\0');
|
|
169
|
+
const workspaceIdIndex = args.indexOf('--workspace_id');
|
|
170
|
+
if (workspaceIdIndex !== -1 && workspaceIdIndex + 1 < args.length) {
|
|
171
|
+
const workspaceId = args[workspaceIdIndex + 1];
|
|
172
|
+
if (workspaceId.startsWith('file_')) {
|
|
173
|
+
const normalized = workspaceId.substring(5);
|
|
174
|
+
const dir = findDirMatchingNormalized(normalized);
|
|
175
|
+
if (dir)
|
|
176
|
+
return dir;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
currentPid = ppid;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Ignore and fallback
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
export function registerProject(dir) {
|
|
190
|
+
try {
|
|
191
|
+
const homeDir = os.homedir();
|
|
192
|
+
const configDir = path.join(homeDir, ".codeatlas");
|
|
193
|
+
if (!fs.existsSync(configDir)) {
|
|
194
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
const regPath = path.join(configDir, "registered_projects.json");
|
|
197
|
+
let projects = [];
|
|
198
|
+
if (fs.existsSync(regPath)) {
|
|
199
|
+
try {
|
|
200
|
+
const data = fs.readFileSync(regPath, "utf-8");
|
|
201
|
+
projects = JSON.parse(data);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
projects = [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!Array.isArray(projects)) {
|
|
208
|
+
projects = [];
|
|
209
|
+
}
|
|
210
|
+
const absPath = path.resolve(dir);
|
|
211
|
+
if (isSystemIdeDirectory(absPath)) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!projects.includes(absPath)) {
|
|
215
|
+
projects.push(absPath);
|
|
216
|
+
fs.writeFileSync(regPath, JSON.stringify(projects, null, 2));
|
|
217
|
+
console.error(`[Project-Registry] 📝 Registered new project: ${absPath}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error(`[Project-Registry] ❌ Failed to register project: ${err}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
let onProjectLoadedCallback = null;
|
|
225
|
+
export function registerOnProjectLoaded(cb) {
|
|
226
|
+
onProjectLoadedCallback = cb;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* LRU cache that evicts oldest entries when exceeding maxSize.
|
|
230
|
+
* On eviction, also cleans up the corresponding CodeAnalyzer instance.
|
|
231
|
+
*/
|
|
232
|
+
class LRUCache {
|
|
233
|
+
map = new Map();
|
|
234
|
+
maxSize;
|
|
235
|
+
evictionLog;
|
|
236
|
+
constructor(maxSize, name = "cache") {
|
|
237
|
+
this.maxSize = maxSize;
|
|
238
|
+
this.evictionLog = `[${name}]`;
|
|
239
|
+
}
|
|
240
|
+
has(key) {
|
|
241
|
+
return this.map.has(key);
|
|
242
|
+
}
|
|
243
|
+
get(key) {
|
|
244
|
+
if (!this.map.has(key))
|
|
245
|
+
return undefined;
|
|
246
|
+
// Move to end (most recently used)
|
|
247
|
+
const value = this.map.get(key);
|
|
248
|
+
this.map.delete(key);
|
|
249
|
+
this.map.set(key, value);
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
set(key, value) {
|
|
253
|
+
this.map.delete(key); // Remove if exists, re-add at end
|
|
254
|
+
this.map.set(key, value);
|
|
255
|
+
this.evict();
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
delete(key) {
|
|
259
|
+
return this.map.delete(key);
|
|
260
|
+
}
|
|
261
|
+
get size() {
|
|
262
|
+
return this.map.size;
|
|
263
|
+
}
|
|
264
|
+
keys() {
|
|
265
|
+
return this.map.keys();
|
|
266
|
+
}
|
|
267
|
+
evict() {
|
|
268
|
+
while (this.map.size > this.maxSize) {
|
|
269
|
+
const oldestKey = this.map.keys().next().value;
|
|
270
|
+
if (oldestKey !== undefined) {
|
|
271
|
+
this.map.delete(oldestKey);
|
|
272
|
+
// Also cleanup the CodeAnalyzer instance to free memory
|
|
273
|
+
const hadAnalyzer = analyzerInstances.delete(oldestKey);
|
|
274
|
+
console.error(`${this.evictionLog} 🗑️ Evicted cache for: ${oldestKey}${hadAnalyzer ? ' (analyzer also freed)' : ''}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
[Symbol.iterator]() {
|
|
279
|
+
return this.map[Symbol.iterator]();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
export const inMemoryAnalysisCache = new LRUCache(3, "Cache");
|
|
283
|
+
export const analyzerInstances = new LRUCache(5, "Analyzer");
|
|
284
|
+
export function getOpenIdeForDir(dir) {
|
|
285
|
+
try {
|
|
286
|
+
const absPath = path.resolve(dir.trim());
|
|
287
|
+
if (!fs.existsSync('/proc'))
|
|
288
|
+
return null;
|
|
289
|
+
const files = fs.readdirSync('/proc');
|
|
290
|
+
// Safety: only scan up to 500 process entries to prevent abuse
|
|
291
|
+
const pidEntries = files.filter(f => /^\d+$/.test(f)).slice(0, 500);
|
|
292
|
+
for (const file of pidEntries) {
|
|
293
|
+
const pid = file;
|
|
294
|
+
const cmdlinePath = `/proc/${pid}/cmdline`;
|
|
295
|
+
try {
|
|
296
|
+
if (fs.existsSync(cmdlinePath)) {
|
|
297
|
+
const cmdline = fs.readFileSync(cmdlinePath, 'utf8');
|
|
298
|
+
const args = cmdline.split('\0').filter(Boolean);
|
|
299
|
+
if (args.length === 0)
|
|
300
|
+
continue;
|
|
301
|
+
const hasDirArg = args.some(arg => {
|
|
302
|
+
try {
|
|
303
|
+
return path.resolve(arg) === absPath;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (hasDirArg) {
|
|
310
|
+
const exePath = args[0].toLowerCase();
|
|
311
|
+
const ideKeywords = ['code', 'vscode', 'cursor', 'windsurf', 'intellij', 'webstorm', 'phpstorm', 'idea', 'eclipse', 'sublime', 'gemini-cli'];
|
|
312
|
+
for (const keyword of ideKeywords) {
|
|
313
|
+
if (exePath.includes(keyword)) {
|
|
314
|
+
return path.basename(args[0]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// ignore
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// ignore
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
export function isProjectDirectory(dir) {
|
|
331
|
+
if (isSystemIdeDirectory(dir)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const gitPath = path.join(dir, ".git");
|
|
339
|
+
if (fs.existsSync(gitPath)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
const codeatlasPath = path.join(dir, ".codeatlas");
|
|
343
|
+
if (fs.existsSync(codeatlasPath)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const openIde = getOpenIdeForDir(dir);
|
|
347
|
+
if (openIde) {
|
|
348
|
+
console.error(`[Project-Discovery] 🖥️ Project ${dir} is active in IDE: ${openIde}`);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export async function isProjectDirectoryAsync(dir) {
|
|
358
|
+
if (isSystemIdeDirectory(dir)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const stat = await fs.promises.stat(dir);
|
|
363
|
+
if (!stat.isDirectory()) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
const gitPath = path.join(dir, ".git");
|
|
367
|
+
if (await fileExists(gitPath)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const codeatlasPath = path.join(dir, ".codeatlas");
|
|
371
|
+
if (await fileExists(codeatlasPath)) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const openIde = getOpenIdeForDir(dir);
|
|
375
|
+
if (openIde) {
|
|
376
|
+
console.error(`[Project-Discovery] 🖥️ Project ${dir} is active in IDE: ${openIde}`);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
export async function fileExists(filePath) {
|
|
386
|
+
try {
|
|
387
|
+
await fs.promises.access(filePath);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export function isSystemIdeDirectory(dir) {
|
|
395
|
+
try {
|
|
396
|
+
const absPath = path.resolve(dir.trim());
|
|
397
|
+
if (absPath === "/config/Downloads/Antigravity" || absPath.startsWith("/config/Downloads/Antigravity/")) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
// Dynamically resolve ~/.gemini/antigravity across operating systems
|
|
401
|
+
const homeDir = os.homedir();
|
|
402
|
+
const dynamicAntigravityPath = path.resolve(path.join(homeDir, ".gemini", "antigravity"));
|
|
403
|
+
if (absPath === dynamicAntigravityPath || absPath.startsWith(dynamicAntigravityPath + path.sep)) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
// Ignore home directory itself, system root, or /config root
|
|
407
|
+
if (absPath === homeDir || absPath === "/" || absPath === "/config") {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
// Ignore system/IDE configuration folders starting with a dot (e.g. .codeium, .vscode, .cursor)
|
|
411
|
+
// but allow double-dot prefixes (like ..projectA)
|
|
412
|
+
const parts = absPath.split(path.sep);
|
|
413
|
+
if (parts.some(part => part.startsWith('.') && !part.startsWith('..') && part !== '.codeatlas')) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
// Check if it's the IDE resources directory
|
|
417
|
+
if (fsWrapper.existsSync(path.join(absPath, "resources", "app", "extensions")) ||
|
|
418
|
+
fsWrapper.existsSync(path.join(absPath, "resources", "app", "out", "vs"))) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Ignore errors
|
|
424
|
+
}
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
export function scanForCodeatlasProjects(parentDir) {
|
|
428
|
+
const discovered = [];
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.existsSync(parentDir) || !fs.statSync(parentDir).isDirectory()) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
// If the directory itself contains .codeatlas, it is a project
|
|
434
|
+
if (fs.existsSync(path.join(parentDir, ".codeatlas"))) {
|
|
435
|
+
discovered.push(path.resolve(parentDir));
|
|
436
|
+
return discovered;
|
|
437
|
+
}
|
|
438
|
+
// Otherwise, scan subdirectories up to 2 levels deep
|
|
439
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
440
|
+
for (const entry of entries) {
|
|
441
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
442
|
+
const subPath = path.join(parentDir, entry.name);
|
|
443
|
+
if (fs.existsSync(path.join(subPath, ".codeatlas"))) {
|
|
444
|
+
discovered.push(path.resolve(subPath));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Check 2nd level
|
|
448
|
+
try {
|
|
449
|
+
const subEntries = fs.readdirSync(subPath, { withFileTypes: true });
|
|
450
|
+
for (const subEntry of subEntries) {
|
|
451
|
+
if (subEntry.isDirectory() && subEntry.name !== "node_modules" && !subEntry.name.startsWith(".")) {
|
|
452
|
+
const subSubPath = path.join(subPath, subEntry.name);
|
|
453
|
+
if (fs.existsSync(path.join(subSubPath, ".codeatlas"))) {
|
|
454
|
+
discovered.push(path.resolve(subSubPath));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch { /* skip */ }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
console.error(`[Project-Discovery] ❌ Failed to scan for .codeatlas projects: ${err}`);
|
|
466
|
+
}
|
|
467
|
+
return discovered;
|
|
468
|
+
}
|
|
469
|
+
export async function scanForCodeatlasProjectsAsync(parentDir) {
|
|
470
|
+
const discovered = [];
|
|
471
|
+
try {
|
|
472
|
+
if (!(await fileExists(parentDir))) {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
const parentStat = await fs.promises.stat(parentDir);
|
|
476
|
+
if (!parentStat.isDirectory()) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
if (await fileExists(path.join(parentDir, ".codeatlas"))) {
|
|
480
|
+
discovered.push(path.resolve(parentDir));
|
|
481
|
+
return discovered;
|
|
482
|
+
}
|
|
483
|
+
const entries = await fs.promises.readdir(parentDir, { withFileTypes: true });
|
|
484
|
+
for (const entry of entries) {
|
|
485
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
486
|
+
const subPath = path.join(parentDir, entry.name);
|
|
487
|
+
if (await fileExists(path.join(subPath, ".codeatlas"))) {
|
|
488
|
+
discovered.push(path.resolve(subPath));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Check 2nd level
|
|
492
|
+
try {
|
|
493
|
+
const subEntries = await fs.promises.readdir(subPath, { withFileTypes: true });
|
|
494
|
+
for (const subEntry of subEntries) {
|
|
495
|
+
if (subEntry.isDirectory() && subEntry.name !== "node_modules" && !subEntry.name.startsWith(".")) {
|
|
496
|
+
const subSubPath = path.join(subPath, subEntry.name);
|
|
497
|
+
if (await fileExists(path.join(subSubPath, ".codeatlas"))) {
|
|
498
|
+
discovered.push(path.resolve(subSubPath));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch { /* skip */ }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
console.error(`[Project-Discovery] ❌ Failed async scan for .codeatlas projects: ${err}`);
|
|
510
|
+
}
|
|
511
|
+
return discovered;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Scans a parent directory for sub-projects with .git directories,
|
|
515
|
+
* optionally filtering to only those currently open in an IDE.
|
|
516
|
+
* Scans up to 3 levels deep and skips node_modules / hidden dirs.
|
|
517
|
+
*/
|
|
518
|
+
export async function discoverGitSubProjects(parentDir, onlyIdeOpen = false) {
|
|
519
|
+
const discovered = [];
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
async function walk(dir, depth) {
|
|
522
|
+
if (depth > 3)
|
|
523
|
+
return;
|
|
524
|
+
let entries;
|
|
525
|
+
try {
|
|
526
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
for (const entry of entries) {
|
|
532
|
+
if (!entry.isDirectory())
|
|
533
|
+
continue;
|
|
534
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.'))
|
|
535
|
+
continue;
|
|
536
|
+
const fullPath = path.join(dir, entry.name);
|
|
537
|
+
const resolved = path.resolve(fullPath);
|
|
538
|
+
if (seen.has(resolved))
|
|
539
|
+
continue;
|
|
540
|
+
seen.add(resolved);
|
|
541
|
+
if (await fileExists(path.join(resolved, '.git'))) {
|
|
542
|
+
if (onlyIdeOpen) {
|
|
543
|
+
const ide = getOpenIdeForDir(resolved);
|
|
544
|
+
if (ide) {
|
|
545
|
+
console.error(`[SubProject] 🖥️ Found project "${entry.name}" — open in IDE: ${ide}`);
|
|
546
|
+
discovered.push(resolved);
|
|
547
|
+
}
|
|
548
|
+
// else: skip — no IDE has this open
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
console.error(`[SubProject] 📂 Found project: ${entry.name}`);
|
|
552
|
+
discovered.push(resolved);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (depth < 3) {
|
|
556
|
+
await walk(resolved, depth + 1);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
await walk(parentDir, 0);
|
|
561
|
+
return discovered;
|
|
562
|
+
}
|
|
563
|
+
export function discoverProjects(tenantId) {
|
|
564
|
+
const projects = [];
|
|
565
|
+
const searchDirs = [];
|
|
566
|
+
// Multi-Tenant Isolation
|
|
567
|
+
if (process.env.CODEATLAS_MULTI_TENANT === "true") {
|
|
568
|
+
const auth = authStorage.getStore();
|
|
569
|
+
const isSystemAdmin = auth
|
|
570
|
+
? (auth.uid === "admin" || auth.role === "admin")
|
|
571
|
+
: (tenantId === "admin");
|
|
572
|
+
if (tenantId && !isSystemAdmin) {
|
|
573
|
+
const tenantRoot = process.env.CODEATLAS_PROJECTS_ROOT || path.join(process.cwd(), "tenants");
|
|
574
|
+
const userDir = path.join(tenantRoot, tenantId);
|
|
575
|
+
if (fs.existsSync(userDir)) {
|
|
576
|
+
try {
|
|
577
|
+
const userProjects = fs.readdirSync(userDir);
|
|
578
|
+
for (const p of userProjects) {
|
|
579
|
+
const fullPath = path.join(userDir, p);
|
|
580
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
581
|
+
searchDirs.push(fullPath);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch { /* skip */ }
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else if (isSystemAdmin) {
|
|
589
|
+
const defaultProjDir = process.env.CODEATLAS_PROJECT_DIR || getWorkspaceFromAncestors() || process.env.GEMINI_CLI_IDE_WORKSPACE_PATH;
|
|
590
|
+
if (defaultProjDir) {
|
|
591
|
+
searchDirs.push(defaultProjDir);
|
|
592
|
+
}
|
|
593
|
+
searchDirs.push(process.cwd());
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
const defaultProjDir = process.env.CODEATLAS_PROJECT_DIR || getWorkspaceFromAncestors() || process.env.GEMINI_CLI_IDE_WORKSPACE_PATH;
|
|
601
|
+
if (defaultProjDir) {
|
|
602
|
+
searchDirs.push(defaultProjDir);
|
|
603
|
+
}
|
|
604
|
+
// Dynamically search defaultProjDir || process.cwd() for any projects configured with .codeatlas
|
|
605
|
+
const baseDir = defaultProjDir || process.cwd();
|
|
606
|
+
const localProjects = scanForCodeatlasProjects(baseDir);
|
|
607
|
+
searchDirs.push(...localProjects);
|
|
608
|
+
// Fallback to active workspace if no subprojects were found with .codeatlas configuration
|
|
609
|
+
if (!searchDirs.includes(baseDir)) {
|
|
610
|
+
searchDirs.push(baseDir);
|
|
611
|
+
}
|
|
612
|
+
// Load globally registered projects
|
|
613
|
+
try {
|
|
614
|
+
const homeDir = os.homedir();
|
|
615
|
+
const regPath = path.join(homeDir, ".codeatlas", "registered_projects.json");
|
|
616
|
+
if (fs.existsSync(regPath)) {
|
|
617
|
+
const registered = JSON.parse(fs.readFileSync(regPath, "utf-8"));
|
|
618
|
+
if (Array.isArray(registered)) {
|
|
619
|
+
let updated = false;
|
|
620
|
+
const filtered = registered.filter((dir) => {
|
|
621
|
+
if (isSystemIdeDirectory(dir)) {
|
|
622
|
+
updated = true;
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
});
|
|
627
|
+
if (updated) {
|
|
628
|
+
fs.writeFileSync(regPath, JSON.stringify(filtered, null, 2));
|
|
629
|
+
}
|
|
630
|
+
for (const dir of filtered) {
|
|
631
|
+
if (fs.existsSync(dir)) {
|
|
632
|
+
searchDirs.push(dir);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch { /* skip */ }
|
|
639
|
+
}
|
|
640
|
+
const seen = new Set();
|
|
641
|
+
for (const dir of searchDirs) {
|
|
642
|
+
if (seen.has(dir))
|
|
643
|
+
continue;
|
|
644
|
+
seen.add(dir);
|
|
645
|
+
if (isSystemIdeDirectory(dir))
|
|
646
|
+
continue;
|
|
647
|
+
if (isProjectDirectory(dir)) {
|
|
648
|
+
try {
|
|
649
|
+
const analysisPath = path.join(dir, ".codeatlas", "analysis.json");
|
|
650
|
+
let modifiedAt;
|
|
651
|
+
if (fs.existsSync(analysisPath)) {
|
|
652
|
+
modifiedAt = fs.statSync(analysisPath).mtime;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
modifiedAt = fs.statSync(dir).mtime;
|
|
656
|
+
}
|
|
657
|
+
projects.push({
|
|
658
|
+
name: path.basename(dir),
|
|
659
|
+
dir,
|
|
660
|
+
analysisPath,
|
|
661
|
+
modifiedAt,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
catch { /* skip */ }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
projects.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
668
|
+
return projects;
|
|
669
|
+
}
|
|
670
|
+
export function loadAnalysis(projectDir, force = false) {
|
|
671
|
+
const auth = authStorage.getStore();
|
|
672
|
+
const tenantId = auth ? auth.uid : undefined;
|
|
673
|
+
const projects = discoverProjects(tenantId);
|
|
674
|
+
if (projects.length === 0)
|
|
675
|
+
return null;
|
|
676
|
+
let target = projects[0];
|
|
677
|
+
if (projectDir) {
|
|
678
|
+
const absPath = path.resolve(projectDir.trim());
|
|
679
|
+
if (isSystemIdeDirectory(absPath)) {
|
|
680
|
+
console.warn(`[Auto-Scan] 🛡️ Ignored IDE system/extensions directory from workspace indexing: ${absPath}`);
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
let match = projects.find((p) => p.dir === absPath || p.name.toLowerCase() === projectDir.trim().toLowerCase());
|
|
684
|
+
if (match) {
|
|
685
|
+
target = match;
|
|
686
|
+
registerProject(target.dir);
|
|
687
|
+
}
|
|
688
|
+
else if (fs.existsSync(absPath) && isProjectDirectory(absPath)) {
|
|
689
|
+
registerProject(absPath);
|
|
690
|
+
target = {
|
|
691
|
+
name: path.basename(absPath),
|
|
692
|
+
dir: absPath,
|
|
693
|
+
analysisPath: path.join(absPath, ".codeatlas", "analysis.json"),
|
|
694
|
+
modifiedAt: new Date()
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
else if (target) {
|
|
702
|
+
registerProject(target.dir);
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
if (onProjectLoadedCallback) {
|
|
706
|
+
onProjectLoadedCallback(target.dir);
|
|
707
|
+
}
|
|
708
|
+
if (!force && inMemoryAnalysisCache.has(target.dir)) {
|
|
709
|
+
const cached = inMemoryAnalysisCache.get(target.dir);
|
|
710
|
+
return { analysis: cached, projectName: target.name, projectDir: target.dir };
|
|
711
|
+
}
|
|
712
|
+
console.error(`[Auto-Scan] ⚠️ Warning: Sync scanning called. Returning null since scan is memory-only.`);
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
console.error(`[Auto-Scan] ❌ Sync scanning failed: ${err}`);
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
export async function discoverProjectsAsync(tenantId) {
|
|
721
|
+
const projects = [];
|
|
722
|
+
const searchDirs = [];
|
|
723
|
+
// Multi-Tenant Isolation
|
|
724
|
+
if (process.env.CODEATLAS_MULTI_TENANT === "true") {
|
|
725
|
+
const auth = authStorage.getStore();
|
|
726
|
+
const isSystemAdmin = auth
|
|
727
|
+
? (auth.uid === "admin" || auth.role === "admin")
|
|
728
|
+
: (tenantId === "admin");
|
|
729
|
+
if (tenantId && !isSystemAdmin) {
|
|
730
|
+
const tenantRoot = process.env.CODEATLAS_PROJECTS_ROOT || path.join(process.cwd(), "tenants");
|
|
731
|
+
const userDir = path.join(tenantRoot, tenantId);
|
|
732
|
+
if (await fileExists(userDir)) {
|
|
733
|
+
try {
|
|
734
|
+
const userProjects = await fs.promises.readdir(userDir);
|
|
735
|
+
for (const p of userProjects) {
|
|
736
|
+
const fullPath = path.join(userDir, p);
|
|
737
|
+
try {
|
|
738
|
+
const stat = await fs.promises.stat(fullPath);
|
|
739
|
+
if (stat.isDirectory()) {
|
|
740
|
+
searchDirs.push(fullPath);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch { /* skip */ }
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch { /* skip */ }
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else if (isSystemAdmin) {
|
|
750
|
+
const defaultProjDir = process.env.CODEATLAS_PROJECT_DIR || getWorkspaceFromAncestors() || process.env.GEMINI_CLI_IDE_WORKSPACE_PATH;
|
|
751
|
+
if (defaultProjDir) {
|
|
752
|
+
searchDirs.push(defaultProjDir);
|
|
753
|
+
}
|
|
754
|
+
searchDirs.push(process.cwd());
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
const defaultProjDir = process.env.CODEATLAS_PROJECT_DIR || getWorkspaceFromAncestors() || process.env.GEMINI_CLI_IDE_WORKSPACE_PATH;
|
|
762
|
+
if (defaultProjDir) {
|
|
763
|
+
searchDirs.push(defaultProjDir);
|
|
764
|
+
}
|
|
765
|
+
// Dynamically search defaultProjDir || process.cwd() for any projects configured with .codeatlas
|
|
766
|
+
const baseDir = defaultProjDir || process.cwd();
|
|
767
|
+
const localProjects = await scanForCodeatlasProjectsAsync(baseDir);
|
|
768
|
+
searchDirs.push(...localProjects);
|
|
769
|
+
// Fallback to active workspace if no subprojects were found with .codeatlas configuration
|
|
770
|
+
if (!searchDirs.includes(baseDir)) {
|
|
771
|
+
searchDirs.push(baseDir);
|
|
772
|
+
}
|
|
773
|
+
// Load globally registered projects
|
|
774
|
+
try {
|
|
775
|
+
const homeDir = os.homedir();
|
|
776
|
+
const regPath = path.join(homeDir, ".codeatlas", "registered_projects.json");
|
|
777
|
+
if (await fileExists(regPath)) {
|
|
778
|
+
const data = await fs.promises.readFile(regPath, "utf-8");
|
|
779
|
+
const registered = JSON.parse(data);
|
|
780
|
+
if (Array.isArray(registered)) {
|
|
781
|
+
let updated = false;
|
|
782
|
+
const filtered = registered.filter((dir) => {
|
|
783
|
+
if (isSystemIdeDirectory(dir)) {
|
|
784
|
+
updated = true;
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
});
|
|
789
|
+
if (updated) {
|
|
790
|
+
await fs.promises.writeFile(regPath, JSON.stringify(filtered, null, 2));
|
|
791
|
+
}
|
|
792
|
+
for (const dir of filtered) {
|
|
793
|
+
if (await fileExists(dir)) {
|
|
794
|
+
searchDirs.push(dir);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
catch { /* skip */ }
|
|
801
|
+
}
|
|
802
|
+
const seen = new Set();
|
|
803
|
+
for (const dir of searchDirs) {
|
|
804
|
+
if (seen.has(dir))
|
|
805
|
+
continue;
|
|
806
|
+
seen.add(dir);
|
|
807
|
+
if (isSystemIdeDirectory(dir))
|
|
808
|
+
continue;
|
|
809
|
+
if (await isProjectDirectoryAsync(dir)) {
|
|
810
|
+
try {
|
|
811
|
+
const analysisPath = path.join(dir, ".codeatlas", "analysis.json");
|
|
812
|
+
let modifiedAt;
|
|
813
|
+
if (await fileExists(analysisPath)) {
|
|
814
|
+
modifiedAt = (await fs.promises.stat(analysisPath)).mtime;
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
modifiedAt = (await fs.promises.stat(dir)).mtime;
|
|
818
|
+
}
|
|
819
|
+
projects.push({
|
|
820
|
+
name: path.basename(dir),
|
|
821
|
+
dir,
|
|
822
|
+
analysisPath,
|
|
823
|
+
modifiedAt,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch { /* skip */ }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
projects.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
830
|
+
return projects;
|
|
831
|
+
}
|
|
832
|
+
export async function loadAnalysisAsync(projectDir, force = false, changedFilePath) {
|
|
833
|
+
const auth = authStorage.getStore();
|
|
834
|
+
const tenantId = auth ? auth.uid : undefined;
|
|
835
|
+
const projects = await discoverProjectsAsync(tenantId);
|
|
836
|
+
if (projects.length === 0)
|
|
837
|
+
return null;
|
|
838
|
+
let target = projects[0];
|
|
839
|
+
if (projectDir) {
|
|
840
|
+
const absPath = path.resolve(projectDir.trim());
|
|
841
|
+
if (isSystemIdeDirectory(absPath)) {
|
|
842
|
+
console.warn(`[Auto-Scan] 🛡️ Ignored IDE system/extensions directory from workspace indexing: ${absPath}`);
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
let match = projects.find((p) => p.dir === absPath || p.name.toLowerCase() === projectDir.trim().toLowerCase());
|
|
846
|
+
if (match) {
|
|
847
|
+
target = match;
|
|
848
|
+
registerProject(target.dir);
|
|
849
|
+
}
|
|
850
|
+
else if (await isProjectDirectoryAsync(absPath)) {
|
|
851
|
+
registerProject(absPath);
|
|
852
|
+
target = {
|
|
853
|
+
name: path.basename(absPath),
|
|
854
|
+
dir: absPath,
|
|
855
|
+
analysisPath: path.join(absPath, ".codeatlas", "analysis.json"),
|
|
856
|
+
modifiedAt: new Date()
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
console.error(`[Auto-Scan] ⚠️ Path is not a valid project directory, skipping: ${absPath}`);
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else if (target) {
|
|
865
|
+
registerProject(target.dir);
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
if (onProjectLoadedCallback) {
|
|
869
|
+
onProjectLoadedCallback(target.dir);
|
|
870
|
+
}
|
|
871
|
+
// Check in-memory cache first
|
|
872
|
+
if (!force && !changedFilePath && inMemoryAnalysisCache.has(target.dir)) {
|
|
873
|
+
const cached = inMemoryAnalysisCache.get(target.dir);
|
|
874
|
+
return { analysis: cached, projectName: target.name, projectDir: target.dir };
|
|
875
|
+
}
|
|
876
|
+
const projectLabel = `[${target.name}]`;
|
|
877
|
+
const startTime = Date.now();
|
|
878
|
+
// Cache CodeAnalyzer instance for incremental updates
|
|
879
|
+
let analyzer = analyzerInstances.get(target.dir);
|
|
880
|
+
if (!analyzer) {
|
|
881
|
+
analyzer = new CodeAnalyzer(target.dir, 5000);
|
|
882
|
+
analyzerInstances.set(target.dir, analyzer);
|
|
883
|
+
}
|
|
884
|
+
let result;
|
|
885
|
+
if (changedFilePath) {
|
|
886
|
+
const relPath = path.relative(target.dir, changedFilePath);
|
|
887
|
+
console.error(`[Indexing] ⚡ ${projectLabel} Incremental indexing file: ${relPath}`);
|
|
888
|
+
result = await analyzer.analyzeFileIncremental(changedFilePath);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
console.error(`[Indexing] 🔍 ${projectLabel} Starting AST indexing: ${target.dir}`);
|
|
892
|
+
result = await analyzer.analyzeProject((percent, done, total, currentFile) => {
|
|
893
|
+
const fileMsg = currentFile ? ` — current: ${currentFile}` : '';
|
|
894
|
+
console.error(`[Indexing] ⏳ ${projectLabel} ${percent}% (${done}/${total} files)${fileMsg}`);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
898
|
+
const { totalFilesAnalyzed, entityCounts } = result;
|
|
899
|
+
if (changedFilePath) {
|
|
900
|
+
console.error(`[Indexing] ✅ ${projectLabel} Incremental re-indexed in ${elapsed}s — ` +
|
|
901
|
+
`Total: ${totalFilesAnalyzed} files | ${entityCounts.modules} modules | ` +
|
|
902
|
+
`${entityCounts.classes} classes | ${entityCounts.functions} functions`);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
console.error(`[Indexing] ✅ ${projectLabel} Done in ${elapsed}s — ` +
|
|
906
|
+
`${totalFilesAnalyzed} files | ${entityCounts.modules} modules | ` +
|
|
907
|
+
`${entityCounts.classes} classes | ${entityCounts.functions} functions`);
|
|
908
|
+
}
|
|
909
|
+
// Save in memory
|
|
910
|
+
inMemoryAnalysisCache.set(target.dir, result);
|
|
911
|
+
// Securely sync analysis to the CodeAtlas Remote VPS Cloud
|
|
912
|
+
syncAnalysisToServer(target.name, result).catch((err) => {
|
|
913
|
+
console.error(`[Auto-Scan] ❌ Background secure cloud sync failed: ${err}`);
|
|
914
|
+
});
|
|
915
|
+
return { analysis: result, projectName: target.name, projectDir: target.dir };
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
console.error(`[Auto-Scan] ❌ Dynamic async scanning failed: ${err}`);
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
export function getResolvedApiKey() {
|
|
923
|
+
let key = process.env.CODEATLAS_API_KEY;
|
|
924
|
+
if (key && (key.startsWith("ca_") || key.startsWith("test-"))) {
|
|
925
|
+
return key;
|
|
926
|
+
}
|
|
927
|
+
const homeDir = os.homedir();
|
|
928
|
+
const pathsToTry = [
|
|
929
|
+
path.join(homeDir, ".gemini", "antigravity", "mcp_config.json"),
|
|
930
|
+
path.join(homeDir, ".cursor", "mcp.json"),
|
|
931
|
+
path.join(homeDir, ".codeatlas", "config.json"),
|
|
932
|
+
path.join(homeDir, ".config", "Claude", "claude_desktop_config.json"),
|
|
933
|
+
path.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
934
|
+
path.join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json"),
|
|
935
|
+
];
|
|
936
|
+
for (const filePath of pathsToTry) {
|
|
937
|
+
try {
|
|
938
|
+
if (fs.existsSync(filePath)) {
|
|
939
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
940
|
+
const parsed = JSON.parse(content);
|
|
941
|
+
if (parsed.mcpServers?.codeatlas?.env?.CODEATLAS_API_KEY) {
|
|
942
|
+
const foundKey = parsed.mcpServers.codeatlas.env.CODEATLAS_API_KEY;
|
|
943
|
+
if (foundKey && typeof foundKey === 'string' && foundKey.trim().length > 0) {
|
|
944
|
+
return foundKey.trim();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
for (const serverName of Object.keys(parsed.mcpServers || {})) {
|
|
948
|
+
if (serverName.toLowerCase().includes("codeatlas")) {
|
|
949
|
+
const foundKey = parsed.mcpServers[serverName]?.env?.CODEATLAS_API_KEY;
|
|
950
|
+
if (foundKey && typeof foundKey === 'string' && foundKey.trim().length > 0) {
|
|
951
|
+
return foundKey.trim();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
// ignore
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return undefined;
|
|
962
|
+
}
|
|
963
|
+
export async function syncAnalysisToServer(projectName, analysis, businessRule, changeDescription) {
|
|
964
|
+
const apiKey = getResolvedApiKey();
|
|
965
|
+
if (!apiKey) {
|
|
966
|
+
console.error("[Auto-Scan] ℹ️ CODEATLAS_API_KEY not set. Local analysis saved but cloud sync skipped.");
|
|
967
|
+
throw new Error("CODEATLAS_API_KEY is not set.");
|
|
968
|
+
}
|
|
969
|
+
return new Promise((resolve, reject) => {
|
|
970
|
+
try {
|
|
971
|
+
const payload = JSON.stringify({ projectName, analysis, businessRule, changeDescription });
|
|
972
|
+
const serverUrlStr = process.env.CODEATLAS_API_URL || "https://your-server.com/api";
|
|
973
|
+
const serverUrl = new URL(serverUrlStr);
|
|
974
|
+
const options = {
|
|
975
|
+
hostname: serverUrl.hostname,
|
|
976
|
+
port: serverUrl.port || (serverUrl.protocol === "https:" ? 443 : 80),
|
|
977
|
+
path: `/api/projects/sync`,
|
|
978
|
+
method: "POST",
|
|
979
|
+
headers: {
|
|
980
|
+
"Content-Type": "application/json",
|
|
981
|
+
"x-api-key": apiKey,
|
|
982
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
const req = https.request(options, (res) => {
|
|
986
|
+
let data = "";
|
|
987
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
988
|
+
res.on("end", () => {
|
|
989
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
990
|
+
console.error(`[Auto-Scan] ✅ Securely synced ${projectName} AST analysis to CodeAtlas Cloud!`);
|
|
991
|
+
resolve();
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
const errMsg = `Secure Cloud Sync failed with status ${res.statusCode}: ${data}`;
|
|
995
|
+
console.error(`[Auto-Scan] ❌ ${errMsg}`);
|
|
996
|
+
reject(new Error(errMsg));
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
req.on("error", (e) => {
|
|
1001
|
+
const errMsg = `Secure Cloud Sync Network Error: ${e.message}`;
|
|
1002
|
+
console.error(`[Auto-Scan] ❌ ${errMsg}`);
|
|
1003
|
+
reject(new Error(errMsg));
|
|
1004
|
+
});
|
|
1005
|
+
req.write(payload);
|
|
1006
|
+
req.end();
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
const errMsg = `Secure Cloud Sync Initialization Error: ${(err instanceof Error ? err.message : String(err))}`;
|
|
1010
|
+
console.error(`[Auto-Scan] ❌ ${errMsg}`);
|
|
1011
|
+
reject(new Error(errMsg));
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
export async function getEpisodicMemoriesFromServer(projectName, eventType) {
|
|
1016
|
+
const apiKey = getResolvedApiKey();
|
|
1017
|
+
if (!apiKey) {
|
|
1018
|
+
console.error("[Auto-Scan] ℹ️ CODEATLAS_API_KEY not set. Cannot fetch episodic memory from cloud.");
|
|
1019
|
+
throw new Error("CODEATLAS_API_KEY is not set.");
|
|
1020
|
+
}
|
|
1021
|
+
return new Promise((resolve, reject) => {
|
|
1022
|
+
try {
|
|
1023
|
+
const serverUrlStr = process.env.CODEATLAS_API_URL || "https://your-server.com/api";
|
|
1024
|
+
const serverUrl = new URL(serverUrlStr);
|
|
1025
|
+
let pathStr = `/api/projects/memory?projectName=${encodeURIComponent(projectName)}`;
|
|
1026
|
+
if (eventType) {
|
|
1027
|
+
pathStr += `&eventType=${encodeURIComponent(eventType)}`;
|
|
1028
|
+
}
|
|
1029
|
+
const options = {
|
|
1030
|
+
hostname: serverUrl.hostname,
|
|
1031
|
+
port: serverUrl.port || (serverUrl.protocol === "https:" ? 443 : 80),
|
|
1032
|
+
path: pathStr,
|
|
1033
|
+
method: "GET",
|
|
1034
|
+
headers: {
|
|
1035
|
+
"x-api-key": apiKey
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
const req = https.request(options, (res) => {
|
|
1039
|
+
let data = "";
|
|
1040
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
1041
|
+
res.on("end", () => {
|
|
1042
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
1043
|
+
try {
|
|
1044
|
+
const responseObj = JSON.parse(data);
|
|
1045
|
+
resolve(responseObj.memories || []);
|
|
1046
|
+
}
|
|
1047
|
+
catch (err) {
|
|
1048
|
+
reject(new Error(`Failed to parse memory response: ${data}`));
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
const errMsg = `Failed to get episodic memory from cloud: status ${res.statusCode}: ${data}`;
|
|
1053
|
+
reject(new Error(errMsg));
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
req.on("error", (e) => {
|
|
1058
|
+
const errMsg = `Memory Retrieval Network Error: ${e.message}`;
|
|
1059
|
+
reject(new Error(errMsg));
|
|
1060
|
+
});
|
|
1061
|
+
req.end();
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
reject(error);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
//# sourceMappingURL=projectService.js.map
|