@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,633 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Explore command - Interactive TUI for graph navigation
|
|
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 { 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
|
+
// Main Explorer Component
|
|
15
|
+
function Explorer({ backend, startNode, projectPath }) {
|
|
16
|
+
const { exit } = useApp();
|
|
17
|
+
const [state, setState] = useState({
|
|
18
|
+
currentNode: startNode,
|
|
19
|
+
callers: [],
|
|
20
|
+
callees: [],
|
|
21
|
+
fields: [],
|
|
22
|
+
methods: [],
|
|
23
|
+
dataFlowSources: [],
|
|
24
|
+
dataFlowTargets: [],
|
|
25
|
+
breadcrumbs: startNode ? [startNode] : [],
|
|
26
|
+
selectedIndex: 0,
|
|
27
|
+
selectedPanel: 'callers',
|
|
28
|
+
searchMode: false,
|
|
29
|
+
searchQuery: '',
|
|
30
|
+
searchResults: [],
|
|
31
|
+
modules: [],
|
|
32
|
+
loading: true,
|
|
33
|
+
error: null,
|
|
34
|
+
visibleCallers: 10,
|
|
35
|
+
visibleCallees: 10,
|
|
36
|
+
viewMode: 'function',
|
|
37
|
+
showCodePreview: false,
|
|
38
|
+
codePreviewLines: [],
|
|
39
|
+
});
|
|
40
|
+
// Load data when currentNode changes
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!state.currentNode)
|
|
43
|
+
return;
|
|
44
|
+
const load = async () => {
|
|
45
|
+
setState(s => ({ ...s, loading: true }));
|
|
46
|
+
try {
|
|
47
|
+
const nodeType = state.currentNode.type;
|
|
48
|
+
if (nodeType === 'CLASS') {
|
|
49
|
+
// Load fields and methods for class
|
|
50
|
+
const { fields, methods } = await getClassMembers(backend, state.currentNode.id);
|
|
51
|
+
setState(s => ({
|
|
52
|
+
...s,
|
|
53
|
+
fields,
|
|
54
|
+
methods,
|
|
55
|
+
viewMode: 'class',
|
|
56
|
+
selectedPanel: 'fields',
|
|
57
|
+
loading: false,
|
|
58
|
+
selectedIndex: 0,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
else if (nodeType === 'VARIABLE' || nodeType === 'PARAMETER') {
|
|
62
|
+
// Load data flow for variable
|
|
63
|
+
const { sources, targets } = await getDataFlow(backend, state.currentNode.id);
|
|
64
|
+
setState(s => ({
|
|
65
|
+
...s,
|
|
66
|
+
dataFlowSources: sources,
|
|
67
|
+
dataFlowTargets: targets,
|
|
68
|
+
viewMode: 'dataflow',
|
|
69
|
+
selectedPanel: 'sources',
|
|
70
|
+
loading: false,
|
|
71
|
+
selectedIndex: 0,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Load callers/callees for function
|
|
76
|
+
const callers = await getCallers(backend, state.currentNode.id, 50);
|
|
77
|
+
const callees = await getCallees(backend, state.currentNode.id, 50);
|
|
78
|
+
setState(s => ({
|
|
79
|
+
...s,
|
|
80
|
+
callers,
|
|
81
|
+
callees,
|
|
82
|
+
viewMode: 'function',
|
|
83
|
+
selectedPanel: 'callers',
|
|
84
|
+
loading: false,
|
|
85
|
+
selectedIndex: 0,
|
|
86
|
+
visibleCallers: 10,
|
|
87
|
+
visibleCallees: 10,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
setState(s => ({
|
|
93
|
+
...s,
|
|
94
|
+
error: err.message,
|
|
95
|
+
loading: false,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
load();
|
|
100
|
+
}, [state.currentNode?.id]);
|
|
101
|
+
// Get current list based on view mode and panel
|
|
102
|
+
const getCurrentList = () => {
|
|
103
|
+
if (state.viewMode === 'search')
|
|
104
|
+
return state.searchResults;
|
|
105
|
+
if (state.viewMode === 'modules')
|
|
106
|
+
return state.modules;
|
|
107
|
+
if (state.viewMode === 'class') {
|
|
108
|
+
return state.selectedPanel === 'fields' ? state.fields : state.methods;
|
|
109
|
+
}
|
|
110
|
+
if (state.viewMode === 'dataflow') {
|
|
111
|
+
return state.selectedPanel === 'sources' ? state.dataFlowSources : state.dataFlowTargets;
|
|
112
|
+
}
|
|
113
|
+
return state.selectedPanel === 'callers' ? state.callers : state.callees;
|
|
114
|
+
};
|
|
115
|
+
// Keyboard input
|
|
116
|
+
useInput((input, key) => {
|
|
117
|
+
if (state.searchMode) {
|
|
118
|
+
if (key.escape) {
|
|
119
|
+
setState(s => ({ ...s, searchMode: false, searchQuery: '' }));
|
|
120
|
+
}
|
|
121
|
+
else if (key.return) {
|
|
122
|
+
performSearch(state.searchQuery);
|
|
123
|
+
setState(s => ({ ...s, searchMode: false }));
|
|
124
|
+
}
|
|
125
|
+
else if (key.backspace || key.delete) {
|
|
126
|
+
setState(s => ({ ...s, searchQuery: s.searchQuery.slice(0, -1) }));
|
|
127
|
+
}
|
|
128
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
129
|
+
setState(s => ({ ...s, searchQuery: s.searchQuery + input }));
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Normal mode
|
|
134
|
+
if (input === 'q') {
|
|
135
|
+
exit();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (input === '/') {
|
|
139
|
+
setState(s => ({ ...s, searchMode: true, searchQuery: '' }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (input === '?') {
|
|
143
|
+
// TODO: show help
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// 'm' - show modules view
|
|
147
|
+
if (input === 'm') {
|
|
148
|
+
loadModules();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Space - toggle code preview
|
|
152
|
+
if (input === ' ') {
|
|
153
|
+
if (state.currentNode && state.currentNode.file && state.currentNode.line) {
|
|
154
|
+
if (state.showCodePreview) {
|
|
155
|
+
// Hide code preview
|
|
156
|
+
setState(s => ({ ...s, showCodePreview: false, codePreviewLines: [] }));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Show code preview
|
|
160
|
+
const preview = getCodePreview({
|
|
161
|
+
file: state.currentNode.file,
|
|
162
|
+
line: state.currentNode.line,
|
|
163
|
+
});
|
|
164
|
+
if (preview) {
|
|
165
|
+
const formatted = formatCodePreview(preview, state.currentNode.line);
|
|
166
|
+
setState(s => ({ ...s, showCodePreview: true, codePreviewLines: formatted }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// 'o' - open in editor
|
|
173
|
+
if (input === 'o') {
|
|
174
|
+
if (state.currentNode && state.currentNode.file) {
|
|
175
|
+
const editor = process.env.EDITOR || 'code';
|
|
176
|
+
const file = state.currentNode.file;
|
|
177
|
+
const line = state.currentNode.line;
|
|
178
|
+
try {
|
|
179
|
+
if (editor.includes('code')) {
|
|
180
|
+
// VS Code supports --goto
|
|
181
|
+
execSync(`${editor} --goto "${file}:${line || 1}"`, { stdio: 'ignore' });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Generic editor
|
|
185
|
+
execSync(`${editor} +${line || 1} "${file}"`, { stdio: 'ignore' });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Ignore editor errors
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Arrow keys for panel switching
|
|
195
|
+
if (key.leftArrow || input === 'h') {
|
|
196
|
+
if (state.viewMode === 'function') {
|
|
197
|
+
setState(s => ({ ...s, selectedPanel: 'callers', selectedIndex: 0 }));
|
|
198
|
+
}
|
|
199
|
+
else if (state.viewMode === 'class') {
|
|
200
|
+
setState(s => ({ ...s, selectedPanel: 'fields', selectedIndex: 0 }));
|
|
201
|
+
}
|
|
202
|
+
else if (state.viewMode === 'dataflow') {
|
|
203
|
+
setState(s => ({ ...s, selectedPanel: 'sources', selectedIndex: 0 }));
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (key.rightArrow || input === 'l') {
|
|
208
|
+
if (state.viewMode === 'function') {
|
|
209
|
+
setState(s => ({ ...s, selectedPanel: 'callees', selectedIndex: 0 }));
|
|
210
|
+
}
|
|
211
|
+
else if (state.viewMode === 'class') {
|
|
212
|
+
setState(s => ({ ...s, selectedPanel: 'methods', selectedIndex: 0 }));
|
|
213
|
+
}
|
|
214
|
+
else if (state.viewMode === 'dataflow') {
|
|
215
|
+
setState(s => ({ ...s, selectedPanel: 'targets', selectedIndex: 0 }));
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Up arrow - works in all modes
|
|
220
|
+
if (key.upArrow || input === 'k') {
|
|
221
|
+
setState(s => ({
|
|
222
|
+
...s,
|
|
223
|
+
selectedIndex: Math.max(0, s.selectedIndex - 1),
|
|
224
|
+
}));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Down arrow - works in all modes
|
|
228
|
+
if (key.downArrow || input === 'j') {
|
|
229
|
+
const list = getCurrentList();
|
|
230
|
+
setState(s => {
|
|
231
|
+
const newIndex = Math.min(list.length - 1, s.selectedIndex + 1);
|
|
232
|
+
return { ...s, selectedIndex: newIndex };
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Enter - select item
|
|
237
|
+
if (key.return) {
|
|
238
|
+
const list = getCurrentList();
|
|
239
|
+
const selected = list[state.selectedIndex];
|
|
240
|
+
if (selected) {
|
|
241
|
+
// Don't navigate into recursive calls (same function)
|
|
242
|
+
if (selected.id !== state.currentNode?.id) {
|
|
243
|
+
navigateTo(selected);
|
|
244
|
+
setState(s => ({ ...s, viewMode: 'function', selectedPanel: 'callers' }));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key.backspace || key.delete) {
|
|
250
|
+
goBack();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (input === 'tab') {
|
|
254
|
+
setState(s => ({
|
|
255
|
+
...s,
|
|
256
|
+
selectedPanel: s.selectedPanel === 'callers' ? 'callees' : 'callers',
|
|
257
|
+
selectedIndex: 0,
|
|
258
|
+
}));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const navigateTo = (node) => {
|
|
263
|
+
setState(s => ({
|
|
264
|
+
...s,
|
|
265
|
+
currentNode: node,
|
|
266
|
+
breadcrumbs: [...s.breadcrumbs, node],
|
|
267
|
+
selectedIndex: 0,
|
|
268
|
+
}));
|
|
269
|
+
};
|
|
270
|
+
const goBack = () => {
|
|
271
|
+
// From search/modules view, go back to function view
|
|
272
|
+
if (state.viewMode !== 'function') {
|
|
273
|
+
setState(s => ({
|
|
274
|
+
...s,
|
|
275
|
+
viewMode: 'function',
|
|
276
|
+
selectedPanel: 'callers',
|
|
277
|
+
selectedIndex: 0,
|
|
278
|
+
}));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// From function view with breadcrumbs, go back
|
|
282
|
+
if (state.breadcrumbs.length > 1) {
|
|
283
|
+
const newBreadcrumbs = state.breadcrumbs.slice(0, -1);
|
|
284
|
+
const previousNode = newBreadcrumbs[newBreadcrumbs.length - 1];
|
|
285
|
+
setState(s => ({
|
|
286
|
+
...s,
|
|
287
|
+
currentNode: previousNode,
|
|
288
|
+
breadcrumbs: newBreadcrumbs,
|
|
289
|
+
selectedIndex: 0,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const performSearch = async (query) => {
|
|
294
|
+
if (!query.trim())
|
|
295
|
+
return;
|
|
296
|
+
setState(s => ({ ...s, loading: true }));
|
|
297
|
+
try {
|
|
298
|
+
const results = await searchNodes(backend, query, 20);
|
|
299
|
+
setState(s => ({
|
|
300
|
+
...s,
|
|
301
|
+
searchResults: results,
|
|
302
|
+
viewMode: 'search',
|
|
303
|
+
selectedPanel: 'search',
|
|
304
|
+
selectedIndex: 0,
|
|
305
|
+
loading: false,
|
|
306
|
+
error: results.length === 0 ? `No results for "${query}"` : null,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
setState(s => ({
|
|
311
|
+
...s,
|
|
312
|
+
error: err.message,
|
|
313
|
+
loading: false,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const loadModules = async () => {
|
|
318
|
+
setState(s => ({ ...s, loading: true }));
|
|
319
|
+
try {
|
|
320
|
+
const modules = await getModules(backend, 50);
|
|
321
|
+
setState(s => ({
|
|
322
|
+
...s,
|
|
323
|
+
modules,
|
|
324
|
+
viewMode: 'modules',
|
|
325
|
+
selectedPanel: 'modules',
|
|
326
|
+
selectedIndex: 0,
|
|
327
|
+
loading: false,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
setState(s => ({
|
|
332
|
+
...s,
|
|
333
|
+
error: err.message,
|
|
334
|
+
loading: false,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const formatLoc = (node) => {
|
|
339
|
+
if (!node.file)
|
|
340
|
+
return '';
|
|
341
|
+
const rel = relative(projectPath, node.file);
|
|
342
|
+
return node.line ? `${rel}:${node.line}` : rel;
|
|
343
|
+
};
|
|
344
|
+
// Render
|
|
345
|
+
if (!state.currentNode) {
|
|
346
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "yellow", children: "No function selected." }), _jsx(Text, { children: "Press / to search, q to quit." }), state.searchMode && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(Text, { color: "cyan", children: state.searchQuery }), _jsx(Text, { color: "gray", children: "_" })] }))] }));
|
|
347
|
+
}
|
|
348
|
+
// Build badges for current node
|
|
349
|
+
const badges = [];
|
|
350
|
+
if (state.currentNode.async)
|
|
351
|
+
badges.push('async');
|
|
352
|
+
if (state.currentNode.exported)
|
|
353
|
+
badges.push('exp');
|
|
354
|
+
if (state.currentNode.generator)
|
|
355
|
+
badges.push('gen');
|
|
356
|
+
if (state.currentNode.arrowFunction)
|
|
357
|
+
badges.push('arrow');
|
|
358
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "blue", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Grafema Explorer" }), state.loading && _jsx(Text, { color: "yellow", children: " (loading...)" }), badges.length > 0 && (_jsxs(Text, { children: [' ', badges.map((badge, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", children: ["[", badge, "]"] }), i < badges.length - 1 ? ' ' : ''] }, `badge-${i}`)))] }))] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: state.breadcrumbs.map((b, i) => (_jsxs(Text, { children: [i > 0 ? ' → ' : '', _jsx(Text, { color: i === state.breadcrumbs.length - 1 ? 'white' : 'gray', children: b.name })] }, `bc-${i}-${b.id}`))) }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: state.currentNode.type }), _jsx(Text, { children: ": " }), _jsx(Text, { bold: true, children: state.currentNode.name })] }), state.currentNode.signature && (_jsx(Text, { color: "yellow", children: state.currentNode.signature })), state.currentNode.jsdocSummary && (_jsxs(Text, { color: "gray", italic: true, children: [" ", state.currentNode.jsdocSummary] })), _jsx(Text, { color: "gray", children: formatLoc(state.currentNode) })] }), state.viewMode === 'search' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Search Results (", state.searchResults.length, "):"] }), state.searchResults.length === 0 ? (_jsx(Text, { color: "gray", children: " No results" })) : (state.searchResults.map((item, i) => (_jsxs(Text, { children: [i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: i === state.selectedIndex ? 'white' : 'gray', children: [_jsx(Text, { color: "green", children: item.type }), " ", item.name] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" ", formatLoc(item)] })] }, `search-${i}-${item.id}`))))] })), state.viewMode === 'modules' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Modules (", state.modules.length, "):"] }), state.modules.length === 0 ? (_jsx(Text, { color: "gray", children: " No modules" })) : (state.modules.slice(0, 20).map((mod, i) => (_jsxs(Text, { children: [i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsx(Text, { color: i === state.selectedIndex ? 'white' : 'gray', children: formatLoc(mod) })] }, `mod-${i}-${mod.id}`)))), state.modules.length > 20 && (_jsxs(Text, { color: "gray", children: [" \u2193 ", state.modules.length - 20, " more"] }))] })), state.viewMode === 'function' && (_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: "50%", paddingRight: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'callers' ? 'cyan' : 'gray', children: ["Called by (", state.callers.length, "):"] }), state.callers.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.callers.slice(0, state.visibleCallers).map((caller, i) => {
|
|
359
|
+
const isRecursive = caller.id === state.currentNode?.id;
|
|
360
|
+
return (_jsxs(Text, { children: [state.selectedPanel === 'callers' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: isRecursive ? 'yellow' : (state.selectedPanel === 'callers' && i === state.selectedIndex ? 'white' : 'gray'), children: [caller.name, isRecursive ? ' ↻' : ''] })] }, `caller-${i}-${caller.id}`));
|
|
361
|
+
})), state.callers.length > state.visibleCallers && (_jsxs(Text, { color: "gray", children: [" \u2193 ", state.callers.length - state.visibleCallers, " more"] }))] }), _jsxs(Box, { flexDirection: "column", width: "50%", paddingLeft: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'callees' ? 'cyan' : 'gray', children: ["Calls (", state.callees.length, "):"] }), state.callees.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.callees.slice(0, state.visibleCallees).map((callee, i) => {
|
|
362
|
+
const isRecursive = callee.id === state.currentNode?.id;
|
|
363
|
+
return (_jsxs(Text, { children: [state.selectedPanel === 'callees' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: isRecursive ? 'yellow' : (state.selectedPanel === 'callees' && i === state.selectedIndex ? 'white' : 'gray'), children: [callee.name, isRecursive ? ' ↻' : ''] })] }, `callee-${i}-${callee.id}`));
|
|
364
|
+
})), state.callees.length > state.visibleCallees && (_jsxs(Text, { color: "gray", children: [" \u2193 ", state.callees.length - state.visibleCallees, " more"] }))] })] })), state.viewMode === 'class' && (_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: "50%", paddingRight: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'fields' ? 'cyan' : 'gray', children: ["Fields (", state.fields.length, "):"] }), state.fields.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.fields.slice(0, 15).map((field, i) => (_jsxs(Text, { children: [state.selectedPanel === 'fields' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsx(Text, { color: state.selectedPanel === 'fields' && i === state.selectedIndex ? 'white' : 'gray', children: field.name })] }, `field-${i}-${field.id}`)))), state.fields.length > 15 && (_jsxs(Text, { color: "gray", children: [" \u2193 ", state.fields.length - 15, " more"] }))] }), _jsxs(Box, { flexDirection: "column", width: "50%", paddingLeft: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'methods' ? 'cyan' : 'gray', children: ["Methods (", state.methods.length, "):"] }), state.methods.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.methods.slice(0, 15).map((method, i) => (_jsxs(Text, { children: [state.selectedPanel === 'methods' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: state.selectedPanel === 'methods' && i === state.selectedIndex ? 'white' : 'gray', children: [method.name, "()"] })] }, `method-${i}-${method.id}`)))), state.methods.length > 15 && (_jsxs(Text, { color: "gray", children: [" \u2193 ", state.methods.length - 15, " more"] }))] })] })), state.viewMode === 'dataflow' && (_jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: "50%", paddingRight: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'sources' ? 'cyan' : 'gray', children: ["Data from (", state.dataFlowSources.length, "):"] }), state.dataFlowSources.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.dataFlowSources.slice(0, 15).map((src, i) => (_jsxs(Text, { children: [state.selectedPanel === 'sources' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: state.selectedPanel === 'sources' && i === state.selectedIndex ? 'white' : 'gray', children: ["\u2190 ", src.name, " ", _jsxs(Text, { dimColor: true, children: ["(", src.type, ")"] })] })] }, `src-${i}-${src.id}`))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", paddingLeft: 1, children: [_jsxs(Text, { bold: true, color: state.selectedPanel === 'targets' ? 'cyan' : 'gray', children: ["Flows to (", state.dataFlowTargets.length, "):"] }), state.dataFlowTargets.length === 0 ? (_jsx(Text, { color: "gray", children: " (none)" })) : (state.dataFlowTargets.slice(0, 15).map((tgt, i) => (_jsxs(Text, { children: [state.selectedPanel === 'targets' && i === state.selectedIndex ? (_jsx(Text, { color: "cyan", bold: true, children: '> ' })) : (_jsx(Text, { children: ' ' })), _jsxs(Text, { color: state.selectedPanel === 'targets' && i === state.selectedIndex ? 'white' : 'gray', children: ["\u2192 ", tgt.name, " ", _jsxs(Text, { dimColor: true, children: ["(", tgt.type, ")"] })] })] }, `tgt-${i}-${tgt.id}`))))] })] })), state.showCodePreview && state.codePreviewLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Code Preview:" }), state.codePreviewLines.map((line, i) => (_jsx(Text, { color: line.startsWith('>') ? 'white' : 'gray', children: line }, `code-${i}`)))] })), state.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", state.error] }) })), state.searchMode && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(Text, { color: "cyan", children: state.searchQuery }), _jsx(Text, { color: "gray", children: "_" })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "\u2191\u2193: Select | \u2190\u2192: Panel | Enter: Open | Backspace: Back | /: Search | m: Modules | Space: Code | o: Editor | q: Quit" }) })] }));
|
|
365
|
+
}
|
|
366
|
+
// Helper function to extract NodeInfo with extended fields from a raw node
|
|
367
|
+
function extractNodeInfo(node) {
|
|
368
|
+
const nodeType = node.type || node.nodeType || 'UNKNOWN';
|
|
369
|
+
return {
|
|
370
|
+
id: node.id,
|
|
371
|
+
type: nodeType,
|
|
372
|
+
name: node.name || '<anonymous>',
|
|
373
|
+
file: node.file || '',
|
|
374
|
+
line: node.line,
|
|
375
|
+
// Function-specific fields
|
|
376
|
+
async: node.async,
|
|
377
|
+
exported: node.exported,
|
|
378
|
+
generator: node.generator,
|
|
379
|
+
arrowFunction: node.arrowFunction,
|
|
380
|
+
params: node.params,
|
|
381
|
+
paramTypes: node.paramTypes,
|
|
382
|
+
returnType: node.returnType,
|
|
383
|
+
signature: node.signature,
|
|
384
|
+
jsdocSummary: node.jsdocSummary,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// Helper functions
|
|
388
|
+
async function getCallers(backend, nodeId, limit) {
|
|
389
|
+
const callers = [];
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
try {
|
|
392
|
+
const callEdges = await backend.getIncomingEdges(nodeId, ['CALLS']);
|
|
393
|
+
for (const edge of callEdges) {
|
|
394
|
+
if (callers.length >= limit)
|
|
395
|
+
break;
|
|
396
|
+
const callNode = await backend.getNode(edge.src);
|
|
397
|
+
if (!callNode)
|
|
398
|
+
continue;
|
|
399
|
+
const containingFunc = await findContainingFunction(backend, callNode.id);
|
|
400
|
+
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
401
|
+
seen.add(containingFunc.id);
|
|
402
|
+
callers.push(containingFunc);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Ignore
|
|
408
|
+
}
|
|
409
|
+
return callers;
|
|
410
|
+
}
|
|
411
|
+
async function getCallees(backend, nodeId, limit) {
|
|
412
|
+
const callees = [];
|
|
413
|
+
const seen = new Set();
|
|
414
|
+
try {
|
|
415
|
+
const callNodes = await findCallsInFunction(backend, nodeId);
|
|
416
|
+
for (const callNode of callNodes) {
|
|
417
|
+
if (callees.length >= limit)
|
|
418
|
+
break;
|
|
419
|
+
const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
|
|
420
|
+
for (const edge of callEdges) {
|
|
421
|
+
if (callees.length >= limit)
|
|
422
|
+
break;
|
|
423
|
+
const targetNode = await backend.getNode(edge.dst);
|
|
424
|
+
if (!targetNode || seen.has(targetNode.id))
|
|
425
|
+
continue;
|
|
426
|
+
seen.add(targetNode.id);
|
|
427
|
+
callees.push(extractNodeInfo(targetNode));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Ignore
|
|
433
|
+
}
|
|
434
|
+
return callees;
|
|
435
|
+
}
|
|
436
|
+
async function findContainingFunction(backend, nodeId) {
|
|
437
|
+
const visited = new Set();
|
|
438
|
+
const queue = [{ id: nodeId, depth: 0 }];
|
|
439
|
+
while (queue.length > 0) {
|
|
440
|
+
const { id, depth } = queue.shift();
|
|
441
|
+
if (visited.has(id) || depth > 15)
|
|
442
|
+
continue;
|
|
443
|
+
visited.add(id);
|
|
444
|
+
try {
|
|
445
|
+
const edges = await backend.getIncomingEdges(id, null);
|
|
446
|
+
for (const edge of edges) {
|
|
447
|
+
const edgeType = edge.edgeType || edge.type;
|
|
448
|
+
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
|
|
449
|
+
continue;
|
|
450
|
+
const parent = await backend.getNode(edge.src);
|
|
451
|
+
if (!parent || visited.has(parent.id))
|
|
452
|
+
continue;
|
|
453
|
+
const parentType = parent.type || parent.nodeType;
|
|
454
|
+
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
455
|
+
return extractNodeInfo(parent);
|
|
456
|
+
}
|
|
457
|
+
queue.push({ id: parent.id, depth: depth + 1 });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Ignore
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
async function findCallsInFunction(backend, nodeId) {
|
|
467
|
+
const calls = [];
|
|
468
|
+
const visited = new Set();
|
|
469
|
+
const queue = [{ id: nodeId, depth: 0 }];
|
|
470
|
+
while (queue.length > 0) {
|
|
471
|
+
const { id, depth } = queue.shift();
|
|
472
|
+
if (visited.has(id) || depth > 10)
|
|
473
|
+
continue;
|
|
474
|
+
visited.add(id);
|
|
475
|
+
try {
|
|
476
|
+
const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
|
|
477
|
+
for (const edge of edges) {
|
|
478
|
+
const child = await backend.getNode(edge.dst);
|
|
479
|
+
if (!child)
|
|
480
|
+
continue;
|
|
481
|
+
const childType = child.type || child.nodeType;
|
|
482
|
+
if (childType === 'CALL') {
|
|
483
|
+
calls.push({
|
|
484
|
+
id: child.id,
|
|
485
|
+
type: 'CALL',
|
|
486
|
+
name: child.name || '',
|
|
487
|
+
file: child.file || '',
|
|
488
|
+
line: child.line,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (childType !== 'FUNCTION' && childType !== 'CLASS') {
|
|
492
|
+
queue.push({ id: child.id, depth: depth + 1 });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Ignore
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return calls;
|
|
501
|
+
}
|
|
502
|
+
async function searchNode(backend, query) {
|
|
503
|
+
const results = await searchNodes(backend, query, 1);
|
|
504
|
+
return results[0] || null;
|
|
505
|
+
}
|
|
506
|
+
async function searchNodes(backend, query, limit) {
|
|
507
|
+
const results = [];
|
|
508
|
+
const lowerQuery = query.toLowerCase();
|
|
509
|
+
for (const nodeType of ['FUNCTION', 'CLASS', 'MODULE']) {
|
|
510
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType })) {
|
|
511
|
+
const name = (node.name || '').toLowerCase();
|
|
512
|
+
if (name === lowerQuery || name.includes(lowerQuery)) {
|
|
513
|
+
results.push(extractNodeInfo(node));
|
|
514
|
+
if (results.length >= limit)
|
|
515
|
+
return results;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return results;
|
|
520
|
+
}
|
|
521
|
+
async function getModules(backend, limit) {
|
|
522
|
+
const modules = [];
|
|
523
|
+
for await (const node of backend.queryNodes({ nodeType: 'MODULE' })) {
|
|
524
|
+
modules.push(extractNodeInfo(node));
|
|
525
|
+
if (modules.length >= limit)
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
// Sort by file path for better navigation
|
|
529
|
+
modules.sort((a, b) => a.file.localeCompare(b.file));
|
|
530
|
+
return modules;
|
|
531
|
+
}
|
|
532
|
+
async function getClassMembers(backend, classId) {
|
|
533
|
+
const fields = [];
|
|
534
|
+
const methods = [];
|
|
535
|
+
try {
|
|
536
|
+
// Get children via CONTAINS edge
|
|
537
|
+
const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
538
|
+
for (const edge of edges) {
|
|
539
|
+
const child = await backend.getNode(edge.dst);
|
|
540
|
+
if (!child)
|
|
541
|
+
continue;
|
|
542
|
+
const childType = child.type || child.nodeType;
|
|
543
|
+
const nodeInfo = extractNodeInfo(child);
|
|
544
|
+
if (childType === 'FUNCTION') {
|
|
545
|
+
methods.push(nodeInfo);
|
|
546
|
+
}
|
|
547
|
+
else if (childType === 'VARIABLE' || childType === 'PARAMETER') {
|
|
548
|
+
fields.push(nodeInfo);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Ignore
|
|
554
|
+
}
|
|
555
|
+
return { fields, methods };
|
|
556
|
+
}
|
|
557
|
+
async function getDataFlow(backend, varId) {
|
|
558
|
+
const sources = [];
|
|
559
|
+
const targets = [];
|
|
560
|
+
try {
|
|
561
|
+
// Get incoming ASSIGNED_FROM edges (where data comes from)
|
|
562
|
+
const inEdges = await backend.getIncomingEdges(varId, ['ASSIGNED_FROM']);
|
|
563
|
+
for (const edge of inEdges) {
|
|
564
|
+
const src = await backend.getNode(edge.src);
|
|
565
|
+
if (src) {
|
|
566
|
+
sources.push(extractNodeInfo(src));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Get outgoing ASSIGNED_FROM edges (where data flows to)
|
|
570
|
+
const outEdges = await backend.getOutgoingEdges(varId, ['ASSIGNED_FROM']);
|
|
571
|
+
for (const edge of outEdges) {
|
|
572
|
+
const tgt = await backend.getNode(edge.dst);
|
|
573
|
+
if (tgt) {
|
|
574
|
+
targets.push(extractNodeInfo(tgt));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Also check DERIVES_FROM for additional flow info
|
|
578
|
+
const derivesIn = await backend.getIncomingEdges(varId, ['DERIVES_FROM']);
|
|
579
|
+
for (const edge of derivesIn) {
|
|
580
|
+
const src = await backend.getNode(edge.src);
|
|
581
|
+
if (src && !sources.find(s => s.id === src.id)) {
|
|
582
|
+
sources.push(extractNodeInfo(src));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Ignore
|
|
588
|
+
}
|
|
589
|
+
return { sources, targets };
|
|
590
|
+
}
|
|
591
|
+
async function findStartNode(backend, startName) {
|
|
592
|
+
if (startName) {
|
|
593
|
+
return searchNode(backend, startName);
|
|
594
|
+
}
|
|
595
|
+
// Find first function with most callers
|
|
596
|
+
let bestNode = null;
|
|
597
|
+
let bestCallerCount = 0;
|
|
598
|
+
let checked = 0;
|
|
599
|
+
for await (const node of backend.queryNodes({ nodeType: 'FUNCTION' })) {
|
|
600
|
+
const incoming = await backend.getIncomingEdges(node.id, ['CALLS']);
|
|
601
|
+
if (incoming.length > bestCallerCount) {
|
|
602
|
+
bestCallerCount = incoming.length;
|
|
603
|
+
bestNode = extractNodeInfo(node);
|
|
604
|
+
}
|
|
605
|
+
checked++;
|
|
606
|
+
if (checked >= 100)
|
|
607
|
+
break; // Limit search
|
|
608
|
+
}
|
|
609
|
+
return bestNode;
|
|
610
|
+
}
|
|
611
|
+
// Command
|
|
612
|
+
export const exploreCommand = new Command('explore')
|
|
613
|
+
.description('Interactive graph navigation')
|
|
614
|
+
.argument('[start]', 'Starting function name')
|
|
615
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
616
|
+
.action(async (start, options) => {
|
|
617
|
+
const projectPath = resolve(options.project);
|
|
618
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
619
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
620
|
+
if (!existsSync(dbPath)) {
|
|
621
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
622
|
+
}
|
|
623
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
624
|
+
try {
|
|
625
|
+
await backend.connect();
|
|
626
|
+
const startNode = await findStartNode(backend, start || null);
|
|
627
|
+
const { waitUntilExit } = render(_jsx(Explorer, { backend: backend, startNode: startNode, projectPath: projectPath }));
|
|
628
|
+
await waitUntilExit();
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
await backend.close();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get command - Retrieve node by semantic ID
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* grafema get "file.js->scope->TYPE->name"
|
|
6
|
+
* grafema get "file.js->scope->TYPE->name" --json
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
export declare const getCommand: Command;
|
|
10
|
+
//# sourceMappingURL=get.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../src/commands/get.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkCpC,eAAO,MAAM,UAAU,SAyCnB,CAAC"}
|