@grafema/cli 0.1.1-alpha
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/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +36 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +209 -0
- package/dist/commands/check.d.ts +10 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +295 -0
- package/dist/commands/coverage.d.ts +11 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +96 -0
- package/dist/commands/explore.d.ts +6 -0
- package/dist/commands/explore.d.ts.map +1 -0
- package/dist/commands/explore.js +633 -0
- package/dist/commands/get.d.ts +10 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +189 -0
- package/dist/commands/impact.d.ts +10 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +313 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +91 -0
- package/dist/commands/query.d.ts +13 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +340 -0
- package/dist/commands/server.d.ts +11 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +300 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +52 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +270 -0
- package/dist/utils/codePreview.d.ts +28 -0
- package/dist/utils/codePreview.d.ts.map +1 -0
- package/dist/utils/codePreview.js +51 -0
- package/dist/utils/errorFormatter.d.ts +24 -0
- package/dist/utils/errorFormatter.d.ts.map +1 -0
- package/dist/utils/errorFormatter.js +32 -0
- package/dist/utils/formatNode.d.ts +53 -0
- package/dist/utils/formatNode.d.ts.map +1 -0
- package/dist/utils/formatNode.js +49 -0
- package/package.json +54 -0
- package/src/cli.ts +41 -0
- package/src/commands/analyze.ts +271 -0
- package/src/commands/check.ts +379 -0
- package/src/commands/coverage.ts +108 -0
- package/src/commands/explore.tsx +1056 -0
- package/src/commands/get.ts +265 -0
- package/src/commands/impact.ts +400 -0
- package/src/commands/init.ts +112 -0
- package/src/commands/overview.ts +108 -0
- package/src/commands/query.ts +425 -0
- package/src/commands/server.ts +335 -0
- package/src/commands/stats.ts +58 -0
- package/src/commands/trace.ts +341 -0
- package/src/utils/codePreview.ts +77 -0
- package/src/utils/errorFormatter.ts +35 -0
- package/src/utils/formatNode.ts +88 -0
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explore command - Interactive TUI for graph navigation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolve, join, relative } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import React, { useState, useEffect } from 'react';
|
|
10
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
11
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
12
|
+
import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
|
|
13
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
14
|
+
|
|
15
|
+
// Types
|
|
16
|
+
interface NodeInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
name: string;
|
|
20
|
+
file: string;
|
|
21
|
+
line?: number;
|
|
22
|
+
// Function-specific fields
|
|
23
|
+
async?: boolean;
|
|
24
|
+
exported?: boolean;
|
|
25
|
+
generator?: boolean;
|
|
26
|
+
arrowFunction?: boolean;
|
|
27
|
+
params?: string[];
|
|
28
|
+
paramTypes?: string[];
|
|
29
|
+
returnType?: string;
|
|
30
|
+
signature?: string;
|
|
31
|
+
jsdocSummary?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ExploreState {
|
|
35
|
+
currentNode: NodeInfo | null;
|
|
36
|
+
callers: NodeInfo[];
|
|
37
|
+
callees: NodeInfo[];
|
|
38
|
+
// For CLASS nodes
|
|
39
|
+
fields: NodeInfo[];
|
|
40
|
+
methods: NodeInfo[];
|
|
41
|
+
// For VARIABLE/field data flow
|
|
42
|
+
dataFlowSources: NodeInfo[];
|
|
43
|
+
dataFlowTargets: NodeInfo[];
|
|
44
|
+
breadcrumbs: NodeInfo[];
|
|
45
|
+
selectedIndex: number;
|
|
46
|
+
selectedPanel: 'callers' | 'callees' | 'search' | 'modules' | 'fields' | 'methods' | 'sources' | 'targets';
|
|
47
|
+
searchMode: boolean;
|
|
48
|
+
searchQuery: string;
|
|
49
|
+
searchResults: NodeInfo[];
|
|
50
|
+
modules: NodeInfo[];
|
|
51
|
+
loading: boolean;
|
|
52
|
+
error: string | null;
|
|
53
|
+
visibleCallers: number;
|
|
54
|
+
visibleCallees: number;
|
|
55
|
+
viewMode: 'function' | 'search' | 'modules' | 'class' | 'dataflow';
|
|
56
|
+
// Code preview
|
|
57
|
+
showCodePreview: boolean;
|
|
58
|
+
codePreviewLines: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ExplorerProps {
|
|
62
|
+
backend: RFDBServerBackend;
|
|
63
|
+
startNode: NodeInfo | null;
|
|
64
|
+
projectPath: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Main Explorer Component
|
|
68
|
+
function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
69
|
+
const { exit } = useApp();
|
|
70
|
+
|
|
71
|
+
const [state, setState] = useState<ExploreState>({
|
|
72
|
+
currentNode: startNode,
|
|
73
|
+
callers: [],
|
|
74
|
+
callees: [],
|
|
75
|
+
fields: [],
|
|
76
|
+
methods: [],
|
|
77
|
+
dataFlowSources: [],
|
|
78
|
+
dataFlowTargets: [],
|
|
79
|
+
breadcrumbs: startNode ? [startNode] : [],
|
|
80
|
+
selectedIndex: 0,
|
|
81
|
+
selectedPanel: 'callers',
|
|
82
|
+
searchMode: false,
|
|
83
|
+
searchQuery: '',
|
|
84
|
+
searchResults: [],
|
|
85
|
+
modules: [],
|
|
86
|
+
loading: true,
|
|
87
|
+
error: null,
|
|
88
|
+
visibleCallers: 10,
|
|
89
|
+
visibleCallees: 10,
|
|
90
|
+
viewMode: 'function',
|
|
91
|
+
showCodePreview: false,
|
|
92
|
+
codePreviewLines: [],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Load data when currentNode changes
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!state.currentNode) return;
|
|
98
|
+
|
|
99
|
+
const load = async () => {
|
|
100
|
+
setState(s => ({ ...s, loading: true }));
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const nodeType = state.currentNode!.type;
|
|
104
|
+
|
|
105
|
+
if (nodeType === 'CLASS') {
|
|
106
|
+
// Load fields and methods for class
|
|
107
|
+
const { fields, methods } = await getClassMembers(backend, state.currentNode!.id);
|
|
108
|
+
setState(s => ({
|
|
109
|
+
...s,
|
|
110
|
+
fields,
|
|
111
|
+
methods,
|
|
112
|
+
viewMode: 'class',
|
|
113
|
+
selectedPanel: 'fields',
|
|
114
|
+
loading: false,
|
|
115
|
+
selectedIndex: 0,
|
|
116
|
+
}));
|
|
117
|
+
} else if (nodeType === 'VARIABLE' || nodeType === 'PARAMETER') {
|
|
118
|
+
// Load data flow for variable
|
|
119
|
+
const { sources, targets } = await getDataFlow(backend, state.currentNode!.id);
|
|
120
|
+
setState(s => ({
|
|
121
|
+
...s,
|
|
122
|
+
dataFlowSources: sources,
|
|
123
|
+
dataFlowTargets: targets,
|
|
124
|
+
viewMode: 'dataflow',
|
|
125
|
+
selectedPanel: 'sources',
|
|
126
|
+
loading: false,
|
|
127
|
+
selectedIndex: 0,
|
|
128
|
+
}));
|
|
129
|
+
} else {
|
|
130
|
+
// Load callers/callees for function
|
|
131
|
+
const callers = await getCallers(backend, state.currentNode!.id, 50);
|
|
132
|
+
const callees = await getCallees(backend, state.currentNode!.id, 50);
|
|
133
|
+
setState(s => ({
|
|
134
|
+
...s,
|
|
135
|
+
callers,
|
|
136
|
+
callees,
|
|
137
|
+
viewMode: 'function',
|
|
138
|
+
selectedPanel: 'callers',
|
|
139
|
+
loading: false,
|
|
140
|
+
selectedIndex: 0,
|
|
141
|
+
visibleCallers: 10,
|
|
142
|
+
visibleCallees: 10,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
setState(s => ({
|
|
147
|
+
...s,
|
|
148
|
+
error: (err as Error).message,
|
|
149
|
+
loading: false,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
load();
|
|
155
|
+
}, [state.currentNode?.id]);
|
|
156
|
+
|
|
157
|
+
// Get current list based on view mode and panel
|
|
158
|
+
const getCurrentList = (): NodeInfo[] => {
|
|
159
|
+
if (state.viewMode === 'search') return state.searchResults;
|
|
160
|
+
if (state.viewMode === 'modules') return state.modules;
|
|
161
|
+
if (state.viewMode === 'class') {
|
|
162
|
+
return state.selectedPanel === 'fields' ? state.fields : state.methods;
|
|
163
|
+
}
|
|
164
|
+
if (state.viewMode === 'dataflow') {
|
|
165
|
+
return state.selectedPanel === 'sources' ? state.dataFlowSources : state.dataFlowTargets;
|
|
166
|
+
}
|
|
167
|
+
return state.selectedPanel === 'callers' ? state.callers : state.callees;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Keyboard input
|
|
171
|
+
useInput((input, key) => {
|
|
172
|
+
if (state.searchMode) {
|
|
173
|
+
if (key.escape) {
|
|
174
|
+
setState(s => ({ ...s, searchMode: false, searchQuery: '' }));
|
|
175
|
+
} else if (key.return) {
|
|
176
|
+
performSearch(state.searchQuery);
|
|
177
|
+
setState(s => ({ ...s, searchMode: false }));
|
|
178
|
+
} else if (key.backspace || key.delete) {
|
|
179
|
+
setState(s => ({ ...s, searchQuery: s.searchQuery.slice(0, -1) }));
|
|
180
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
181
|
+
setState(s => ({ ...s, searchQuery: s.searchQuery + input }));
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Normal mode
|
|
187
|
+
if (input === 'q') {
|
|
188
|
+
exit();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (input === '/') {
|
|
193
|
+
setState(s => ({ ...s, searchMode: true, searchQuery: '' }));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (input === '?') {
|
|
198
|
+
// TODO: show help
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 'm' - show modules view
|
|
203
|
+
if (input === 'm') {
|
|
204
|
+
loadModules();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Space - toggle code preview
|
|
209
|
+
if (input === ' ') {
|
|
210
|
+
if (state.currentNode && state.currentNode.file && state.currentNode.line) {
|
|
211
|
+
if (state.showCodePreview) {
|
|
212
|
+
// Hide code preview
|
|
213
|
+
setState(s => ({ ...s, showCodePreview: false, codePreviewLines: [] }));
|
|
214
|
+
} else {
|
|
215
|
+
// Show code preview
|
|
216
|
+
const preview = getCodePreview({
|
|
217
|
+
file: state.currentNode.file,
|
|
218
|
+
line: state.currentNode.line,
|
|
219
|
+
});
|
|
220
|
+
if (preview) {
|
|
221
|
+
const formatted = formatCodePreview(preview, state.currentNode.line);
|
|
222
|
+
setState(s => ({ ...s, showCodePreview: true, codePreviewLines: formatted }));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 'o' - open in editor
|
|
230
|
+
if (input === 'o') {
|
|
231
|
+
if (state.currentNode && state.currentNode.file) {
|
|
232
|
+
const editor = process.env.EDITOR || 'code';
|
|
233
|
+
const file = state.currentNode.file;
|
|
234
|
+
const line = state.currentNode.line;
|
|
235
|
+
try {
|
|
236
|
+
if (editor.includes('code')) {
|
|
237
|
+
// VS Code supports --goto
|
|
238
|
+
execSync(`${editor} --goto "${file}:${line || 1}"`, { stdio: 'ignore' });
|
|
239
|
+
} else {
|
|
240
|
+
// Generic editor
|
|
241
|
+
execSync(`${editor} +${line || 1} "${file}"`, { stdio: 'ignore' });
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Ignore editor errors
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Arrow keys for panel switching
|
|
251
|
+
if (key.leftArrow || input === 'h') {
|
|
252
|
+
if (state.viewMode === 'function') {
|
|
253
|
+
setState(s => ({ ...s, selectedPanel: 'callers', selectedIndex: 0 }));
|
|
254
|
+
} else if (state.viewMode === 'class') {
|
|
255
|
+
setState(s => ({ ...s, selectedPanel: 'fields', selectedIndex: 0 }));
|
|
256
|
+
} else if (state.viewMode === 'dataflow') {
|
|
257
|
+
setState(s => ({ ...s, selectedPanel: 'sources', selectedIndex: 0 }));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (key.rightArrow || input === 'l') {
|
|
263
|
+
if (state.viewMode === 'function') {
|
|
264
|
+
setState(s => ({ ...s, selectedPanel: 'callees', selectedIndex: 0 }));
|
|
265
|
+
} else if (state.viewMode === 'class') {
|
|
266
|
+
setState(s => ({ ...s, selectedPanel: 'methods', selectedIndex: 0 }));
|
|
267
|
+
} else if (state.viewMode === 'dataflow') {
|
|
268
|
+
setState(s => ({ ...s, selectedPanel: 'targets', selectedIndex: 0 }));
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Up arrow - works in all modes
|
|
274
|
+
if (key.upArrow || input === 'k') {
|
|
275
|
+
setState(s => ({
|
|
276
|
+
...s,
|
|
277
|
+
selectedIndex: Math.max(0, s.selectedIndex - 1),
|
|
278
|
+
}));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Down arrow - works in all modes
|
|
283
|
+
if (key.downArrow || input === 'j') {
|
|
284
|
+
const list = getCurrentList();
|
|
285
|
+
setState(s => {
|
|
286
|
+
const newIndex = Math.min(list.length - 1, s.selectedIndex + 1);
|
|
287
|
+
return { ...s, selectedIndex: newIndex };
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Enter - select item
|
|
293
|
+
if (key.return) {
|
|
294
|
+
const list = getCurrentList();
|
|
295
|
+
const selected = list[state.selectedIndex];
|
|
296
|
+
if (selected) {
|
|
297
|
+
// Don't navigate into recursive calls (same function)
|
|
298
|
+
if (selected.id !== state.currentNode?.id) {
|
|
299
|
+
navigateTo(selected);
|
|
300
|
+
setState(s => ({ ...s, viewMode: 'function', selectedPanel: 'callers' }));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (key.backspace || key.delete) {
|
|
307
|
+
goBack();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (input === 'tab') {
|
|
312
|
+
setState(s => ({
|
|
313
|
+
...s,
|
|
314
|
+
selectedPanel: s.selectedPanel === 'callers' ? 'callees' : 'callers',
|
|
315
|
+
selectedIndex: 0,
|
|
316
|
+
}));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const navigateTo = (node: NodeInfo) => {
|
|
322
|
+
setState(s => ({
|
|
323
|
+
...s,
|
|
324
|
+
currentNode: node,
|
|
325
|
+
breadcrumbs: [...s.breadcrumbs, node],
|
|
326
|
+
selectedIndex: 0,
|
|
327
|
+
}));
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const goBack = () => {
|
|
331
|
+
// From search/modules view, go back to function view
|
|
332
|
+
if (state.viewMode !== 'function') {
|
|
333
|
+
setState(s => ({
|
|
334
|
+
...s,
|
|
335
|
+
viewMode: 'function',
|
|
336
|
+
selectedPanel: 'callers',
|
|
337
|
+
selectedIndex: 0,
|
|
338
|
+
}));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// From function view with breadcrumbs, go back
|
|
343
|
+
if (state.breadcrumbs.length > 1) {
|
|
344
|
+
const newBreadcrumbs = state.breadcrumbs.slice(0, -1);
|
|
345
|
+
const previousNode = newBreadcrumbs[newBreadcrumbs.length - 1];
|
|
346
|
+
|
|
347
|
+
setState(s => ({
|
|
348
|
+
...s,
|
|
349
|
+
currentNode: previousNode,
|
|
350
|
+
breadcrumbs: newBreadcrumbs,
|
|
351
|
+
selectedIndex: 0,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const performSearch = async (query: string) => {
|
|
357
|
+
if (!query.trim()) return;
|
|
358
|
+
|
|
359
|
+
setState(s => ({ ...s, loading: true }));
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const results = await searchNodes(backend, query, 20);
|
|
363
|
+
setState(s => ({
|
|
364
|
+
...s,
|
|
365
|
+
searchResults: results,
|
|
366
|
+
viewMode: 'search',
|
|
367
|
+
selectedPanel: 'search',
|
|
368
|
+
selectedIndex: 0,
|
|
369
|
+
loading: false,
|
|
370
|
+
error: results.length === 0 ? `No results for "${query}"` : null,
|
|
371
|
+
}));
|
|
372
|
+
} catch (err) {
|
|
373
|
+
setState(s => ({
|
|
374
|
+
...s,
|
|
375
|
+
error: (err as Error).message,
|
|
376
|
+
loading: false,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const loadModules = async () => {
|
|
382
|
+
setState(s => ({ ...s, loading: true }));
|
|
383
|
+
try {
|
|
384
|
+
const modules = await getModules(backend, 50);
|
|
385
|
+
setState(s => ({
|
|
386
|
+
...s,
|
|
387
|
+
modules,
|
|
388
|
+
viewMode: 'modules',
|
|
389
|
+
selectedPanel: 'modules',
|
|
390
|
+
selectedIndex: 0,
|
|
391
|
+
loading: false,
|
|
392
|
+
}));
|
|
393
|
+
} catch (err) {
|
|
394
|
+
setState(s => ({
|
|
395
|
+
...s,
|
|
396
|
+
error: (err as Error).message,
|
|
397
|
+
loading: false,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const formatLoc = (node: NodeInfo) => {
|
|
403
|
+
if (!node.file) return '';
|
|
404
|
+
const rel = relative(projectPath, node.file);
|
|
405
|
+
return node.line ? `${rel}:${node.line}` : rel;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Render
|
|
409
|
+
if (!state.currentNode) {
|
|
410
|
+
return (
|
|
411
|
+
<Box flexDirection="column" padding={1}>
|
|
412
|
+
<Text color="yellow">No function selected.</Text>
|
|
413
|
+
<Text>Press / to search, q to quit.</Text>
|
|
414
|
+
{state.searchMode && (
|
|
415
|
+
<Box marginTop={1}>
|
|
416
|
+
<Text>Search: </Text>
|
|
417
|
+
<Text color="cyan">{state.searchQuery}</Text>
|
|
418
|
+
<Text color="gray">_</Text>
|
|
419
|
+
</Box>
|
|
420
|
+
)}
|
|
421
|
+
</Box>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Build badges for current node
|
|
426
|
+
const badges: string[] = [];
|
|
427
|
+
if (state.currentNode.async) badges.push('async');
|
|
428
|
+
if (state.currentNode.exported) badges.push('exp');
|
|
429
|
+
if (state.currentNode.generator) badges.push('gen');
|
|
430
|
+
if (state.currentNode.arrowFunction) badges.push('arrow');
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<Box flexDirection="column" borderStyle="round" borderColor="blue" padding={1}>
|
|
434
|
+
{/* Header with badges */}
|
|
435
|
+
<Box marginBottom={1}>
|
|
436
|
+
<Text bold color="cyan">Grafema Explorer</Text>
|
|
437
|
+
{state.loading && <Text color="yellow"> (loading...)</Text>}
|
|
438
|
+
{badges.length > 0 && (
|
|
439
|
+
<Text>
|
|
440
|
+
{' '}
|
|
441
|
+
{badges.map((badge, i) => (
|
|
442
|
+
<Text key={`badge-${i}`}>
|
|
443
|
+
<Text color="magenta">[{badge}]</Text>
|
|
444
|
+
{i < badges.length - 1 ? ' ' : ''}
|
|
445
|
+
</Text>
|
|
446
|
+
))}
|
|
447
|
+
</Text>
|
|
448
|
+
)}
|
|
449
|
+
</Box>
|
|
450
|
+
|
|
451
|
+
{/* Breadcrumbs */}
|
|
452
|
+
<Box marginBottom={1}>
|
|
453
|
+
<Text color="gray">
|
|
454
|
+
{state.breadcrumbs.map((b, i) => (
|
|
455
|
+
<Text key={`bc-${i}-${b.id}`}>
|
|
456
|
+
{i > 0 ? ' → ' : ''}
|
|
457
|
+
<Text color={i === state.breadcrumbs.length - 1 ? 'white' : 'gray'}>
|
|
458
|
+
{b.name}
|
|
459
|
+
</Text>
|
|
460
|
+
</Text>
|
|
461
|
+
))}
|
|
462
|
+
</Text>
|
|
463
|
+
</Box>
|
|
464
|
+
|
|
465
|
+
{/* Current node info with signature */}
|
|
466
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
467
|
+
<Text>
|
|
468
|
+
<Text color="green" bold>{state.currentNode.type}</Text>
|
|
469
|
+
<Text>: </Text>
|
|
470
|
+
<Text bold>{state.currentNode.name}</Text>
|
|
471
|
+
</Text>
|
|
472
|
+
{state.currentNode.signature && (
|
|
473
|
+
<Text color="yellow">{state.currentNode.signature}</Text>
|
|
474
|
+
)}
|
|
475
|
+
{state.currentNode.jsdocSummary && (
|
|
476
|
+
<Text color="gray" italic> {state.currentNode.jsdocSummary}</Text>
|
|
477
|
+
)}
|
|
478
|
+
<Text color="gray">{formatLoc(state.currentNode)}</Text>
|
|
479
|
+
</Box>
|
|
480
|
+
|
|
481
|
+
{/* Content based on view mode */}
|
|
482
|
+
{state.viewMode === 'search' && (
|
|
483
|
+
<Box flexDirection="column">
|
|
484
|
+
<Text bold color="cyan">Search Results ({state.searchResults.length}):</Text>
|
|
485
|
+
{state.searchResults.length === 0 ? (
|
|
486
|
+
<Text color="gray"> No results</Text>
|
|
487
|
+
) : (
|
|
488
|
+
state.searchResults.map((item, i) => (
|
|
489
|
+
<Text key={`search-${i}-${item.id}`}>
|
|
490
|
+
{i === state.selectedIndex ? (
|
|
491
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
492
|
+
) : (
|
|
493
|
+
<Text>{' '}</Text>
|
|
494
|
+
)}
|
|
495
|
+
<Text color={i === state.selectedIndex ? 'white' : 'gray'}>
|
|
496
|
+
<Text color="green">{item.type}</Text> {item.name}
|
|
497
|
+
</Text>
|
|
498
|
+
<Text color="gray" dimColor> {formatLoc(item)}</Text>
|
|
499
|
+
</Text>
|
|
500
|
+
))
|
|
501
|
+
)}
|
|
502
|
+
</Box>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{state.viewMode === 'modules' && (
|
|
506
|
+
<Box flexDirection="column">
|
|
507
|
+
<Text bold color="cyan">Modules ({state.modules.length}):</Text>
|
|
508
|
+
{state.modules.length === 0 ? (
|
|
509
|
+
<Text color="gray"> No modules</Text>
|
|
510
|
+
) : (
|
|
511
|
+
state.modules.slice(0, 20).map((mod, i) => (
|
|
512
|
+
<Text key={`mod-${i}-${mod.id}`}>
|
|
513
|
+
{i === state.selectedIndex ? (
|
|
514
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
515
|
+
) : (
|
|
516
|
+
<Text>{' '}</Text>
|
|
517
|
+
)}
|
|
518
|
+
<Text color={i === state.selectedIndex ? 'white' : 'gray'}>
|
|
519
|
+
{formatLoc(mod)}
|
|
520
|
+
</Text>
|
|
521
|
+
</Text>
|
|
522
|
+
))
|
|
523
|
+
)}
|
|
524
|
+
{state.modules.length > 20 && (
|
|
525
|
+
<Text color="gray"> ↓ {state.modules.length - 20} more</Text>
|
|
526
|
+
)}
|
|
527
|
+
</Box>
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
{state.viewMode === 'function' && (
|
|
531
|
+
<Box>
|
|
532
|
+
{/* Callers column */}
|
|
533
|
+
<Box flexDirection="column" width="50%" paddingRight={1}>
|
|
534
|
+
<Text bold color={state.selectedPanel === 'callers' ? 'cyan' : 'gray'}>
|
|
535
|
+
Called by ({state.callers.length}):
|
|
536
|
+
</Text>
|
|
537
|
+
{state.callers.length === 0 ? (
|
|
538
|
+
<Text color="gray"> (none)</Text>
|
|
539
|
+
) : (
|
|
540
|
+
state.callers.slice(0, state.visibleCallers).map((caller, i) => {
|
|
541
|
+
const isRecursive = caller.id === state.currentNode?.id;
|
|
542
|
+
return (
|
|
543
|
+
<Text key={`caller-${i}-${caller.id}`}>
|
|
544
|
+
{state.selectedPanel === 'callers' && i === state.selectedIndex ? (
|
|
545
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
546
|
+
) : (
|
|
547
|
+
<Text>{' '}</Text>
|
|
548
|
+
)}
|
|
549
|
+
<Text color={isRecursive ? 'yellow' : (state.selectedPanel === 'callers' && i === state.selectedIndex ? 'white' : 'gray')}>
|
|
550
|
+
{caller.name}{isRecursive ? ' ↻' : ''}
|
|
551
|
+
</Text>
|
|
552
|
+
</Text>
|
|
553
|
+
);
|
|
554
|
+
})
|
|
555
|
+
)}
|
|
556
|
+
{state.callers.length > state.visibleCallers && (
|
|
557
|
+
<Text color="gray"> ↓ {state.callers.length - state.visibleCallers} more</Text>
|
|
558
|
+
)}
|
|
559
|
+
</Box>
|
|
560
|
+
|
|
561
|
+
{/* Callees column */}
|
|
562
|
+
<Box flexDirection="column" width="50%" paddingLeft={1}>
|
|
563
|
+
<Text bold color={state.selectedPanel === 'callees' ? 'cyan' : 'gray'}>
|
|
564
|
+
Calls ({state.callees.length}):
|
|
565
|
+
</Text>
|
|
566
|
+
{state.callees.length === 0 ? (
|
|
567
|
+
<Text color="gray"> (none)</Text>
|
|
568
|
+
) : (
|
|
569
|
+
state.callees.slice(0, state.visibleCallees).map((callee, i) => {
|
|
570
|
+
const isRecursive = callee.id === state.currentNode?.id;
|
|
571
|
+
return (
|
|
572
|
+
<Text key={`callee-${i}-${callee.id}`}>
|
|
573
|
+
{state.selectedPanel === 'callees' && i === state.selectedIndex ? (
|
|
574
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
575
|
+
) : (
|
|
576
|
+
<Text>{' '}</Text>
|
|
577
|
+
)}
|
|
578
|
+
<Text color={isRecursive ? 'yellow' : (state.selectedPanel === 'callees' && i === state.selectedIndex ? 'white' : 'gray')}>
|
|
579
|
+
{callee.name}{isRecursive ? ' ↻' : ''}
|
|
580
|
+
</Text>
|
|
581
|
+
</Text>
|
|
582
|
+
);
|
|
583
|
+
})
|
|
584
|
+
)}
|
|
585
|
+
{state.callees.length > state.visibleCallees && (
|
|
586
|
+
<Text color="gray"> ↓ {state.callees.length - state.visibleCallees} more</Text>
|
|
587
|
+
)}
|
|
588
|
+
</Box>
|
|
589
|
+
</Box>
|
|
590
|
+
)}
|
|
591
|
+
|
|
592
|
+
{state.viewMode === 'class' && (
|
|
593
|
+
<Box>
|
|
594
|
+
{/* Fields column */}
|
|
595
|
+
<Box flexDirection="column" width="50%" paddingRight={1}>
|
|
596
|
+
<Text bold color={state.selectedPanel === 'fields' ? 'cyan' : 'gray'}>
|
|
597
|
+
Fields ({state.fields.length}):
|
|
598
|
+
</Text>
|
|
599
|
+
{state.fields.length === 0 ? (
|
|
600
|
+
<Text color="gray"> (none)</Text>
|
|
601
|
+
) : (
|
|
602
|
+
state.fields.slice(0, 15).map((field, i) => (
|
|
603
|
+
<Text key={`field-${i}-${field.id}`}>
|
|
604
|
+
{state.selectedPanel === 'fields' && i === state.selectedIndex ? (
|
|
605
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
606
|
+
) : (
|
|
607
|
+
<Text>{' '}</Text>
|
|
608
|
+
)}
|
|
609
|
+
<Text color={state.selectedPanel === 'fields' && i === state.selectedIndex ? 'white' : 'gray'}>
|
|
610
|
+
{field.name}
|
|
611
|
+
</Text>
|
|
612
|
+
</Text>
|
|
613
|
+
))
|
|
614
|
+
)}
|
|
615
|
+
{state.fields.length > 15 && (
|
|
616
|
+
<Text color="gray"> ↓ {state.fields.length - 15} more</Text>
|
|
617
|
+
)}
|
|
618
|
+
</Box>
|
|
619
|
+
|
|
620
|
+
{/* Methods column */}
|
|
621
|
+
<Box flexDirection="column" width="50%" paddingLeft={1}>
|
|
622
|
+
<Text bold color={state.selectedPanel === 'methods' ? 'cyan' : 'gray'}>
|
|
623
|
+
Methods ({state.methods.length}):
|
|
624
|
+
</Text>
|
|
625
|
+
{state.methods.length === 0 ? (
|
|
626
|
+
<Text color="gray"> (none)</Text>
|
|
627
|
+
) : (
|
|
628
|
+
state.methods.slice(0, 15).map((method, i) => (
|
|
629
|
+
<Text key={`method-${i}-${method.id}`}>
|
|
630
|
+
{state.selectedPanel === 'methods' && i === state.selectedIndex ? (
|
|
631
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
632
|
+
) : (
|
|
633
|
+
<Text>{' '}</Text>
|
|
634
|
+
)}
|
|
635
|
+
<Text color={state.selectedPanel === 'methods' && i === state.selectedIndex ? 'white' : 'gray'}>
|
|
636
|
+
{method.name}()
|
|
637
|
+
</Text>
|
|
638
|
+
</Text>
|
|
639
|
+
))
|
|
640
|
+
)}
|
|
641
|
+
{state.methods.length > 15 && (
|
|
642
|
+
<Text color="gray"> ↓ {state.methods.length - 15} more</Text>
|
|
643
|
+
)}
|
|
644
|
+
</Box>
|
|
645
|
+
</Box>
|
|
646
|
+
)}
|
|
647
|
+
|
|
648
|
+
{state.viewMode === 'dataflow' && (
|
|
649
|
+
<Box>
|
|
650
|
+
{/* Sources column (where data comes from) */}
|
|
651
|
+
<Box flexDirection="column" width="50%" paddingRight={1}>
|
|
652
|
+
<Text bold color={state.selectedPanel === 'sources' ? 'cyan' : 'gray'}>
|
|
653
|
+
Data from ({state.dataFlowSources.length}):
|
|
654
|
+
</Text>
|
|
655
|
+
{state.dataFlowSources.length === 0 ? (
|
|
656
|
+
<Text color="gray"> (none)</Text>
|
|
657
|
+
) : (
|
|
658
|
+
state.dataFlowSources.slice(0, 15).map((src, i) => (
|
|
659
|
+
<Text key={`src-${i}-${src.id}`}>
|
|
660
|
+
{state.selectedPanel === 'sources' && i === state.selectedIndex ? (
|
|
661
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
662
|
+
) : (
|
|
663
|
+
<Text>{' '}</Text>
|
|
664
|
+
)}
|
|
665
|
+
<Text color={state.selectedPanel === 'sources' && i === state.selectedIndex ? 'white' : 'gray'}>
|
|
666
|
+
← {src.name} <Text dimColor>({src.type})</Text>
|
|
667
|
+
</Text>
|
|
668
|
+
</Text>
|
|
669
|
+
))
|
|
670
|
+
)}
|
|
671
|
+
</Box>
|
|
672
|
+
|
|
673
|
+
{/* Targets column (where data flows to) */}
|
|
674
|
+
<Box flexDirection="column" width="50%" paddingLeft={1}>
|
|
675
|
+
<Text bold color={state.selectedPanel === 'targets' ? 'cyan' : 'gray'}>
|
|
676
|
+
Flows to ({state.dataFlowTargets.length}):
|
|
677
|
+
</Text>
|
|
678
|
+
{state.dataFlowTargets.length === 0 ? (
|
|
679
|
+
<Text color="gray"> (none)</Text>
|
|
680
|
+
) : (
|
|
681
|
+
state.dataFlowTargets.slice(0, 15).map((tgt, i) => (
|
|
682
|
+
<Text key={`tgt-${i}-${tgt.id}`}>
|
|
683
|
+
{state.selectedPanel === 'targets' && i === state.selectedIndex ? (
|
|
684
|
+
<Text color="cyan" bold>{'> '}</Text>
|
|
685
|
+
) : (
|
|
686
|
+
<Text>{' '}</Text>
|
|
687
|
+
)}
|
|
688
|
+
<Text color={state.selectedPanel === 'targets' && i === state.selectedIndex ? 'white' : 'gray'}>
|
|
689
|
+
→ {tgt.name} <Text dimColor>({tgt.type})</Text>
|
|
690
|
+
</Text>
|
|
691
|
+
</Text>
|
|
692
|
+
))
|
|
693
|
+
)}
|
|
694
|
+
</Box>
|
|
695
|
+
</Box>
|
|
696
|
+
)}
|
|
697
|
+
|
|
698
|
+
{/* Code Preview Panel */}
|
|
699
|
+
{state.showCodePreview && state.codePreviewLines.length > 0 && (
|
|
700
|
+
<Box flexDirection="column" marginTop={1} borderStyle="single" borderColor="yellow" paddingX={1}>
|
|
701
|
+
<Text bold color="yellow">Code Preview:</Text>
|
|
702
|
+
{state.codePreviewLines.map((line, i) => (
|
|
703
|
+
<Text key={`code-${i}`} color={line.startsWith('>') ? 'white' : 'gray'}>
|
|
704
|
+
{line}
|
|
705
|
+
</Text>
|
|
706
|
+
))}
|
|
707
|
+
</Box>
|
|
708
|
+
)}
|
|
709
|
+
|
|
710
|
+
{/* Error message */}
|
|
711
|
+
{state.error && (
|
|
712
|
+
<Box marginTop={1}>
|
|
713
|
+
<Text color="red">Error: {state.error}</Text>
|
|
714
|
+
</Box>
|
|
715
|
+
)}
|
|
716
|
+
|
|
717
|
+
{/* Search mode */}
|
|
718
|
+
{state.searchMode && (
|
|
719
|
+
<Box marginTop={1}>
|
|
720
|
+
<Text>Search: </Text>
|
|
721
|
+
<Text color="cyan">{state.searchQuery}</Text>
|
|
722
|
+
<Text color="gray">_</Text>
|
|
723
|
+
</Box>
|
|
724
|
+
)}
|
|
725
|
+
|
|
726
|
+
{/* Help footer */}
|
|
727
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
728
|
+
<Text color="gray">
|
|
729
|
+
↑↓: Select | ←→: Panel | Enter: Open | Backspace: Back | /: Search | m: Modules | Space: Code | o: Editor | q: Quit
|
|
730
|
+
</Text>
|
|
731
|
+
</Box>
|
|
732
|
+
</Box>
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Helper function to extract NodeInfo with extended fields from a raw node
|
|
737
|
+
function extractNodeInfo(node: any): NodeInfo {
|
|
738
|
+
const nodeType = node.type || node.nodeType || 'UNKNOWN';
|
|
739
|
+
return {
|
|
740
|
+
id: node.id,
|
|
741
|
+
type: nodeType,
|
|
742
|
+
name: node.name || '<anonymous>',
|
|
743
|
+
file: node.file || '',
|
|
744
|
+
line: node.line,
|
|
745
|
+
// Function-specific fields
|
|
746
|
+
async: node.async,
|
|
747
|
+
exported: node.exported,
|
|
748
|
+
generator: node.generator,
|
|
749
|
+
arrowFunction: node.arrowFunction,
|
|
750
|
+
params: node.params,
|
|
751
|
+
paramTypes: node.paramTypes,
|
|
752
|
+
returnType: node.returnType,
|
|
753
|
+
signature: node.signature,
|
|
754
|
+
jsdocSummary: node.jsdocSummary,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Helper functions
|
|
759
|
+
async function getCallers(backend: RFDBServerBackend, nodeId: string, limit: number): Promise<NodeInfo[]> {
|
|
760
|
+
const callers: NodeInfo[] = [];
|
|
761
|
+
const seen = new Set<string>();
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
const callEdges = await backend.getIncomingEdges(nodeId, ['CALLS']);
|
|
765
|
+
|
|
766
|
+
for (const edge of callEdges) {
|
|
767
|
+
if (callers.length >= limit) break;
|
|
768
|
+
|
|
769
|
+
const callNode = await backend.getNode(edge.src);
|
|
770
|
+
if (!callNode) continue;
|
|
771
|
+
|
|
772
|
+
const containingFunc = await findContainingFunction(backend, callNode.id);
|
|
773
|
+
|
|
774
|
+
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
775
|
+
seen.add(containingFunc.id);
|
|
776
|
+
callers.push(containingFunc);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} catch {
|
|
780
|
+
// Ignore
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return callers;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function getCallees(backend: RFDBServerBackend, nodeId: string, limit: number): Promise<NodeInfo[]> {
|
|
787
|
+
const callees: NodeInfo[] = [];
|
|
788
|
+
const seen = new Set<string>();
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const callNodes = await findCallsInFunction(backend, nodeId);
|
|
792
|
+
|
|
793
|
+
for (const callNode of callNodes) {
|
|
794
|
+
if (callees.length >= limit) break;
|
|
795
|
+
|
|
796
|
+
const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
|
|
797
|
+
|
|
798
|
+
for (const edge of callEdges) {
|
|
799
|
+
if (callees.length >= limit) break;
|
|
800
|
+
|
|
801
|
+
const targetNode = await backend.getNode(edge.dst);
|
|
802
|
+
if (!targetNode || seen.has(targetNode.id)) continue;
|
|
803
|
+
|
|
804
|
+
seen.add(targetNode.id);
|
|
805
|
+
callees.push(extractNodeInfo(targetNode));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
// Ignore
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return callees;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function findContainingFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo | null> {
|
|
816
|
+
const visited = new Set<string>();
|
|
817
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
818
|
+
|
|
819
|
+
while (queue.length > 0) {
|
|
820
|
+
const { id, depth } = queue.shift()!;
|
|
821
|
+
if (visited.has(id) || depth > 15) continue;
|
|
822
|
+
visited.add(id);
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const edges = await backend.getIncomingEdges(id, null);
|
|
826
|
+
|
|
827
|
+
for (const edge of edges) {
|
|
828
|
+
const edgeType = (edge as any).edgeType || (edge as any).type;
|
|
829
|
+
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
|
|
830
|
+
|
|
831
|
+
const parent = await backend.getNode(edge.src);
|
|
832
|
+
if (!parent || visited.has(parent.id)) continue;
|
|
833
|
+
|
|
834
|
+
const parentType = (parent as any).type || (parent as any).nodeType;
|
|
835
|
+
|
|
836
|
+
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
837
|
+
return extractNodeInfo(parent);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
queue.push({ id: parent.id, depth: depth + 1 });
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
// Ignore
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function findCallsInFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo[]> {
|
|
851
|
+
const calls: NodeInfo[] = [];
|
|
852
|
+
const visited = new Set<string>();
|
|
853
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
854
|
+
|
|
855
|
+
while (queue.length > 0) {
|
|
856
|
+
const { id, depth } = queue.shift()!;
|
|
857
|
+
if (visited.has(id) || depth > 10) continue;
|
|
858
|
+
visited.add(id);
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
|
|
862
|
+
|
|
863
|
+
for (const edge of edges) {
|
|
864
|
+
const child = await backend.getNode(edge.dst);
|
|
865
|
+
if (!child) continue;
|
|
866
|
+
|
|
867
|
+
const childType = (child as any).type || (child as any).nodeType;
|
|
868
|
+
|
|
869
|
+
if (childType === 'CALL') {
|
|
870
|
+
calls.push({
|
|
871
|
+
id: child.id,
|
|
872
|
+
type: 'CALL',
|
|
873
|
+
name: (child as any).name || '',
|
|
874
|
+
file: (child as any).file || '',
|
|
875
|
+
line: (child as any).line,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (childType !== 'FUNCTION' && childType !== 'CLASS') {
|
|
880
|
+
queue.push({ id: child.id, depth: depth + 1 });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
// Ignore
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return calls;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async function searchNode(backend: RFDBServerBackend, query: string): Promise<NodeInfo | null> {
|
|
892
|
+
const results = await searchNodes(backend, query, 1);
|
|
893
|
+
return results[0] || null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async function searchNodes(backend: RFDBServerBackend, query: string, limit: number): Promise<NodeInfo[]> {
|
|
897
|
+
const results: NodeInfo[] = [];
|
|
898
|
+
const lowerQuery = query.toLowerCase();
|
|
899
|
+
|
|
900
|
+
for (const nodeType of ['FUNCTION', 'CLASS', 'MODULE']) {
|
|
901
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
|
|
902
|
+
const name = ((node as any).name || '').toLowerCase();
|
|
903
|
+
if (name === lowerQuery || name.includes(lowerQuery)) {
|
|
904
|
+
results.push(extractNodeInfo(node));
|
|
905
|
+
if (results.length >= limit) return results;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return results;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function getModules(backend: RFDBServerBackend, limit: number): Promise<NodeInfo[]> {
|
|
914
|
+
const modules: NodeInfo[] = [];
|
|
915
|
+
|
|
916
|
+
for await (const node of backend.queryNodes({ nodeType: 'MODULE' as any })) {
|
|
917
|
+
modules.push(extractNodeInfo(node));
|
|
918
|
+
if (modules.length >= limit) break;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Sort by file path for better navigation
|
|
922
|
+
modules.sort((a, b) => a.file.localeCompare(b.file));
|
|
923
|
+
|
|
924
|
+
return modules;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function getClassMembers(
|
|
928
|
+
backend: RFDBServerBackend,
|
|
929
|
+
classId: string
|
|
930
|
+
): Promise<{ fields: NodeInfo[]; methods: NodeInfo[] }> {
|
|
931
|
+
const fields: NodeInfo[] = [];
|
|
932
|
+
const methods: NodeInfo[] = [];
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
// Get children via CONTAINS edge
|
|
936
|
+
const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
937
|
+
|
|
938
|
+
for (const edge of edges) {
|
|
939
|
+
const child = await backend.getNode(edge.dst);
|
|
940
|
+
if (!child) continue;
|
|
941
|
+
|
|
942
|
+
const childType = (child as any).type || (child as any).nodeType;
|
|
943
|
+
const nodeInfo = extractNodeInfo(child);
|
|
944
|
+
|
|
945
|
+
if (childType === 'FUNCTION') {
|
|
946
|
+
methods.push(nodeInfo);
|
|
947
|
+
} else if (childType === 'VARIABLE' || childType === 'PARAMETER') {
|
|
948
|
+
fields.push(nodeInfo);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
// Ignore
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { fields, methods };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function getDataFlow(
|
|
959
|
+
backend: RFDBServerBackend,
|
|
960
|
+
varId: string
|
|
961
|
+
): Promise<{ sources: NodeInfo[]; targets: NodeInfo[] }> {
|
|
962
|
+
const sources: NodeInfo[] = [];
|
|
963
|
+
const targets: NodeInfo[] = [];
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
// Get incoming ASSIGNED_FROM edges (where data comes from)
|
|
967
|
+
const inEdges = await backend.getIncomingEdges(varId, ['ASSIGNED_FROM']);
|
|
968
|
+
for (const edge of inEdges) {
|
|
969
|
+
const src = await backend.getNode(edge.src);
|
|
970
|
+
if (src) {
|
|
971
|
+
sources.push(extractNodeInfo(src));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Get outgoing ASSIGNED_FROM edges (where data flows to)
|
|
976
|
+
const outEdges = await backend.getOutgoingEdges(varId, ['ASSIGNED_FROM']);
|
|
977
|
+
for (const edge of outEdges) {
|
|
978
|
+
const tgt = await backend.getNode(edge.dst);
|
|
979
|
+
if (tgt) {
|
|
980
|
+
targets.push(extractNodeInfo(tgt));
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Also check DERIVES_FROM for additional flow info
|
|
985
|
+
const derivesIn = await backend.getIncomingEdges(varId, ['DERIVES_FROM']);
|
|
986
|
+
for (const edge of derivesIn) {
|
|
987
|
+
const src = await backend.getNode(edge.src);
|
|
988
|
+
if (src && !sources.find(s => s.id === src.id)) {
|
|
989
|
+
sources.push(extractNodeInfo(src));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} catch {
|
|
993
|
+
// Ignore
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return { sources, targets };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function findStartNode(backend: RFDBServerBackend, startName: string | null): Promise<NodeInfo | null> {
|
|
1000
|
+
if (startName) {
|
|
1001
|
+
return searchNode(backend, startName);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Find first function with most callers
|
|
1005
|
+
let bestNode: NodeInfo | null = null;
|
|
1006
|
+
let bestCallerCount = 0;
|
|
1007
|
+
|
|
1008
|
+
let checked = 0;
|
|
1009
|
+
for await (const node of backend.queryNodes({ nodeType: 'FUNCTION' as any })) {
|
|
1010
|
+
const incoming = await backend.getIncomingEdges(node.id, ['CALLS']);
|
|
1011
|
+
if (incoming.length > bestCallerCount) {
|
|
1012
|
+
bestCallerCount = incoming.length;
|
|
1013
|
+
bestNode = extractNodeInfo(node);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
checked++;
|
|
1017
|
+
if (checked >= 100) break; // Limit search
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return bestNode;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Command
|
|
1024
|
+
export const exploreCommand = new Command('explore')
|
|
1025
|
+
.description('Interactive graph navigation')
|
|
1026
|
+
.argument('[start]', 'Starting function name')
|
|
1027
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
1028
|
+
.action(async (start: string | undefined, options: { project: string }) => {
|
|
1029
|
+
const projectPath = resolve(options.project);
|
|
1030
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
1031
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
1032
|
+
|
|
1033
|
+
if (!existsSync(dbPath)) {
|
|
1034
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
1038
|
+
|
|
1039
|
+
try {
|
|
1040
|
+
await backend.connect();
|
|
1041
|
+
|
|
1042
|
+
const startNode = await findStartNode(backend, start || null);
|
|
1043
|
+
|
|
1044
|
+
const { waitUntilExit } = render(
|
|
1045
|
+
<Explorer
|
|
1046
|
+
backend={backend}
|
|
1047
|
+
startNode={startNode}
|
|
1048
|
+
projectPath={projectPath}
|
|
1049
|
+
/>
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
await waitUntilExit();
|
|
1053
|
+
} finally {
|
|
1054
|
+
await backend.close();
|
|
1055
|
+
}
|
|
1056
|
+
});
|