@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/src/commands/query.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Command } from 'commander';
|
|
13
|
-
import { resolve, join, relative } from 'path';
|
|
13
|
+
import { resolve, join, relative, basename } from 'path';
|
|
14
14
|
import { existsSync } from 'fs';
|
|
15
|
-
import { RFDBServerBackend } from '@grafema/core';
|
|
16
|
-
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
15
|
+
import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
16
|
+
import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
|
|
17
17
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
18
|
|
|
19
19
|
interface QueryOptions {
|
|
@@ -21,6 +21,7 @@ interface QueryOptions {
|
|
|
21
21
|
json?: boolean;
|
|
22
22
|
limit: string;
|
|
23
23
|
raw?: boolean;
|
|
24
|
+
type?: string; // Explicit node type (bypasses type aliases)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface NodeInfo {
|
|
@@ -29,16 +30,94 @@ interface NodeInfo {
|
|
|
29
30
|
name: string;
|
|
30
31
|
file: string;
|
|
31
32
|
line?: number;
|
|
33
|
+
method?: string; // For http:route, http:request
|
|
34
|
+
path?: string; // For http:route
|
|
35
|
+
url?: string; // For http:request
|
|
36
|
+
event?: string; // For socketio:emit, socketio:on, socketio:event
|
|
37
|
+
room?: string; // For socketio:emit
|
|
38
|
+
namespace?: string; // For socketio:emit
|
|
39
|
+
broadcast?: boolean; // For socketio:emit
|
|
40
|
+
objectName?: string; // For socketio:emit, socketio:on
|
|
41
|
+
handlerName?: string; // For socketio:on
|
|
42
|
+
/** Human-readable scope context */
|
|
43
|
+
scopeContext?: string | null;
|
|
32
44
|
[key: string]: unknown;
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Parsed query with optional scope constraints.
|
|
49
|
+
*
|
|
50
|
+
* Supports patterns like:
|
|
51
|
+
* "response" -> { name: "response" }
|
|
52
|
+
* "variable response" -> { type: "VARIABLE", name: "response" }
|
|
53
|
+
* "response in fetchData" -> { name: "response", scopes: ["fetchData"] }
|
|
54
|
+
* "response in src/app.ts" -> { name: "response", file: "src/app.ts" }
|
|
55
|
+
* "response in catch in fetchData" -> { name: "response", scopes: ["fetchData", "catch"] }
|
|
56
|
+
*/
|
|
57
|
+
export interface ParsedQuery {
|
|
58
|
+
/** Node type (e.g., "FUNCTION", "VARIABLE") or null for any */
|
|
59
|
+
type: string | null;
|
|
60
|
+
/** Node name to search (partial match) */
|
|
61
|
+
name: string;
|
|
62
|
+
/** File scope - filter to nodes in this file */
|
|
63
|
+
file: string | null;
|
|
64
|
+
/** Scope chain - filter to nodes inside these scopes (function/class/block names) */
|
|
65
|
+
scopes: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
export const queryCommand = new Command('query')
|
|
36
69
|
.description('Search the code graph')
|
|
37
70
|
.argument('<pattern>', 'Search pattern: "function X", "class Y", or just "X"')
|
|
38
71
|
.option('-p, --project <path>', 'Project path', '.')
|
|
39
72
|
.option('-j, --json', 'Output as JSON')
|
|
40
73
|
.option('-l, --limit <n>', 'Limit results', '10')
|
|
41
|
-
.option(
|
|
74
|
+
.option(
|
|
75
|
+
'--raw',
|
|
76
|
+
`Execute raw Datalog query
|
|
77
|
+
|
|
78
|
+
Predicates:
|
|
79
|
+
type(Id, Type) Find nodes by type or get type of node
|
|
80
|
+
node(Id, Type) Alias for type
|
|
81
|
+
edge(Src, Dst, Type) Find edges between nodes
|
|
82
|
+
attr(Id, Name, Value) Access node attributes (name, file, line, etc.)
|
|
83
|
+
path(Src, Dst) Check reachability between nodes
|
|
84
|
+
incoming(Dst, Src, T) Find incoming edges
|
|
85
|
+
|
|
86
|
+
Examples:
|
|
87
|
+
grafema query --raw 'type(X, "FUNCTION")'
|
|
88
|
+
grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")'
|
|
89
|
+
grafema query --raw 'edge(X, Y, "CALLS")'`
|
|
90
|
+
)
|
|
91
|
+
.option(
|
|
92
|
+
'-t, --type <nodeType>',
|
|
93
|
+
`Filter by exact node type (bypasses type aliases)
|
|
94
|
+
|
|
95
|
+
Use this when:
|
|
96
|
+
- Searching custom node types (jsx:component, redis:cache)
|
|
97
|
+
- You need exact type match without alias resolution
|
|
98
|
+
- Discovering nodes from plugins or custom analyzers
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
grafema query --type http:request "/api"
|
|
102
|
+
grafema query --type FUNCTION "auth"
|
|
103
|
+
grafema query -t socketio:event "connect"`
|
|
104
|
+
)
|
|
105
|
+
.addHelpText('after', `
|
|
106
|
+
Examples:
|
|
107
|
+
grafema query "auth" Search by name (partial match)
|
|
108
|
+
grafema query "function login" Search functions only
|
|
109
|
+
grafema query "class UserService" Search classes only
|
|
110
|
+
grafema query "route /api/users" Search HTTP routes by path
|
|
111
|
+
grafema query "response in fetchData" Search in specific function scope
|
|
112
|
+
grafema query "error in catch in fetchData" Search in nested scopes
|
|
113
|
+
grafema query "token in src/auth.ts" Search in specific file
|
|
114
|
+
grafema query "variable x in foo in app.ts" Combine type, name, and scopes
|
|
115
|
+
grafema query -l 20 "fetch" Return up to 20 results
|
|
116
|
+
grafema query --json "config" Output results as JSON
|
|
117
|
+
grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
|
|
118
|
+
grafema query -t http:request "/api" Search custom node types
|
|
119
|
+
grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
|
|
120
|
+
`)
|
|
42
121
|
.action(async (pattern: string, options: QueryOptions) => {
|
|
43
122
|
const projectPath = resolve(options.project);
|
|
44
123
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -58,17 +137,37 @@ export const queryCommand = new Command('query')
|
|
|
58
137
|
return;
|
|
59
138
|
}
|
|
60
139
|
|
|
61
|
-
// Parse pattern
|
|
62
|
-
const { type, name } = parsePattern(pattern);
|
|
63
140
|
const limit = parseInt(options.limit, 10);
|
|
64
141
|
|
|
142
|
+
// Parse query with scope support
|
|
143
|
+
let query: ParsedQuery;
|
|
144
|
+
|
|
145
|
+
if (options.type) {
|
|
146
|
+
// Explicit --type bypasses pattern parsing for type
|
|
147
|
+
// But we still parse for scope support
|
|
148
|
+
const scopeParsed = parseQuery(pattern);
|
|
149
|
+
query = {
|
|
150
|
+
type: options.type,
|
|
151
|
+
name: scopeParsed.name,
|
|
152
|
+
file: scopeParsed.file,
|
|
153
|
+
scopes: scopeParsed.scopes,
|
|
154
|
+
};
|
|
155
|
+
} else {
|
|
156
|
+
query = parseQuery(pattern);
|
|
157
|
+
}
|
|
158
|
+
|
|
65
159
|
// Find matching nodes
|
|
66
|
-
const nodes = await findNodes(backend,
|
|
160
|
+
const nodes = await findNodes(backend, query, limit);
|
|
161
|
+
|
|
162
|
+
// Check if query has scope constraints for suggestion
|
|
163
|
+
const hasScope = query.file !== null || query.scopes.length > 0;
|
|
67
164
|
|
|
68
165
|
if (nodes.length === 0) {
|
|
69
166
|
console.log(`No results for "${pattern}"`);
|
|
70
|
-
if (
|
|
71
|
-
console.log(`
|
|
167
|
+
if (hasScope) {
|
|
168
|
+
console.log(` Try: grafema query "${query.name}" (search all scopes)`);
|
|
169
|
+
} else if (query.type) {
|
|
170
|
+
console.log(` Try: grafema query "${query.name}" (search all types)`);
|
|
72
171
|
}
|
|
73
172
|
return;
|
|
74
173
|
}
|
|
@@ -88,7 +187,7 @@ export const queryCommand = new Command('query')
|
|
|
88
187
|
// Display results
|
|
89
188
|
for (const node of nodes) {
|
|
90
189
|
console.log('');
|
|
91
|
-
displayNode(node, projectPath);
|
|
190
|
+
await displayNode(node, projectPath, backend);
|
|
92
191
|
|
|
93
192
|
// Show callers and callees for functions
|
|
94
193
|
if (node.type === 'FUNCTION' || node.type === 'CLASS') {
|
|
@@ -143,6 +242,18 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
|
|
|
143
242
|
var: 'VARIABLE',
|
|
144
243
|
const: 'CONSTANT',
|
|
145
244
|
constant: 'CONSTANT',
|
|
245
|
+
// HTTP route aliases
|
|
246
|
+
route: 'http:route',
|
|
247
|
+
endpoint: 'http:route',
|
|
248
|
+
// HTTP request aliases
|
|
249
|
+
request: 'http:request',
|
|
250
|
+
fetch: 'http:request',
|
|
251
|
+
api: 'http:request',
|
|
252
|
+
// Socket.IO aliases
|
|
253
|
+
event: 'socketio:event',
|
|
254
|
+
emit: 'socketio:emit',
|
|
255
|
+
on: 'socketio:on',
|
|
256
|
+
listener: 'socketio:on',
|
|
146
257
|
};
|
|
147
258
|
|
|
148
259
|
if (typeMap[typeWord]) {
|
|
@@ -154,33 +265,349 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
|
|
|
154
265
|
}
|
|
155
266
|
|
|
156
267
|
/**
|
|
157
|
-
*
|
|
268
|
+
* Parse search pattern with scope support.
|
|
269
|
+
*
|
|
270
|
+
* Grammar:
|
|
271
|
+
* query := [type] name [" in " scope]*
|
|
272
|
+
* type := "function" | "class" | "variable" | etc.
|
|
273
|
+
* scope := <filename> | <functionName>
|
|
274
|
+
*
|
|
275
|
+
* File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
|
|
276
|
+
* Function scope detection: anything else
|
|
277
|
+
*
|
|
278
|
+
* IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
|
|
279
|
+
*
|
|
280
|
+
* Examples:
|
|
281
|
+
* "response" -> { type: null, name: "response", file: null, scopes: [] }
|
|
282
|
+
* "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
|
|
283
|
+
* "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
|
|
284
|
+
* "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
|
|
285
|
+
*/
|
|
286
|
+
export function parseQuery(pattern: string): ParsedQuery {
|
|
287
|
+
// Split on " in " (space-padded) to get clauses
|
|
288
|
+
const clauses = pattern.split(/ in /);
|
|
289
|
+
|
|
290
|
+
// First clause is [type] name - use existing parsePattern logic
|
|
291
|
+
const firstClause = clauses[0];
|
|
292
|
+
const { type, name } = parsePattern(firstClause);
|
|
293
|
+
|
|
294
|
+
// Remaining clauses are scopes
|
|
295
|
+
let file: string | null = null;
|
|
296
|
+
const scopes: string[] = [];
|
|
297
|
+
|
|
298
|
+
for (let i = 1; i < clauses.length; i++) {
|
|
299
|
+
const scope = clauses[i].trim();
|
|
300
|
+
if (scope === '') continue; // Skip empty clauses from trailing whitespace
|
|
301
|
+
if (isFileScope(scope)) {
|
|
302
|
+
file = scope;
|
|
303
|
+
} else {
|
|
304
|
+
scopes.push(scope);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { type, name, file, scopes };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Detect if a scope string looks like a file path.
|
|
313
|
+
*
|
|
314
|
+
* Heuristics:
|
|
315
|
+
* - Contains "/" -> file path
|
|
316
|
+
* - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
|
|
317
|
+
*
|
|
318
|
+
* Examples:
|
|
319
|
+
* "src/app.ts" -> true
|
|
320
|
+
* "app.js" -> true
|
|
321
|
+
* "fetchData" -> false
|
|
322
|
+
* "UserService" -> false
|
|
323
|
+
* "catch" -> false
|
|
324
|
+
*/
|
|
325
|
+
export function isFileScope(scope: string): boolean {
|
|
326
|
+
// Contains path separator
|
|
327
|
+
if (scope.includes('/')) return true;
|
|
328
|
+
|
|
329
|
+
// Ends with common JS/TS extensions
|
|
330
|
+
const fileExtensions = /\.(ts|js|tsx|jsx|mjs|cjs)$/i;
|
|
331
|
+
if (fileExtensions.test(scope)) return true;
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check if a semantic ID matches the given scope constraints.
|
|
338
|
+
*
|
|
339
|
+
* Uses parseSemanticId from @grafema/core for robust ID parsing.
|
|
340
|
+
*
|
|
341
|
+
* Scope matching rules:
|
|
342
|
+
* - File scope: semantic ID must match the file path (full or basename)
|
|
343
|
+
* - Function/class scope: semantic ID must contain the scope in its scopePath
|
|
344
|
+
* - Multiple scopes: ALL must match (AND logic)
|
|
345
|
+
* - Scope order: independent - all scopes just need to be present
|
|
346
|
+
*
|
|
347
|
+
* Examples:
|
|
348
|
+
* ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
349
|
+
* Matches: scopes=["fetchData"] -> true
|
|
350
|
+
* Matches: scopes=["try"] -> true (matches "try#0")
|
|
351
|
+
* Matches: scopes=["fetchData", "try"] -> true (both present)
|
|
352
|
+
* Matches: scopes=["processData"] -> false (not in ID)
|
|
353
|
+
*
|
|
354
|
+
* @param semanticId - The full semantic ID to check
|
|
355
|
+
* @param file - File scope (null for any file)
|
|
356
|
+
* @param scopes - Array of scope names to match
|
|
357
|
+
* @returns true if ID matches all constraints
|
|
358
|
+
*/
|
|
359
|
+
export function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean {
|
|
360
|
+
const parsed = parseSemanticId(semanticId);
|
|
361
|
+
if (!parsed) return false;
|
|
362
|
+
|
|
363
|
+
// File scope check
|
|
364
|
+
if (file !== null) {
|
|
365
|
+
// Full path match
|
|
366
|
+
if (parsed.file === file) {
|
|
367
|
+
// Exact match - OK
|
|
368
|
+
}
|
|
369
|
+
// Basename match: "app.ts" matches "src/app.ts"
|
|
370
|
+
else if (parsed.file.endsWith('/' + file)) {
|
|
371
|
+
// Partial path match - OK
|
|
372
|
+
}
|
|
373
|
+
// Also try if parsed.file ends with the file name (e.g., file is basename)
|
|
374
|
+
else if (basename(parsed.file) === file) {
|
|
375
|
+
// Basename exact match - OK
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Function/class/block scope check
|
|
383
|
+
for (const scope of scopes) {
|
|
384
|
+
// Check if scope appears in the scopePath
|
|
385
|
+
// Handle numbered scopes: "try" matches "try#0"
|
|
386
|
+
const matches = parsed.scopePath.some(s =>
|
|
387
|
+
s === scope || s.startsWith(scope + '#')
|
|
388
|
+
);
|
|
389
|
+
if (!matches) return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Extract human-readable scope context from a semantic ID.
|
|
397
|
+
*
|
|
398
|
+
* Parses the ID and returns a description of the scope chain.
|
|
399
|
+
*
|
|
400
|
+
* Examples:
|
|
401
|
+
* "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
402
|
+
* -> "inside fetchData, inside try block"
|
|
403
|
+
*
|
|
404
|
+
* "src/app.ts->UserService->login->VARIABLE->token"
|
|
405
|
+
* -> "inside UserService, inside login"
|
|
406
|
+
*
|
|
407
|
+
* "src/app.ts->global->FUNCTION->main"
|
|
408
|
+
* -> null (no interesting scope)
|
|
409
|
+
*
|
|
410
|
+
* @param semanticId - The semantic ID to parse
|
|
411
|
+
* @returns Human-readable scope context or null
|
|
412
|
+
*/
|
|
413
|
+
export function extractScopeContext(semanticId: string): string | null {
|
|
414
|
+
const parsed = parseSemanticId(semanticId);
|
|
415
|
+
if (!parsed) return null;
|
|
416
|
+
|
|
417
|
+
// Filter out "global" and format remaining scopes
|
|
418
|
+
const meaningfulScopes = parsed.scopePath.filter(s => s !== 'global');
|
|
419
|
+
if (meaningfulScopes.length === 0) return null;
|
|
420
|
+
|
|
421
|
+
// Format each scope with context
|
|
422
|
+
const formatted = meaningfulScopes.map(scope => {
|
|
423
|
+
// Handle numbered scopes: "try#0" -> "try block"
|
|
424
|
+
if (scope.match(/^try#\d+$/)) return 'try block';
|
|
425
|
+
if (scope.match(/^catch#\d+$/)) return 'catch block';
|
|
426
|
+
if (scope.match(/^if#\d+$/)) return 'conditional';
|
|
427
|
+
if (scope.match(/^else#\d+$/)) return 'else block';
|
|
428
|
+
if (scope.match(/^for#\d+$/)) return 'loop';
|
|
429
|
+
if (scope.match(/^while#\d+$/)) return 'loop';
|
|
430
|
+
if (scope.match(/^switch#\d+$/)) return 'switch';
|
|
431
|
+
|
|
432
|
+
// Regular scope: function or class name
|
|
433
|
+
return scope;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Build "inside X, inside Y" string
|
|
437
|
+
return 'inside ' + formatted.join(', inside ');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if a node matches the search pattern based on its type.
|
|
442
|
+
*
|
|
443
|
+
* Different node types have different searchable fields:
|
|
444
|
+
* - http:route: search method and path fields
|
|
445
|
+
* - http:request: search method and url fields
|
|
446
|
+
* - socketio:event: search name field (standard)
|
|
447
|
+
* - socketio:emit/on: search event field
|
|
448
|
+
* - Default: search name field
|
|
449
|
+
*/
|
|
450
|
+
function matchesSearchPattern(
|
|
451
|
+
node: {
|
|
452
|
+
name?: string;
|
|
453
|
+
method?: string;
|
|
454
|
+
path?: string;
|
|
455
|
+
url?: string;
|
|
456
|
+
event?: string;
|
|
457
|
+
[key: string]: unknown
|
|
458
|
+
},
|
|
459
|
+
nodeType: string,
|
|
460
|
+
pattern: string
|
|
461
|
+
): boolean {
|
|
462
|
+
const lowerPattern = pattern.toLowerCase();
|
|
463
|
+
|
|
464
|
+
// HTTP routes: search method and path
|
|
465
|
+
if (nodeType === 'http:route') {
|
|
466
|
+
const method = (node.method || '').toLowerCase();
|
|
467
|
+
const path = (node.path || '').toLowerCase();
|
|
468
|
+
|
|
469
|
+
// Pattern could be: "POST", "/api/users", "POST /api", etc.
|
|
470
|
+
const patternParts = pattern.trim().split(/\s+/);
|
|
471
|
+
|
|
472
|
+
if (patternParts.length === 1) {
|
|
473
|
+
// Single term: match method OR path
|
|
474
|
+
const term = patternParts[0].toLowerCase();
|
|
475
|
+
return method === term || path.includes(term);
|
|
476
|
+
} else {
|
|
477
|
+
// Multiple terms: first is method, rest is path pattern
|
|
478
|
+
const methodPattern = patternParts[0].toLowerCase();
|
|
479
|
+
const pathPattern = patternParts.slice(1).join(' ').toLowerCase();
|
|
480
|
+
|
|
481
|
+
// Method must match exactly (GET, POST, etc.)
|
|
482
|
+
const methodMatches = method === methodPattern;
|
|
483
|
+
// Path must contain the pattern
|
|
484
|
+
const pathMatches = path.includes(pathPattern);
|
|
485
|
+
|
|
486
|
+
return methodMatches && pathMatches;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// HTTP requests: search method and url
|
|
491
|
+
if (nodeType === 'http:request') {
|
|
492
|
+
const method = (node.method || '').toLowerCase();
|
|
493
|
+
const url = (node.url || '').toLowerCase();
|
|
494
|
+
|
|
495
|
+
// Pattern could be: "POST", "/api/users", "POST /api", etc.
|
|
496
|
+
const patternParts = pattern.trim().split(/\s+/);
|
|
497
|
+
|
|
498
|
+
if (patternParts.length === 1) {
|
|
499
|
+
// Single term: match method OR url
|
|
500
|
+
const term = patternParts[0].toLowerCase();
|
|
501
|
+
return method === term || url.includes(term);
|
|
502
|
+
} else {
|
|
503
|
+
// Multiple terms: first is method, rest is url pattern
|
|
504
|
+
const methodPattern = patternParts[0].toLowerCase();
|
|
505
|
+
const urlPattern = patternParts.slice(1).join(' ').toLowerCase();
|
|
506
|
+
|
|
507
|
+
// Method must match exactly (GET, POST, etc.)
|
|
508
|
+
const methodMatches = method === methodPattern;
|
|
509
|
+
// URL must contain the pattern
|
|
510
|
+
const urlMatches = url.includes(urlPattern);
|
|
511
|
+
|
|
512
|
+
return methodMatches && urlMatches;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Socket.IO event channels: search name field (standard)
|
|
517
|
+
if (nodeType === 'socketio:event') {
|
|
518
|
+
const nodeName = (node.name || '').toLowerCase();
|
|
519
|
+
return nodeName.includes(lowerPattern);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Socket.IO emit/on: search event field
|
|
523
|
+
if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
|
|
524
|
+
const eventName = (node.event || '').toLowerCase();
|
|
525
|
+
return eventName.includes(lowerPattern);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Default: search name field
|
|
529
|
+
const nodeName = (node.name || '').toLowerCase();
|
|
530
|
+
return nodeName.includes(lowerPattern);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Find nodes by query (type, name, file scope, function scopes)
|
|
158
535
|
*/
|
|
159
536
|
async function findNodes(
|
|
160
537
|
backend: RFDBServerBackend,
|
|
161
|
-
|
|
162
|
-
name: string,
|
|
538
|
+
query: ParsedQuery,
|
|
163
539
|
limit: number
|
|
164
540
|
): Promise<NodeInfo[]> {
|
|
165
541
|
const results: NodeInfo[] = [];
|
|
166
|
-
const searchTypes = type
|
|
167
|
-
? [type]
|
|
168
|
-
: [
|
|
542
|
+
const searchTypes = query.type
|
|
543
|
+
? [query.type]
|
|
544
|
+
: [
|
|
545
|
+
'FUNCTION',
|
|
546
|
+
'CLASS',
|
|
547
|
+
'MODULE',
|
|
548
|
+
'VARIABLE',
|
|
549
|
+
'CONSTANT',
|
|
550
|
+
'http:route',
|
|
551
|
+
'http:request',
|
|
552
|
+
'socketio:event',
|
|
553
|
+
'socketio:emit',
|
|
554
|
+
'socketio:on'
|
|
555
|
+
];
|
|
169
556
|
|
|
170
557
|
for (const nodeType of searchTypes) {
|
|
171
558
|
for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
559
|
+
// Type-aware field matching (name)
|
|
560
|
+
const nameMatches = matchesSearchPattern(node, nodeType, query.name);
|
|
561
|
+
if (!nameMatches) continue;
|
|
562
|
+
|
|
563
|
+
// Scope matching (file and function scopes)
|
|
564
|
+
const scopeMatches = matchesScope(node.id, query.file, query.scopes);
|
|
565
|
+
if (!scopeMatches) continue;
|
|
566
|
+
|
|
567
|
+
const nodeInfo: NodeInfo = {
|
|
568
|
+
id: node.id,
|
|
569
|
+
type: node.type || nodeType,
|
|
570
|
+
name: node.name || '',
|
|
571
|
+
file: node.file || '',
|
|
572
|
+
line: node.line,
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// Add scope context for display
|
|
576
|
+
nodeInfo.scopeContext = extractScopeContext(node.id);
|
|
577
|
+
|
|
578
|
+
// Include method and path for http:route nodes
|
|
579
|
+
if (nodeType === 'http:route') {
|
|
580
|
+
nodeInfo.method = node.method as string | undefined;
|
|
581
|
+
nodeInfo.path = node.path as string | undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Include method and url for http:request nodes
|
|
585
|
+
if (nodeType === 'http:request') {
|
|
586
|
+
nodeInfo.method = node.method as string | undefined;
|
|
587
|
+
nodeInfo.url = node.url as string | undefined;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Include event field for Socket.IO nodes
|
|
591
|
+
if (nodeType === 'socketio:event' || nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
|
|
592
|
+
nodeInfo.event = node.event as string | undefined;
|
|
183
593
|
}
|
|
594
|
+
|
|
595
|
+
// Include emit-specific fields
|
|
596
|
+
if (nodeType === 'socketio:emit') {
|
|
597
|
+
nodeInfo.room = node.room as string | undefined;
|
|
598
|
+
nodeInfo.namespace = node.namespace as string | undefined;
|
|
599
|
+
nodeInfo.broadcast = node.broadcast as boolean | undefined;
|
|
600
|
+
nodeInfo.objectName = node.objectName as string | undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Include listener-specific fields
|
|
604
|
+
if (nodeType === 'socketio:on') {
|
|
605
|
+
nodeInfo.objectName = node.objectName as string | undefined;
|
|
606
|
+
nodeInfo.handlerName = node.handlerName as string | undefined;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
results.push(nodeInfo);
|
|
610
|
+
if (results.length >= limit) break;
|
|
184
611
|
}
|
|
185
612
|
if (results.length >= limit) break;
|
|
186
613
|
}
|
|
@@ -213,8 +640,8 @@ async function getCallers(
|
|
|
213
640
|
const callNode = await backend.getNode(edge.src);
|
|
214
641
|
if (!callNode) continue;
|
|
215
642
|
|
|
216
|
-
// Find the FUNCTION that contains this CALL
|
|
217
|
-
const containingFunc = await
|
|
643
|
+
// Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
|
|
644
|
+
const containingFunc = await findContainingFunctionCore(backend, callNode.id);
|
|
218
645
|
|
|
219
646
|
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
220
647
|
seen.add(containingFunc.id);
|
|
@@ -227,173 +654,240 @@ async function getCallers(
|
|
|
227
654
|
});
|
|
228
655
|
}
|
|
229
656
|
}
|
|
230
|
-
} catch {
|
|
231
|
-
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (process.env.DEBUG) {
|
|
659
|
+
console.error('[query] Error in getCallers:', error);
|
|
660
|
+
}
|
|
232
661
|
}
|
|
233
662
|
|
|
234
663
|
return callers;
|
|
235
664
|
}
|
|
236
665
|
|
|
237
666
|
/**
|
|
238
|
-
*
|
|
667
|
+
* Get functions that this node calls
|
|
239
668
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
669
|
+
* Uses shared utility from @grafema/core which:
|
|
670
|
+
* - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
|
|
671
|
+
* - Finds both CALL and METHOD_CALL nodes
|
|
672
|
+
* - Only returns resolved calls (those with CALLS edges to targets)
|
|
242
673
|
*/
|
|
243
|
-
async function
|
|
674
|
+
async function getCallees(
|
|
244
675
|
backend: RFDBServerBackend,
|
|
245
676
|
nodeId: string,
|
|
246
|
-
|
|
247
|
-
): Promise<NodeInfo
|
|
248
|
-
const
|
|
249
|
-
const
|
|
677
|
+
limit: number
|
|
678
|
+
): Promise<NodeInfo[]> {
|
|
679
|
+
const callees: NodeInfo[] = [];
|
|
680
|
+
const seen = new Set<string>();
|
|
250
681
|
|
|
251
|
-
|
|
252
|
-
|
|
682
|
+
try {
|
|
683
|
+
// Use shared utility (now includes METHOD_CALL and correct graph traversal)
|
|
684
|
+
const calls = await findCallsInFunctionCore(backend, nodeId);
|
|
253
685
|
|
|
254
|
-
|
|
255
|
-
|
|
686
|
+
for (const call of calls) {
|
|
687
|
+
if (callees.length >= limit) break;
|
|
256
688
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
689
|
+
// Only include resolved calls with targets
|
|
690
|
+
if (call.resolved && call.target && !seen.has(call.target.id)) {
|
|
691
|
+
seen.add(call.target.id);
|
|
692
|
+
callees.push({
|
|
693
|
+
id: call.target.id,
|
|
694
|
+
type: 'FUNCTION',
|
|
695
|
+
name: call.target.name || '<anonymous>',
|
|
696
|
+
file: call.target.file || '',
|
|
697
|
+
line: call.target.line,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (process.env.DEBUG) {
|
|
703
|
+
console.error('[query] Error in getCallees:', error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
260
706
|
|
|
261
|
-
|
|
262
|
-
|
|
707
|
+
return callees;
|
|
708
|
+
}
|
|
263
709
|
|
|
264
|
-
|
|
265
|
-
|
|
710
|
+
/**
|
|
711
|
+
* Display a node with semantic ID as primary identifier
|
|
712
|
+
*/
|
|
713
|
+
async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBServerBackend): Promise<void> {
|
|
714
|
+
// Special formatting for HTTP routes
|
|
715
|
+
if (node.type === 'http:route' && node.method && node.path) {
|
|
716
|
+
console.log(formatHttpRouteDisplay(node, projectPath));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
266
719
|
|
|
267
|
-
|
|
268
|
-
|
|
720
|
+
// Special formatting for HTTP requests
|
|
721
|
+
if (node.type === 'http:request') {
|
|
722
|
+
console.log(formatHttpRequestDisplay(node, projectPath));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
269
725
|
|
|
270
|
-
|
|
726
|
+
// Special formatting for Socket.IO event channels
|
|
727
|
+
if (node.type === 'socketio:event') {
|
|
728
|
+
console.log(await formatSocketEventDisplay(node, projectPath, backend));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
271
731
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
name: parentNode.name || '<anonymous>',
|
|
278
|
-
file: parentNode.file || '',
|
|
279
|
-
line: parentNode.line,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
732
|
+
// Special formatting for Socket.IO emit/on
|
|
733
|
+
if (node.type === 'socketio:emit' || node.type === 'socketio:on') {
|
|
734
|
+
console.log(formatSocketIONodeDisplay(node, projectPath));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
282
737
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
738
|
+
console.log(formatNodeDisplay(node, { projectPath }));
|
|
739
|
+
|
|
740
|
+
// Add scope context if present
|
|
741
|
+
if (node.scopeContext) {
|
|
742
|
+
console.log(` Scope: ${node.scopeContext}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Format HTTP route for display
|
|
748
|
+
*
|
|
749
|
+
* Output:
|
|
750
|
+
* [http:route] POST /api/users
|
|
751
|
+
* Location: src/routes/users.js:15
|
|
752
|
+
*/
|
|
753
|
+
function formatHttpRouteDisplay(node: NodeInfo, projectPath: string): string {
|
|
754
|
+
const lines: string[] = [];
|
|
755
|
+
|
|
756
|
+
// Line 1: [type] METHOD PATH
|
|
757
|
+
lines.push(`[${node.type}] ${node.method} ${node.path}`);
|
|
758
|
+
|
|
759
|
+
// Line 2: Location
|
|
760
|
+
if (node.file) {
|
|
761
|
+
const relPath = relative(projectPath, node.file);
|
|
762
|
+
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
763
|
+
lines.push(` Location: ${loc}`);
|
|
289
764
|
}
|
|
290
765
|
|
|
291
|
-
return
|
|
766
|
+
return lines.join('\n');
|
|
292
767
|
}
|
|
293
768
|
|
|
294
769
|
/**
|
|
295
|
-
*
|
|
770
|
+
* Format HTTP request for display
|
|
296
771
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
772
|
+
* Output:
|
|
773
|
+
* [http:request] GET /api/users
|
|
774
|
+
* Location: src/pages/Users.tsx:42
|
|
299
775
|
*/
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
776
|
+
function formatHttpRequestDisplay(node: NodeInfo, projectPath: string): string {
|
|
777
|
+
const lines: string[] = [];
|
|
778
|
+
|
|
779
|
+
// Line 1: [type] METHOD URL
|
|
780
|
+
const method = node.method || 'GET';
|
|
781
|
+
const url = node.url || 'dynamic';
|
|
782
|
+
lines.push(`[${node.type}] ${method} ${url}`);
|
|
783
|
+
|
|
784
|
+
// Line 2: Location
|
|
785
|
+
if (node.file) {
|
|
786
|
+
const relPath = relative(projectPath, node.file);
|
|
787
|
+
const loc = node.line ? `${relPath}:${node.line}` : relPath;
|
|
788
|
+
lines.push(` Location: ${loc}`);
|
|
789
|
+
}
|
|
307
790
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const callNodes = await findCallsInFunction(backend, nodeId);
|
|
791
|
+
return lines.join('\n');
|
|
792
|
+
}
|
|
311
793
|
|
|
312
|
-
|
|
313
|
-
|
|
794
|
+
/**
|
|
795
|
+
* Format Socket.IO event channel for display
|
|
796
|
+
*
|
|
797
|
+
* Output:
|
|
798
|
+
* [socketio:event] slot:booked
|
|
799
|
+
* ID: socketio:event#slot:booked
|
|
800
|
+
* Emitted by: 3 locations
|
|
801
|
+
* Listened by: 5 locations
|
|
802
|
+
*/
|
|
803
|
+
async function formatSocketEventDisplay(
|
|
804
|
+
node: NodeInfo,
|
|
805
|
+
projectPath: string,
|
|
806
|
+
backend: RFDBServerBackend
|
|
807
|
+
): Promise<string> {
|
|
808
|
+
const lines: string[] = [];
|
|
809
|
+
|
|
810
|
+
// Line 1: [type] event_name
|
|
811
|
+
lines.push(`[${node.type}] ${node.name}`);
|
|
314
812
|
|
|
315
|
-
|
|
316
|
-
|
|
813
|
+
// Line 2: ID
|
|
814
|
+
lines.push(` ID: ${node.id}`);
|
|
317
815
|
|
|
318
|
-
|
|
319
|
-
|
|
816
|
+
// Query edges to get emitter and listener counts
|
|
817
|
+
try {
|
|
818
|
+
const incomingEdges = await backend.getIncomingEdges(node.id, ['EMITS_EVENT']);
|
|
819
|
+
const outgoingEdges = await backend.getOutgoingEdges(node.id, ['LISTENED_BY']);
|
|
320
820
|
|
|
321
|
-
|
|
322
|
-
|
|
821
|
+
if (incomingEdges.length > 0) {
|
|
822
|
+
lines.push(` Emitted by: ${incomingEdges.length} location${incomingEdges.length !== 1 ? 's' : ''}`);
|
|
823
|
+
}
|
|
323
824
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
line: targetNode.line,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
825
|
+
if (outgoingEdges.length > 0) {
|
|
826
|
+
lines.push(` Listened by: ${outgoingEdges.length} location${outgoingEdges.length !== 1 ? 's' : ''}`);
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (process.env.DEBUG) {
|
|
830
|
+
console.error('[query] Error in formatSocketEventDisplay:', error);
|
|
333
831
|
}
|
|
334
|
-
} catch {
|
|
335
|
-
// Ignore errors
|
|
336
832
|
}
|
|
337
833
|
|
|
338
|
-
return
|
|
834
|
+
return lines.join('\n');
|
|
339
835
|
}
|
|
340
836
|
|
|
341
837
|
/**
|
|
342
|
-
*
|
|
838
|
+
* Format Socket.IO emit/on for display
|
|
839
|
+
*
|
|
840
|
+
* Output for emit:
|
|
841
|
+
* [socketio:emit] slot:booked
|
|
842
|
+
* ID: socketio:emit#slot:booked#server.js#28
|
|
843
|
+
* Location: server.js:28
|
|
844
|
+
* Room: gig:123 (if applicable)
|
|
845
|
+
* Namespace: /admin (if applicable)
|
|
846
|
+
* Broadcast: true (if applicable)
|
|
847
|
+
*
|
|
848
|
+
* Output for on:
|
|
849
|
+
* [socketio:on] slot:booked
|
|
850
|
+
* ID: socketio:on#slot:booked#client.js#13
|
|
851
|
+
* Location: client.js:13
|
|
852
|
+
* Handler: anonymous:27
|
|
343
853
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
nodeId: string,
|
|
347
|
-
maxDepth: number = 10
|
|
348
|
-
): Promise<NodeInfo[]> {
|
|
349
|
-
const calls: NodeInfo[] = [];
|
|
350
|
-
const visited = new Set<string>();
|
|
351
|
-
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
854
|
+
function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string {
|
|
855
|
+
const lines: string[] = [];
|
|
352
856
|
|
|
353
|
-
|
|
354
|
-
|
|
857
|
+
// Line 1: [type] event_name
|
|
858
|
+
const eventName = node.event || node.name || 'unknown';
|
|
859
|
+
lines.push(`[${node.type}] ${eventName}`);
|
|
355
860
|
|
|
356
|
-
|
|
357
|
-
|
|
861
|
+
// Line 2: ID
|
|
862
|
+
lines.push(` ID: ${node.id}`);
|
|
358
863
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const childType = child.type;
|
|
368
|
-
|
|
369
|
-
if (childType === 'CALL') {
|
|
370
|
-
calls.push({
|
|
371
|
-
id: child.id,
|
|
372
|
-
type: 'CALL',
|
|
373
|
-
name: child.name || '',
|
|
374
|
-
file: child.file || '',
|
|
375
|
-
line: child.line,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
864
|
+
// Line 3: Location (if applicable)
|
|
865
|
+
if (node.file) {
|
|
866
|
+
const loc = formatLocation(node.file, node.line, projectPath);
|
|
867
|
+
if (loc) {
|
|
868
|
+
lines.push(` Location: ${loc}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
378
871
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
872
|
+
// Emit-specific fields
|
|
873
|
+
if (node.type === 'socketio:emit') {
|
|
874
|
+
if (node.room) {
|
|
875
|
+
lines.push(` Room: ${node.room}`);
|
|
876
|
+
}
|
|
877
|
+
if (node.namespace) {
|
|
878
|
+
lines.push(` Namespace: ${node.namespace}`);
|
|
879
|
+
}
|
|
880
|
+
if (node.broadcast) {
|
|
881
|
+
lines.push(` Broadcast: true`);
|
|
386
882
|
}
|
|
387
883
|
}
|
|
388
884
|
|
|
389
|
-
|
|
390
|
-
|
|
885
|
+
// Listener-specific fields
|
|
886
|
+
if (node.type === 'socketio:on' && node.handlerName) {
|
|
887
|
+
lines.push(` Handler: ${node.handlerName}`);
|
|
888
|
+
}
|
|
391
889
|
|
|
392
|
-
|
|
393
|
-
* Display a node with semantic ID as primary identifier
|
|
394
|
-
*/
|
|
395
|
-
function displayNode(node: NodeInfo, projectPath: string): void {
|
|
396
|
-
console.log(formatNodeDisplay(node, { projectPath }));
|
|
890
|
+
return lines.join('\n');
|
|
397
891
|
}
|
|
398
892
|
|
|
399
893
|
/**
|