@grafema/cli 0.1.1-alpha → 0.2.1-beta
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/dist/cli.js +10 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +69 -11
- package/dist/commands/check.d.ts +6 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +177 -1
- package/dist/commands/coverage.d.ts.map +1 -1
- package/dist/commands/coverage.js +7 -0
- package/dist/commands/doctor/checks.d.ts +55 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -0
- package/dist/commands/doctor/checks.js +534 -0
- package/dist/commands/doctor/output.d.ts +20 -0
- package/dist/commands/doctor/output.d.ts.map +1 -0
- package/dist/commands/doctor/output.js +94 -0
- package/dist/commands/doctor/types.d.ts +42 -0
- package/dist/commands/doctor/types.d.ts.map +1 -0
- package/dist/commands/doctor/types.js +4 -0
- package/dist/commands/doctor.d.ts +17 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +80 -0
- package/dist/commands/explain.d.ts +16 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +145 -0
- package/dist/commands/explore.d.ts +7 -1
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +204 -85
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +16 -4
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +48 -50
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +93 -15
- package/dist/commands/ls.d.ts +14 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +132 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +15 -2
- package/dist/commands/query.d.ts +98 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +549 -136
- package/dist/commands/schema.d.ts +13 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +279 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +13 -6
- package/dist/commands/stats.d.ts.map +1 -1
- package/dist/commands/stats.js +7 -0
- package/dist/commands/trace.d.ts +73 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +500 -5
- package/dist/commands/types.d.ts +12 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +79 -0
- package/dist/utils/formatNode.d.ts +13 -0
- package/dist/utils/formatNode.d.ts.map +1 -1
- package/dist/utils/formatNode.js +35 -2
- package/package.json +3 -3
- package/src/cli.ts +10 -0
- package/src/commands/analyze.ts +84 -9
- package/src/commands/check.ts +201 -0
- package/src/commands/coverage.ts +7 -0
- package/src/commands/doctor/checks.ts +612 -0
- package/src/commands/doctor/output.ts +115 -0
- package/src/commands/doctor/types.ts +45 -0
- package/src/commands/doctor.ts +106 -0
- package/src/commands/explain.ts +173 -0
- package/src/commands/explore.tsx +247 -97
- package/src/commands/get.ts +20 -6
- package/src/commands/impact.ts +55 -61
- package/src/commands/init.ts +101 -14
- package/src/commands/ls.ts +166 -0
- package/src/commands/overview.ts +15 -2
- package/src/commands/query.ts +643 -149
- package/src/commands/schema.ts +345 -0
- package/src/commands/server.ts +13 -6
- package/src/commands/stats.ts +7 -0
- package/src/commands/trace.ts +647 -6
- package/src/commands/types.ts +94 -0
- package/src/utils/formatNode.ts +42 -2
package/dist/commands/query.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* For raw Datalog queries, use --raw flag
|
|
10
10
|
*/
|
|
11
11
|
import { Command } from 'commander';
|
|
12
|
-
import { resolve, join } from 'path';
|
|
12
|
+
import { resolve, join, relative, basename } from 'path';
|
|
13
13
|
import { existsSync } from 'fs';
|
|
14
|
-
import { RFDBServerBackend } from '@grafema/core';
|
|
15
|
-
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
14
|
+
import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
15
|
+
import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
|
|
16
16
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
17
17
|
export const queryCommand = new Command('query')
|
|
18
18
|
.description('Search the code graph')
|
|
@@ -20,7 +20,47 @@ export const queryCommand = new Command('query')
|
|
|
20
20
|
.option('-p, --project <path>', 'Project path', '.')
|
|
21
21
|
.option('-j, --json', 'Output as JSON')
|
|
22
22
|
.option('-l, --limit <n>', 'Limit results', '10')
|
|
23
|
-
.option('--raw',
|
|
23
|
+
.option('--raw', `Execute raw Datalog query
|
|
24
|
+
|
|
25
|
+
Predicates:
|
|
26
|
+
type(Id, Type) Find nodes by type or get type of node
|
|
27
|
+
node(Id, Type) Alias for type
|
|
28
|
+
edge(Src, Dst, Type) Find edges between nodes
|
|
29
|
+
attr(Id, Name, Value) Access node attributes (name, file, line, etc.)
|
|
30
|
+
path(Src, Dst) Check reachability between nodes
|
|
31
|
+
incoming(Dst, Src, T) Find incoming edges
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
grafema query --raw 'type(X, "FUNCTION")'
|
|
35
|
+
grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
|
|
36
|
+
grafema query --raw 'edge(X, Y, "CALLS")'`)
|
|
37
|
+
.option('-t, --type <nodeType>', `Filter by exact node type (bypasses type aliases)
|
|
38
|
+
|
|
39
|
+
Use this when:
|
|
40
|
+
- Searching custom node types (jsx:component, redis:cache)
|
|
41
|
+
- You need exact type match without alias resolution
|
|
42
|
+
- Discovering nodes from plugins or custom analyzers
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
grafema query --type http:request "/api"
|
|
46
|
+
grafema query --type FUNCTION "auth"
|
|
47
|
+
grafema query -t socketio:event "connect"`)
|
|
48
|
+
.addHelpText('after', `
|
|
49
|
+
Examples:
|
|
50
|
+
grafema query "auth" Search by name (partial match)
|
|
51
|
+
grafema query "function login" Search functions only
|
|
52
|
+
grafema query "class UserService" Search classes only
|
|
53
|
+
grafema query "route /api/users" Search HTTP routes by path
|
|
54
|
+
grafema query "response in fetchData" Search in specific function scope
|
|
55
|
+
grafema query "error in catch in fetchData" Search in nested scopes
|
|
56
|
+
grafema query "token in src/auth.ts" Search in specific file
|
|
57
|
+
grafema query "variable x in foo in app.ts" Combine type, name, and scopes
|
|
58
|
+
grafema query -l 20 "fetch" Return up to 20 results
|
|
59
|
+
grafema query --json "config" Output results as JSON
|
|
60
|
+
grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
|
|
61
|
+
grafema query -t http:request "/api" Search custom node types
|
|
62
|
+
grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
|
|
63
|
+
`)
|
|
24
64
|
.action(async (pattern, options) => {
|
|
25
65
|
const projectPath = resolve(options.project);
|
|
26
66
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -36,15 +76,34 @@ export const queryCommand = new Command('query')
|
|
|
36
76
|
await executeRawQuery(backend, pattern, options);
|
|
37
77
|
return;
|
|
38
78
|
}
|
|
39
|
-
// Parse pattern
|
|
40
|
-
const { type, name } = parsePattern(pattern);
|
|
41
79
|
const limit = parseInt(options.limit, 10);
|
|
80
|
+
// Parse query with scope support
|
|
81
|
+
let query;
|
|
82
|
+
if (options.type) {
|
|
83
|
+
// Explicit --type bypasses pattern parsing for type
|
|
84
|
+
// But we still parse for scope support
|
|
85
|
+
const scopeParsed = parseQuery(pattern);
|
|
86
|
+
query = {
|
|
87
|
+
type: options.type,
|
|
88
|
+
name: scopeParsed.name,
|
|
89
|
+
file: scopeParsed.file,
|
|
90
|
+
scopes: scopeParsed.scopes,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
query = parseQuery(pattern);
|
|
95
|
+
}
|
|
42
96
|
// Find matching nodes
|
|
43
|
-
const nodes = await findNodes(backend,
|
|
97
|
+
const nodes = await findNodes(backend, query, limit);
|
|
98
|
+
// Check if query has scope constraints for suggestion
|
|
99
|
+
const hasScope = query.file !== null || query.scopes.length > 0;
|
|
44
100
|
if (nodes.length === 0) {
|
|
45
101
|
console.log(`No results for "${pattern}"`);
|
|
46
|
-
if (
|
|
47
|
-
console.log(`
|
|
102
|
+
if (hasScope) {
|
|
103
|
+
console.log(` Try: grafema query "${query.name}" (search all scopes)`);
|
|
104
|
+
}
|
|
105
|
+
else if (query.type) {
|
|
106
|
+
console.log(` Try: grafema query "${query.name}" (search all types)`);
|
|
48
107
|
}
|
|
49
108
|
return;
|
|
50
109
|
}
|
|
@@ -60,7 +119,7 @@ export const queryCommand = new Command('query')
|
|
|
60
119
|
// Display results
|
|
61
120
|
for (const node of nodes) {
|
|
62
121
|
console.log('');
|
|
63
|
-
displayNode(node, projectPath);
|
|
122
|
+
await displayNode(node, projectPath, backend);
|
|
64
123
|
// Show callers and callees for functions
|
|
65
124
|
if (node.type === 'FUNCTION' || node.type === 'CLASS') {
|
|
66
125
|
const callers = await getCallers(backend, node.id, 5);
|
|
@@ -108,6 +167,18 @@ function parsePattern(pattern) {
|
|
|
108
167
|
var: 'VARIABLE',
|
|
109
168
|
const: 'CONSTANT',
|
|
110
169
|
constant: 'CONSTANT',
|
|
170
|
+
// HTTP route aliases
|
|
171
|
+
route: 'http:route',
|
|
172
|
+
endpoint: 'http:route',
|
|
173
|
+
// HTTP request aliases
|
|
174
|
+
request: 'http:request',
|
|
175
|
+
fetch: 'http:request',
|
|
176
|
+
api: 'http:request',
|
|
177
|
+
// Socket.IO aliases
|
|
178
|
+
event: 'socketio:event',
|
|
179
|
+
emit: 'socketio:emit',
|
|
180
|
+
on: 'socketio:on',
|
|
181
|
+
listener: 'socketio:on',
|
|
111
182
|
};
|
|
112
183
|
if (typeMap[typeWord]) {
|
|
113
184
|
return { type: typeMap[typeWord], name };
|
|
@@ -116,28 +187,311 @@ function parsePattern(pattern) {
|
|
|
116
187
|
return { type: null, name: pattern.trim() };
|
|
117
188
|
}
|
|
118
189
|
/**
|
|
119
|
-
*
|
|
190
|
+
* Parse search pattern with scope support.
|
|
191
|
+
*
|
|
192
|
+
* Grammar:
|
|
193
|
+
* query := [type] name [" in " scope]*
|
|
194
|
+
* type := "function" | "class" | "variable" | etc.
|
|
195
|
+
* scope := <filename> | <functionName>
|
|
196
|
+
*
|
|
197
|
+
* File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
|
|
198
|
+
* Function scope detection: anything else
|
|
199
|
+
*
|
|
200
|
+
* IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
|
|
201
|
+
*
|
|
202
|
+
* Examples:
|
|
203
|
+
* "response" -> { type: null, name: "response", file: null, scopes: [] }
|
|
204
|
+
* "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
|
|
205
|
+
* "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
|
|
206
|
+
* "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
|
|
120
207
|
*/
|
|
121
|
-
|
|
208
|
+
export function parseQuery(pattern) {
|
|
209
|
+
// Split on " in " (space-padded) to get clauses
|
|
210
|
+
const clauses = pattern.split(/ in /);
|
|
211
|
+
// First clause is [type] name - use existing parsePattern logic
|
|
212
|
+
const firstClause = clauses[0];
|
|
213
|
+
const { type, name } = parsePattern(firstClause);
|
|
214
|
+
// Remaining clauses are scopes
|
|
215
|
+
let file = null;
|
|
216
|
+
const scopes = [];
|
|
217
|
+
for (let i = 1; i < clauses.length; i++) {
|
|
218
|
+
const scope = clauses[i].trim();
|
|
219
|
+
if (scope === '')
|
|
220
|
+
continue; // Skip empty clauses from trailing whitespace
|
|
221
|
+
if (isFileScope(scope)) {
|
|
222
|
+
file = scope;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
scopes.push(scope);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { type, name, file, scopes };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Detect if a scope string looks like a file path.
|
|
232
|
+
*
|
|
233
|
+
* Heuristics:
|
|
234
|
+
* - Contains "/" -> file path
|
|
235
|
+
* - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
|
|
236
|
+
*
|
|
237
|
+
* Examples:
|
|
238
|
+
* "src/app.ts" -> true
|
|
239
|
+
* "app.js" -> true
|
|
240
|
+
* "fetchData" -> false
|
|
241
|
+
* "UserService" -> false
|
|
242
|
+
* "catch" -> false
|
|
243
|
+
*/
|
|
244
|
+
export function isFileScope(scope) {
|
|
245
|
+
// Contains path separator
|
|
246
|
+
if (scope.includes('/'))
|
|
247
|
+
return true;
|
|
248
|
+
// Ends with common JS/TS extensions
|
|
249
|
+
const fileExtensions = /\.(ts|js|tsx|jsx|mjs|cjs)$/i;
|
|
250
|
+
if (fileExtensions.test(scope))
|
|
251
|
+
return true;
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Check if a semantic ID matches the given scope constraints.
|
|
256
|
+
*
|
|
257
|
+
* Uses parseSemanticId from @grafema/core for robust ID parsing.
|
|
258
|
+
*
|
|
259
|
+
* Scope matching rules:
|
|
260
|
+
* - File scope: semantic ID must match the file path (full or basename)
|
|
261
|
+
* - Function/class scope: semantic ID must contain the scope in its scopePath
|
|
262
|
+
* - Multiple scopes: ALL must match (AND logic)
|
|
263
|
+
* - Scope order: independent - all scopes just need to be present
|
|
264
|
+
*
|
|
265
|
+
* Examples:
|
|
266
|
+
* ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
267
|
+
* Matches: scopes=["fetchData"] -> true
|
|
268
|
+
* Matches: scopes=["try"] -> true (matches "try#0")
|
|
269
|
+
* Matches: scopes=["fetchData", "try"] -> true (both present)
|
|
270
|
+
* Matches: scopes=["processData"] -> false (not in ID)
|
|
271
|
+
*
|
|
272
|
+
* @param semanticId - The full semantic ID to check
|
|
273
|
+
* @param file - File scope (null for any file)
|
|
274
|
+
* @param scopes - Array of scope names to match
|
|
275
|
+
* @returns true if ID matches all constraints
|
|
276
|
+
*/
|
|
277
|
+
export function matchesScope(semanticId, file, scopes) {
|
|
278
|
+
const parsed = parseSemanticId(semanticId);
|
|
279
|
+
if (!parsed)
|
|
280
|
+
return false;
|
|
281
|
+
// File scope check
|
|
282
|
+
if (file !== null) {
|
|
283
|
+
// Full path match
|
|
284
|
+
if (parsed.file === file) {
|
|
285
|
+
// Exact match - OK
|
|
286
|
+
}
|
|
287
|
+
// Basename match: "app.ts" matches "src/app.ts"
|
|
288
|
+
else if (parsed.file.endsWith('/' + file)) {
|
|
289
|
+
// Partial path match - OK
|
|
290
|
+
}
|
|
291
|
+
// Also try if parsed.file ends with the file name (e.g., file is basename)
|
|
292
|
+
else if (basename(parsed.file) === file) {
|
|
293
|
+
// Basename exact match - OK
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Function/class/block scope check
|
|
300
|
+
for (const scope of scopes) {
|
|
301
|
+
// Check if scope appears in the scopePath
|
|
302
|
+
// Handle numbered scopes: "try" matches "try#0"
|
|
303
|
+
const matches = parsed.scopePath.some(s => s === scope || s.startsWith(scope + '#'));
|
|
304
|
+
if (!matches)
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Extract human-readable scope context from a semantic ID.
|
|
311
|
+
*
|
|
312
|
+
* Parses the ID and returns a description of the scope chain.
|
|
313
|
+
*
|
|
314
|
+
* Examples:
|
|
315
|
+
* "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
316
|
+
* -> "inside fetchData, inside try block"
|
|
317
|
+
*
|
|
318
|
+
* "src/app.ts->UserService->login->VARIABLE->token"
|
|
319
|
+
* -> "inside UserService, inside login"
|
|
320
|
+
*
|
|
321
|
+
* "src/app.ts->global->FUNCTION->main"
|
|
322
|
+
* -> null (no interesting scope)
|
|
323
|
+
*
|
|
324
|
+
* @param semanticId - The semantic ID to parse
|
|
325
|
+
* @returns Human-readable scope context or null
|
|
326
|
+
*/
|
|
327
|
+
export function extractScopeContext(semanticId) {
|
|
328
|
+
const parsed = parseSemanticId(semanticId);
|
|
329
|
+
if (!parsed)
|
|
330
|
+
return null;
|
|
331
|
+
// Filter out "global" and format remaining scopes
|
|
332
|
+
const meaningfulScopes = parsed.scopePath.filter(s => s !== 'global');
|
|
333
|
+
if (meaningfulScopes.length === 0)
|
|
334
|
+
return null;
|
|
335
|
+
// Format each scope with context
|
|
336
|
+
const formatted = meaningfulScopes.map(scope => {
|
|
337
|
+
// Handle numbered scopes: "try#0" -> "try block"
|
|
338
|
+
if (scope.match(/^try#\d+$/))
|
|
339
|
+
return 'try block';
|
|
340
|
+
if (scope.match(/^catch#\d+$/))
|
|
341
|
+
return 'catch block';
|
|
342
|
+
if (scope.match(/^if#\d+$/))
|
|
343
|
+
return 'conditional';
|
|
344
|
+
if (scope.match(/^else#\d+$/))
|
|
345
|
+
return 'else block';
|
|
346
|
+
if (scope.match(/^for#\d+$/))
|
|
347
|
+
return 'loop';
|
|
348
|
+
if (scope.match(/^while#\d+$/))
|
|
349
|
+
return 'loop';
|
|
350
|
+
if (scope.match(/^switch#\d+$/))
|
|
351
|
+
return 'switch';
|
|
352
|
+
// Regular scope: function or class name
|
|
353
|
+
return scope;
|
|
354
|
+
});
|
|
355
|
+
// Build "inside X, inside Y" string
|
|
356
|
+
return 'inside ' + formatted.join(', inside ');
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Check if a node matches the search pattern based on its type.
|
|
360
|
+
*
|
|
361
|
+
* Different node types have different searchable fields:
|
|
362
|
+
* - http:route: search method and path fields
|
|
363
|
+
* - http:request: search method and url fields
|
|
364
|
+
* - socketio:event: search name field (standard)
|
|
365
|
+
* - socketio:emit/on: search event field
|
|
366
|
+
* - Default: search name field
|
|
367
|
+
*/
|
|
368
|
+
function matchesSearchPattern(node, nodeType, pattern) {
|
|
369
|
+
const lowerPattern = pattern.toLowerCase();
|
|
370
|
+
// HTTP routes: search method and path
|
|
371
|
+
if (nodeType === 'http:route') {
|
|
372
|
+
const method = (node.method || '').toLowerCase();
|
|
373
|
+
const path = (node.path || '').toLowerCase();
|
|
374
|
+
// Pattern could be: "POST", "/api/users", "POST /api", etc.
|
|
375
|
+
const patternParts = pattern.trim().split(/\s+/);
|
|
376
|
+
if (patternParts.length === 1) {
|
|
377
|
+
// Single term: match method OR path
|
|
378
|
+
const term = patternParts[0].toLowerCase();
|
|
379
|
+
return method === term || path.includes(term);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Multiple terms: first is method, rest is path pattern
|
|
383
|
+
const methodPattern = patternParts[0].toLowerCase();
|
|
384
|
+
const pathPattern = patternParts.slice(1).join(' ').toLowerCase();
|
|
385
|
+
// Method must match exactly (GET, POST, etc.)
|
|
386
|
+
const methodMatches = method === methodPattern;
|
|
387
|
+
// Path must contain the pattern
|
|
388
|
+
const pathMatches = path.includes(pathPattern);
|
|
389
|
+
return methodMatches && pathMatches;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// HTTP requests: search method and url
|
|
393
|
+
if (nodeType === 'http:request') {
|
|
394
|
+
const method = (node.method || '').toLowerCase();
|
|
395
|
+
const url = (node.url || '').toLowerCase();
|
|
396
|
+
// Pattern could be: "POST", "/api/users", "POST /api", etc.
|
|
397
|
+
const patternParts = pattern.trim().split(/\s+/);
|
|
398
|
+
if (patternParts.length === 1) {
|
|
399
|
+
// Single term: match method OR url
|
|
400
|
+
const term = patternParts[0].toLowerCase();
|
|
401
|
+
return method === term || url.includes(term);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// Multiple terms: first is method, rest is url pattern
|
|
405
|
+
const methodPattern = patternParts[0].toLowerCase();
|
|
406
|
+
const urlPattern = patternParts.slice(1).join(' ').toLowerCase();
|
|
407
|
+
// Method must match exactly (GET, POST, etc.)
|
|
408
|
+
const methodMatches = method === methodPattern;
|
|
409
|
+
// URL must contain the pattern
|
|
410
|
+
const urlMatches = url.includes(urlPattern);
|
|
411
|
+
return methodMatches && urlMatches;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Socket.IO event channels: search name field (standard)
|
|
415
|
+
if (nodeType === 'socketio:event') {
|
|
416
|
+
const nodeName = (node.name || '').toLowerCase();
|
|
417
|
+
return nodeName.includes(lowerPattern);
|
|
418
|
+
}
|
|
419
|
+
// Socket.IO emit/on: search event field
|
|
420
|
+
if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
|
|
421
|
+
const eventName = (node.event || '').toLowerCase();
|
|
422
|
+
return eventName.includes(lowerPattern);
|
|
423
|
+
}
|
|
424
|
+
// Default: search name field
|
|
425
|
+
const nodeName = (node.name || '').toLowerCase();
|
|
426
|
+
return nodeName.includes(lowerPattern);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Find nodes by query (type, name, file scope, function scopes)
|
|
430
|
+
*/
|
|
431
|
+
async function findNodes(backend, query, limit) {
|
|
122
432
|
const results = [];
|
|
123
|
-
const searchTypes = type
|
|
124
|
-
? [type]
|
|
125
|
-
: [
|
|
433
|
+
const searchTypes = query.type
|
|
434
|
+
? [query.type]
|
|
435
|
+
: [
|
|
436
|
+
'FUNCTION',
|
|
437
|
+
'CLASS',
|
|
438
|
+
'MODULE',
|
|
439
|
+
'VARIABLE',
|
|
440
|
+
'CONSTANT',
|
|
441
|
+
'http:route',
|
|
442
|
+
'http:request',
|
|
443
|
+
'socketio:event',
|
|
444
|
+
'socketio:emit',
|
|
445
|
+
'socketio:on'
|
|
446
|
+
];
|
|
126
447
|
for (const nodeType of searchTypes) {
|
|
127
448
|
for await (const node of backend.queryNodes({ nodeType: nodeType })) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
449
|
+
// Type-aware field matching (name)
|
|
450
|
+
const nameMatches = matchesSearchPattern(node, nodeType, query.name);
|
|
451
|
+
if (!nameMatches)
|
|
452
|
+
continue;
|
|
453
|
+
// Scope matching (file and function scopes)
|
|
454
|
+
const scopeMatches = matchesScope(node.id, query.file, query.scopes);
|
|
455
|
+
if (!scopeMatches)
|
|
456
|
+
continue;
|
|
457
|
+
const nodeInfo = {
|
|
458
|
+
id: node.id,
|
|
459
|
+
type: node.type || nodeType,
|
|
460
|
+
name: node.name || '',
|
|
461
|
+
file: node.file || '',
|
|
462
|
+
line: node.line,
|
|
463
|
+
};
|
|
464
|
+
// Add scope context for display
|
|
465
|
+
nodeInfo.scopeContext = extractScopeContext(node.id);
|
|
466
|
+
// Include method and path for http:route nodes
|
|
467
|
+
if (nodeType === 'http:route') {
|
|
468
|
+
nodeInfo.method = node.method;
|
|
469
|
+
nodeInfo.path = node.path;
|
|
470
|
+
}
|
|
471
|
+
// Include method and url for http:request nodes
|
|
472
|
+
if (nodeType === 'http:request') {
|
|
473
|
+
nodeInfo.method = node.method;
|
|
474
|
+
nodeInfo.url = node.url;
|
|
475
|
+
}
|
|
476
|
+
// Include event field for Socket.IO nodes
|
|
477
|
+
if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
|
|
478
|
+
nodeInfo.event = node.event;
|
|
140
479
|
}
|
|
480
|
+
// Include emit-specific fields
|
|
481
|
+
if (nodeType === 'socketio:emit') {
|
|
482
|
+
nodeInfo.room = node.room;
|
|
483
|
+
nodeInfo.namespace = node.namespace;
|
|
484
|
+
nodeInfo.broadcast = node.broadcast;
|
|
485
|
+
nodeInfo.objectName = node.objectName;
|
|
486
|
+
}
|
|
487
|
+
// Include listener-specific fields
|
|
488
|
+
if (nodeType === 'socketio:on') {
|
|
489
|
+
nodeInfo.objectName = node.objectName;
|
|
490
|
+
nodeInfo.handlerName = node.handlerName;
|
|
491
|
+
}
|
|
492
|
+
results.push(nodeInfo);
|
|
493
|
+
if (results.length >= limit)
|
|
494
|
+
break;
|
|
141
495
|
}
|
|
142
496
|
if (results.length >= limit)
|
|
143
497
|
break;
|
|
@@ -163,8 +517,8 @@ async function getCallers(backend, nodeId, limit) {
|
|
|
163
517
|
const callNode = await backend.getNode(edge.src);
|
|
164
518
|
if (!callNode)
|
|
165
519
|
continue;
|
|
166
|
-
// Find the FUNCTION that contains this CALL
|
|
167
|
-
const containingFunc = await
|
|
520
|
+
// Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
|
|
521
|
+
const containingFunc = await findContainingFunctionCore(backend, callNode.id);
|
|
168
522
|
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
169
523
|
seen.add(containingFunc.id);
|
|
170
524
|
callers.push({
|
|
@@ -177,142 +531,201 @@ async function getCallers(backend, nodeId, limit) {
|
|
|
177
531
|
}
|
|
178
532
|
}
|
|
179
533
|
}
|
|
180
|
-
catch {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return callers;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Find the FUNCTION or CLASS that contains a node
|
|
187
|
-
*
|
|
188
|
-
* Path can be: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
|
|
189
|
-
* So we need to follow both CONTAINS and HAS_SCOPE edges
|
|
190
|
-
*/
|
|
191
|
-
async function findContainingFunction(backend, nodeId, maxDepth = 15) {
|
|
192
|
-
const visited = new Set();
|
|
193
|
-
const queue = [{ id: nodeId, depth: 0 }];
|
|
194
|
-
while (queue.length > 0) {
|
|
195
|
-
const { id, depth } = queue.shift();
|
|
196
|
-
if (visited.has(id) || depth > maxDepth)
|
|
197
|
-
continue;
|
|
198
|
-
visited.add(id);
|
|
199
|
-
try {
|
|
200
|
-
// Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
|
|
201
|
-
const edges = await backend.getIncomingEdges(id, null);
|
|
202
|
-
for (const edge of edges) {
|
|
203
|
-
const edgeType = edge.edgeType || edge.type;
|
|
204
|
-
// Only follow structural edges
|
|
205
|
-
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
|
|
206
|
-
continue;
|
|
207
|
-
const parentNode = await backend.getNode(edge.src);
|
|
208
|
-
if (!parentNode || visited.has(parentNode.id))
|
|
209
|
-
continue;
|
|
210
|
-
const parentType = parentNode.type;
|
|
211
|
-
// FUNCTION, CLASS, or MODULE (for top-level calls)
|
|
212
|
-
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
213
|
-
return {
|
|
214
|
-
id: parentNode.id,
|
|
215
|
-
type: parentType,
|
|
216
|
-
name: parentNode.name || '<anonymous>',
|
|
217
|
-
file: parentNode.file || '',
|
|
218
|
-
line: parentNode.line,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
// Continue searching from this parent
|
|
222
|
-
queue.push({ id: parentNode.id, depth: depth + 1 });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// Ignore errors
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (process.env.DEBUG) {
|
|
536
|
+
console.error('[query] Error in getCallers:', error);
|
|
227
537
|
}
|
|
228
538
|
}
|
|
229
|
-
return
|
|
539
|
+
return callers;
|
|
230
540
|
}
|
|
231
541
|
/**
|
|
232
542
|
* Get functions that this node calls
|
|
233
543
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
544
|
+
* Uses shared utility from @grafema/core which:
|
|
545
|
+
* - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
|
|
546
|
+
* - Finds both CALL and METHOD_CALL nodes
|
|
547
|
+
* - Only returns resolved calls (those with CALLS edges to targets)
|
|
236
548
|
*/
|
|
237
549
|
async function getCallees(backend, nodeId, limit) {
|
|
238
550
|
const callees = [];
|
|
239
551
|
const seen = new Set();
|
|
240
552
|
try {
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
for (const
|
|
553
|
+
// Use shared utility (now includes METHOD_CALL and correct graph traversal)
|
|
554
|
+
const calls = await findCallsInFunctionCore(backend, nodeId);
|
|
555
|
+
for (const call of calls) {
|
|
244
556
|
if (callees.length >= limit)
|
|
245
557
|
break;
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (callees.length >= limit)
|
|
250
|
-
break;
|
|
251
|
-
const targetNode = await backend.getNode(edge.dst);
|
|
252
|
-
if (!targetNode || seen.has(targetNode.id))
|
|
253
|
-
continue;
|
|
254
|
-
seen.add(targetNode.id);
|
|
558
|
+
// Only include resolved calls with targets
|
|
559
|
+
if (call.resolved && call.target && !seen.has(call.target.id)) {
|
|
560
|
+
seen.add(call.target.id);
|
|
255
561
|
callees.push({
|
|
256
|
-
id:
|
|
257
|
-
type:
|
|
258
|
-
name:
|
|
259
|
-
file:
|
|
260
|
-
line:
|
|
562
|
+
id: call.target.id,
|
|
563
|
+
type: 'FUNCTION',
|
|
564
|
+
name: call.target.name || '<anonymous>',
|
|
565
|
+
file: call.target.file || '',
|
|
566
|
+
line: call.target.line,
|
|
261
567
|
});
|
|
262
568
|
}
|
|
263
569
|
}
|
|
264
570
|
}
|
|
265
|
-
catch {
|
|
266
|
-
|
|
571
|
+
catch (error) {
|
|
572
|
+
if (process.env.DEBUG) {
|
|
573
|
+
console.error('[query] Error in getCallees:', error);
|
|
574
|
+
}
|
|
267
575
|
}
|
|
268
576
|
return callees;
|
|
269
577
|
}
|
|
270
578
|
/**
|
|
271
|
-
*
|
|
579
|
+
* Display a node with semantic ID as primary identifier
|
|
272
580
|
*/
|
|
273
|
-
async function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
581
|
+
async function displayNode(node, projectPath, backend) {
|
|
582
|
+
// Special formatting for HTTP routes
|
|
583
|
+
if (node.type === 'http:route' && node.method && node.path) {
|
|
584
|
+
console.log(formatHttpRouteDisplay(node, projectPath));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Special formatting for HTTP requests
|
|
588
|
+
if (node.type === 'http:request') {
|
|
589
|
+
console.log(formatHttpRequestDisplay(node, projectPath));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Special formatting for Socket.IO event channels
|
|
593
|
+
if (node.type === 'socketio:event') {
|
|
594
|
+
console.log(await formatSocketEventDisplay(node, projectPath, backend));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// Special formatting for Socket.IO emit/on
|
|
598
|
+
if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
|
|
599
|
+
console.log(formatSocketIONodeDisplay(node, projectPath));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
console.log(formatNodeDisplay(node, { projectPath }));
|
|
603
|
+
// Add scope context if present
|
|
604
|
+
if (node.scopeContext) {
|
|
605
|
+
console.log(` Scope: ${node.scopeContext}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Format HTTP route for display
|
|
610
|
+
*
|
|
611
|
+
* Output:
|
|
612
|
+
* [http:route] POST /api/users
|
|
613
|
+
* Location: src/routes/users.js:15
|
|
614
|
+
*/
|
|
615
|
+
function formatHttpRouteDisplay(node, projectPath) {
|
|
616
|
+
const lines = [];
|
|
617
|
+
// Line 1: [type] METHOD PATH
|
|
618
|
+
lines.push(`[${node.type}] ${node.method} ${node.path}`);
|
|
619
|
+
// Line 2: Location
|
|
620
|
+
if (node.file) {
|
|
621
|
+
const relPath = relative(projectPath, node.file);
|
|
622
|
+
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
623
|
+
lines.push(` Location: ${loc}`);
|
|
624
|
+
}
|
|
625
|
+
return lines.join('\n');
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Format HTTP request for display
|
|
629
|
+
*
|
|
630
|
+
* Output:
|
|
631
|
+
* [http:request] GET /api/users
|
|
632
|
+
* Location: src/pages/Users.tsx:42
|
|
633
|
+
*/
|
|
634
|
+
function formatHttpRequestDisplay(node, projectPath) {
|
|
635
|
+
const lines = [];
|
|
636
|
+
// Line 1: [type] METHOD URL
|
|
637
|
+
const method = node.method || 'GET';
|
|
638
|
+
const url = node.url || 'dynamic';
|
|
639
|
+
lines.push(`[${node.type}] ${method} ${url}`);
|
|
640
|
+
// Line 2: Location
|
|
641
|
+
if (node.file) {
|
|
642
|
+
const relPath = relative(projectPath, node.file);
|
|
643
|
+
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
644
|
+
lines.push(` Location: ${loc}`);
|
|
645
|
+
}
|
|
646
|
+
return lines.join('\n');
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Format Socket.IO event channel for display
|
|
650
|
+
*
|
|
651
|
+
* Output:
|
|
652
|
+
* [socketio:event] slot:booked
|
|
653
|
+
* ID: socketio:event#slot:booked
|
|
654
|
+
* Emitted by: 3 locations
|
|
655
|
+
* Listened by: 5 locations
|
|
656
|
+
*/
|
|
657
|
+
async function formatSocketEventDisplay(node, projectPath, backend) {
|
|
658
|
+
const lines = [];
|
|
659
|
+
// Line 1: [type] event_name
|
|
660
|
+
lines.push(`[${node.type}] ${node.name}`);
|
|
661
|
+
// Line 2: ID
|
|
662
|
+
lines.push(` ID: ${node.id}`);
|
|
663
|
+
// Query edges to get emitter and listener counts
|
|
664
|
+
try {
|
|
665
|
+
const incomingEdges = await backend.getIncomingEdges(node.id, ['EMITS_EVENT']);
|
|
666
|
+
const outgoingEdges = await backend.getOutgoingEdges(node.id, ['LISTENED_BY']);
|
|
667
|
+
if (incomingEdges.length > 0) {
|
|
668
|
+
lines.push(` Emitted by: ${incomingEdges.length} location${incomingEdges.length !== 1 ? 's' : ''}`);
|
|
669
|
+
}
|
|
670
|
+
if (outgoingEdges.length > 0) {
|
|
671
|
+
lines.push(` Listened by: ${outgoingEdges.length} location${outgoingEdges.length !== 1 ? 's' : ''}`);
|
|
304
672
|
}
|
|
305
|
-
|
|
306
|
-
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (process.env.DEBUG) {
|
|
676
|
+
console.error('[query] Error in formatSocketEventDisplay:', error);
|
|
307
677
|
}
|
|
308
678
|
}
|
|
309
|
-
return
|
|
679
|
+
return lines.join('\n');
|
|
310
680
|
}
|
|
311
681
|
/**
|
|
312
|
-
*
|
|
682
|
+
* Format Socket.IO emit/on for display
|
|
683
|
+
*
|
|
684
|
+
* Output for emit:
|
|
685
|
+
* [socketio:emit] slot:booked
|
|
686
|
+
* ID: socketio:emit#slot:booked#server.js#28
|
|
687
|
+
* Location: server.js:28
|
|
688
|
+
* Room: gig:123 (if applicable)
|
|
689
|
+
* Namespace: /admin (if applicable)
|
|
690
|
+
* Broadcast: true (if applicable)
|
|
691
|
+
*
|
|
692
|
+
* Output for on:
|
|
693
|
+
* [socketio:on] slot:booked
|
|
694
|
+
* ID: socketio:on#slot:booked#client.js#13
|
|
695
|
+
* Location: client.js:13
|
|
696
|
+
* Handler: anonymous:27
|
|
313
697
|
*/
|
|
314
|
-
function
|
|
315
|
-
|
|
698
|
+
function formatSocketIONodeDisplay(node, projectPath) {
|
|
699
|
+
const lines = [];
|
|
700
|
+
// Line 1: [type] event_name
|
|
701
|
+
const eventName = node.event || node.name || 'unknown';
|
|
702
|
+
lines.push(`[${node.type}] ${eventName}`);
|
|
703
|
+
// Line 2: ID
|
|
704
|
+
lines.push(` ID: ${node.id}`);
|
|
705
|
+
// Line 3: Location (if applicable)
|
|
706
|
+
if (node.file) {
|
|
707
|
+
const loc = formatLocation(node.file, node.line, projectPath);
|
|
708
|
+
if (loc) {
|
|
709
|
+
lines.push(` Location: ${loc}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Emit-specific fields
|
|
713
|
+
if (node.type === 'socketio:emit') {
|
|
714
|
+
if (node.room) {
|
|
715
|
+
lines.push(` Room: ${node.room}`);
|
|
716
|
+
}
|
|
717
|
+
if (node.namespace) {
|
|
718
|
+
lines.push(` Namespace: ${node.namespace}`);
|
|
719
|
+
}
|
|
720
|
+
if (node.broadcast) {
|
|
721
|
+
lines.push(` Broadcast: true`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Listener-specific fields
|
|
725
|
+
if (node.type === 'socketio:on' && node.handlerName) {
|
|
726
|
+
lines.push(` Handler: ${node.handlerName}`);
|
|
727
|
+
}
|
|
728
|
+
return lines.join('\n');
|
|
316
729
|
}
|
|
317
730
|
/**
|
|
318
731
|
* Execute raw Datalog query (backwards compat)
|