@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.
Files changed (79) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/commands/analyze.d.ts.map +1 -1
  3. package/dist/commands/analyze.js +69 -11
  4. package/dist/commands/check.d.ts +6 -0
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +177 -1
  7. package/dist/commands/coverage.d.ts.map +1 -1
  8. package/dist/commands/coverage.js +7 -0
  9. package/dist/commands/doctor/checks.d.ts +55 -0
  10. package/dist/commands/doctor/checks.d.ts.map +1 -0
  11. package/dist/commands/doctor/checks.js +534 -0
  12. package/dist/commands/doctor/output.d.ts +20 -0
  13. package/dist/commands/doctor/output.d.ts.map +1 -0
  14. package/dist/commands/doctor/output.js +94 -0
  15. package/dist/commands/doctor/types.d.ts +42 -0
  16. package/dist/commands/doctor/types.d.ts.map +1 -0
  17. package/dist/commands/doctor/types.js +4 -0
  18. package/dist/commands/doctor.d.ts +17 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +80 -0
  21. package/dist/commands/explain.d.ts +16 -0
  22. package/dist/commands/explain.d.ts.map +1 -0
  23. package/dist/commands/explain.js +145 -0
  24. package/dist/commands/explore.d.ts +7 -1
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +204 -85
  27. package/dist/commands/get.d.ts.map +1 -1
  28. package/dist/commands/get.js +16 -4
  29. package/dist/commands/impact.d.ts.map +1 -1
  30. package/dist/commands/impact.js +48 -50
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +93 -15
  33. package/dist/commands/ls.d.ts +14 -0
  34. package/dist/commands/ls.d.ts.map +1 -0
  35. package/dist/commands/ls.js +132 -0
  36. package/dist/commands/overview.d.ts.map +1 -1
  37. package/dist/commands/overview.js +15 -2
  38. package/dist/commands/query.d.ts +98 -0
  39. package/dist/commands/query.d.ts.map +1 -1
  40. package/dist/commands/query.js +549 -136
  41. package/dist/commands/schema.d.ts +13 -0
  42. package/dist/commands/schema.d.ts.map +1 -0
  43. package/dist/commands/schema.js +279 -0
  44. package/dist/commands/server.d.ts.map +1 -1
  45. package/dist/commands/server.js +13 -6
  46. package/dist/commands/stats.d.ts.map +1 -1
  47. package/dist/commands/stats.js +7 -0
  48. package/dist/commands/trace.d.ts +73 -0
  49. package/dist/commands/trace.d.ts.map +1 -1
  50. package/dist/commands/trace.js +500 -5
  51. package/dist/commands/types.d.ts +12 -0
  52. package/dist/commands/types.d.ts.map +1 -0
  53. package/dist/commands/types.js +79 -0
  54. package/dist/utils/formatNode.d.ts +13 -0
  55. package/dist/utils/formatNode.d.ts.map +1 -1
  56. package/dist/utils/formatNode.js +35 -2
  57. package/package.json +3 -3
  58. package/src/cli.ts +10 -0
  59. package/src/commands/analyze.ts +84 -9
  60. package/src/commands/check.ts +201 -0
  61. package/src/commands/coverage.ts +7 -0
  62. package/src/commands/doctor/checks.ts +612 -0
  63. package/src/commands/doctor/output.ts +115 -0
  64. package/src/commands/doctor/types.ts +45 -0
  65. package/src/commands/doctor.ts +106 -0
  66. package/src/commands/explain.ts +173 -0
  67. package/src/commands/explore.tsx +247 -97
  68. package/src/commands/get.ts +20 -6
  69. package/src/commands/impact.ts +55 -61
  70. package/src/commands/init.ts +101 -14
  71. package/src/commands/ls.ts +166 -0
  72. package/src/commands/overview.ts +15 -2
  73. package/src/commands/query.ts +643 -149
  74. package/src/commands/schema.ts +345 -0
  75. package/src/commands/server.ts +13 -6
  76. package/src/commands/stats.ts +7 -0
  77. package/src/commands/trace.ts +647 -6
  78. package/src/commands/types.ts +94 -0
  79. package/src/utils/formatNode.ts +42 -2
@@ -4,19 +4,32 @@
4
4
  * Usage:
5
5
  * grafema trace "userId from authenticate"
6
6
  * grafema trace "config"
7
+ * grafema trace --to "addNode#0.type" (sink-based trace)
7
8
  */
8
9
  import { Command } from 'commander';
9
10
  import { resolve, join } from 'path';
10
11
  import { existsSync } from 'fs';
11
- import { RFDBServerBackend, parseSemanticId } from '@grafema/core';
12
+ import { RFDBServerBackend, parseSemanticId, traceValues } from '@grafema/core';
12
13
  import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
13
14
  import { exitWithError } from '../utils/errorFormatter.js';
14
15
  export const traceCommand = new Command('trace')
15
- .description('Trace data flow for a variable')
16
- .argument('<pattern>', 'Pattern: "varName from functionName" or just "varName"')
16
+ .description('Trace data flow for a variable or to a sink point')
17
+ .argument('[pattern]', 'Pattern: "varName from functionName" or just "varName"')
17
18
  .option('-p, --project <path>', 'Project path', '.')
18
19
  .option('-j, --json', 'Output as JSON')
19
20
  .option('-d, --depth <n>', 'Max trace depth', '10')
21
+ .option('-t, --to <sink>', 'Sink point: "fn#argIndex.property" (e.g., "addNode#0.type")')
22
+ .option('-r, --from-route <pattern>', 'Trace from route response (e.g., "GET /status" or "/status")')
23
+ .addHelpText('after', `
24
+ Examples:
25
+ grafema trace "userId" Trace all variables named "userId"
26
+ grafema trace "userId from authenticate" Trace userId within authenticate function
27
+ grafema trace "config" --depth 5 Limit trace depth to 5 levels
28
+ grafema trace "apiKey" --json Output trace as JSON
29
+ grafema trace --to "addNode#0.type" Trace values reaching sink point
30
+ grafema trace --from-route "GET /status" Trace values from route response
31
+ grafema trace -r "/status" Trace by path only
32
+ `)
20
33
  .action(async (pattern, options) => {
21
34
  const projectPath = resolve(options.project);
22
35
  const grafemaDir = join(projectPath, '.grafema');
@@ -27,6 +40,21 @@ export const traceCommand = new Command('trace')
27
40
  const backend = new RFDBServerBackend({ dbPath });
28
41
  await backend.connect();
29
42
  try {
43
+ // Handle sink-based trace if --to option is provided
44
+ if (options.to) {
45
+ await handleSinkTrace(backend, options.to, projectPath, options.json);
46
+ return;
47
+ }
48
+ // Handle route-based trace if --from-route option is provided
49
+ if (options.fromRoute) {
50
+ const maxDepth = parseInt(options.depth, 10);
51
+ await handleRouteTrace(backend, options.fromRoute, projectPath, options.json, maxDepth);
52
+ return;
53
+ }
54
+ // Regular trace requires pattern
55
+ if (!pattern) {
56
+ exitWithError('Pattern required', ['Provide a pattern, use --to for sink trace, or --from-route for route trace']);
57
+ }
30
58
  // Parse pattern: "varName from functionName" or just "varName"
31
59
  const { varName, scopeName } = parseTracePattern(pattern);
32
60
  const maxDepth = parseInt(options.depth, 10);
@@ -141,6 +169,7 @@ async function findVariables(backend, varName, scopeName) {
141
169
  async function traceBackward(backend, startId, maxDepth) {
142
170
  const trace = [];
143
171
  const visited = new Set();
172
+ const seenNodes = new Set();
144
173
  const queue = [{ id: startId, depth: 0 }];
145
174
  while (queue.length > 0) {
146
175
  const { id, depth } = queue.shift();
@@ -153,6 +182,9 @@ async function traceBackward(backend, startId, maxDepth) {
153
182
  const targetNode = await backend.getNode(edge.dst);
154
183
  if (!targetNode)
155
184
  continue;
185
+ if (seenNodes.has(targetNode.id))
186
+ continue;
187
+ seenNodes.add(targetNode.id);
156
188
  const nodeInfo = {
157
189
  id: targetNode.id,
158
190
  type: targetNode.type || 'UNKNOWN',
@@ -163,7 +195,7 @@ async function traceBackward(backend, startId, maxDepth) {
163
195
  };
164
196
  trace.push({
165
197
  node: nodeInfo,
166
- edgeType: edge.edgeType || edge.type,
198
+ edgeType: edge.type,
167
199
  depth: depth + 1,
168
200
  });
169
201
  // Continue tracing unless we hit a leaf
@@ -185,6 +217,7 @@ async function traceBackward(backend, startId, maxDepth) {
185
217
  async function traceForward(backend, startId, maxDepth) {
186
218
  const trace = [];
187
219
  const visited = new Set();
220
+ const seenNodes = new Set();
188
221
  const queue = [{ id: startId, depth: 0 }];
189
222
  while (queue.length > 0) {
190
223
  const { id, depth } = queue.shift();
@@ -198,6 +231,9 @@ async function traceForward(backend, startId, maxDepth) {
198
231
  const sourceNode = await backend.getNode(edge.src);
199
232
  if (!sourceNode)
200
233
  continue;
234
+ if (seenNodes.has(sourceNode.id))
235
+ continue;
236
+ seenNodes.add(sourceNode.id);
201
237
  const nodeInfo = {
202
238
  id: sourceNode.id,
203
239
  type: sourceNode.type || 'UNKNOWN',
@@ -207,7 +243,7 @@ async function traceForward(backend, startId, maxDepth) {
207
243
  };
208
244
  trace.push({
209
245
  node: nodeInfo,
210
- edgeType: edge.edgeType || edge.type,
246
+ edgeType: edge.type,
211
247
  depth: depth + 1,
212
248
  });
213
249
  // Continue forward
@@ -268,3 +304,462 @@ function displayTrace(trace, _projectPath, indent) {
268
304
  }
269
305
  }
270
306
  }
307
+ // =============================================================================
308
+ // SINK-BASED TRACE IMPLEMENTATION (REG-230)
309
+ // =============================================================================
310
+ /**
311
+ * Parse sink specification string into structured format
312
+ *
313
+ * Format: "functionName#argIndex.property.path"
314
+ * Examples:
315
+ * - "addNode#0.type" -> {functionName: "addNode", argIndex: 0, propertyPath: ["type"]}
316
+ * - "fn#0" -> {functionName: "fn", argIndex: 0, propertyPath: []}
317
+ * - "add_node_v2#1.config.options" -> {functionName: "add_node_v2", argIndex: 1, propertyPath: ["config", "options"]}
318
+ *
319
+ * @throws Error if spec is invalid
320
+ */
321
+ export function parseSinkSpec(spec) {
322
+ if (!spec || spec.trim() === '') {
323
+ throw new Error('Invalid sink spec: empty string');
324
+ }
325
+ const trimmed = spec.trim();
326
+ // Must contain # separator
327
+ const hashIndex = trimmed.indexOf('#');
328
+ if (hashIndex === -1) {
329
+ throw new Error('Invalid sink spec: missing # separator');
330
+ }
331
+ // Extract function name (before #)
332
+ const functionName = trimmed.substring(0, hashIndex);
333
+ if (!functionName || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(functionName)) {
334
+ throw new Error('Invalid sink spec: invalid function name');
335
+ }
336
+ // Extract argument index and optional property path (after #)
337
+ const afterHash = trimmed.substring(hashIndex + 1);
338
+ if (!afterHash) {
339
+ throw new Error('Invalid sink spec: missing argument index');
340
+ }
341
+ // Split by first dot to separate argIndex from property path
342
+ const dotIndex = afterHash.indexOf('.');
343
+ const argIndexStr = dotIndex === -1 ? afterHash : afterHash.substring(0, dotIndex);
344
+ const propertyPathStr = dotIndex === -1 ? '' : afterHash.substring(dotIndex + 1);
345
+ // Parse argument index
346
+ if (!/^\d+$/.test(argIndexStr)) {
347
+ throw new Error('Invalid sink spec: argument index must be numeric');
348
+ }
349
+ const argIndex = parseInt(argIndexStr, 10);
350
+ if (argIndex < 0) {
351
+ throw new Error('Invalid sink spec: negative argument index');
352
+ }
353
+ // Parse property path (split by dots)
354
+ const propertyPath = propertyPathStr ? propertyPathStr.split('.').filter(p => p) : [];
355
+ return {
356
+ functionName,
357
+ argIndex,
358
+ propertyPath,
359
+ raw: trimmed,
360
+ };
361
+ }
362
+ /**
363
+ * Find all call sites for a function by name
364
+ *
365
+ * Handles both:
366
+ * - Direct calls: fn() where name === targetFunctionName
367
+ * - Method calls: obj.fn() where method attribute === targetFunctionName
368
+ */
369
+ export async function findCallSites(backend, targetFunctionName) {
370
+ const callSites = [];
371
+ for await (const node of backend.queryNodes({ nodeType: 'CALL' })) {
372
+ const nodeName = node.name || '';
373
+ const nodeMethod = node.method || '';
374
+ // Match direct calls (name === targetFunctionName)
375
+ // Or method calls (method === targetFunctionName)
376
+ if (nodeName === targetFunctionName || nodeMethod === targetFunctionName) {
377
+ callSites.push({
378
+ id: node.id,
379
+ calleeFunction: targetFunctionName,
380
+ file: node.file || '',
381
+ line: node.line || 0,
382
+ });
383
+ }
384
+ }
385
+ return callSites;
386
+ }
387
+ /**
388
+ * Extract the argument node ID at a specific index from a call site
389
+ *
390
+ * Follows PASSES_ARGUMENT edges and matches by argIndex metadata
391
+ *
392
+ * @returns Node ID of the argument, or null if not found
393
+ */
394
+ export async function extractArgument(backend, callSiteId, argIndex) {
395
+ const edges = await backend.getOutgoingEdges(callSiteId, ['PASSES_ARGUMENT']);
396
+ for (const edge of edges) {
397
+ // argIndex is stored in edge metadata
398
+ const edgeArgIndex = edge.metadata?.argIndex;
399
+ if (edgeArgIndex === argIndex) {
400
+ return edge.dst;
401
+ }
402
+ }
403
+ return null;
404
+ }
405
+ /**
406
+ * Extract a property from a node by following HAS_PROPERTY edges
407
+ *
408
+ * If node is a VARIABLE, first traces through ASSIGNED_FROM to find OBJECT_LITERAL
409
+ *
410
+ * @returns Node ID of the property value, or null if not found
411
+ */
412
+ async function extractProperty(backend, nodeId, propertyName) {
413
+ const node = await backend.getNode(nodeId);
414
+ if (!node)
415
+ return null;
416
+ const nodeType = node.type || node.nodeType;
417
+ // If it's an OBJECT_LITERAL, follow HAS_PROPERTY directly
418
+ if (nodeType === 'OBJECT_LITERAL') {
419
+ const edges = await backend.getOutgoingEdges(nodeId, ['HAS_PROPERTY']);
420
+ for (const edge of edges) {
421
+ if (edge.metadata?.propertyName === propertyName) {
422
+ return edge.dst;
423
+ }
424
+ }
425
+ return null;
426
+ }
427
+ // If it's a VARIABLE, first trace to the object literal
428
+ if (nodeType === 'VARIABLE' || nodeType === 'CONSTANT') {
429
+ const assignedEdges = await backend.getOutgoingEdges(nodeId, ['ASSIGNED_FROM']);
430
+ for (const edge of assignedEdges) {
431
+ const result = await extractProperty(backend, edge.dst, propertyName);
432
+ if (result)
433
+ return result;
434
+ }
435
+ }
436
+ return null;
437
+ }
438
+ /**
439
+ * Trace a node to its literal values.
440
+ * Uses shared traceValues utility from @grafema/core (REG-244).
441
+ *
442
+ * @param backend - RFDBServerBackend for graph queries
443
+ * @param nodeId - Starting node ID
444
+ * @param _visited - Kept for API compatibility (internal cycle detection in shared utility)
445
+ * @param maxDepth - Maximum traversal depth
446
+ * @returns Array of traced values with sources
447
+ */
448
+ async function traceToLiterals(backend, nodeId, _visited = new Set(), maxDepth = 10) {
449
+ // RFDBServerBackend implements TraceValuesGraphBackend interface
450
+ const traced = await traceValues(backend, nodeId, {
451
+ maxDepth,
452
+ followDerivesFrom: true,
453
+ detectNondeterministic: true,
454
+ });
455
+ // Map to expected format (strip reason field)
456
+ return traced.map(t => ({
457
+ value: t.value,
458
+ source: t.source,
459
+ isUnknown: t.isUnknown,
460
+ }));
461
+ }
462
+ /**
463
+ * Resolve a sink specification to all possible values
464
+ *
465
+ * This is the main entry point for sink-based trace.
466
+ * It finds all call sites, extracts the specified argument,
467
+ * optionally follows property path, and traces to literal values.
468
+ */
469
+ export async function resolveSink(backend, sink) {
470
+ // Find all call sites for the function
471
+ const callSites = await findCallSites(backend, sink.functionName);
472
+ const resolvedCallSites = [];
473
+ const valueMap = new Map();
474
+ let hasUnknown = false;
475
+ let totalSources = 0;
476
+ for (const callSite of callSites) {
477
+ resolvedCallSites.push(callSite);
478
+ // Extract the argument at the specified index
479
+ const argNodeId = await extractArgument(backend, callSite.id, sink.argIndex);
480
+ if (!argNodeId) {
481
+ // Argument doesn't exist at this call site
482
+ continue;
483
+ }
484
+ // If property path specified, navigate to that property
485
+ let targetNodeId = argNodeId;
486
+ for (const propName of sink.propertyPath) {
487
+ const propNodeId = await extractProperty(backend, targetNodeId, propName);
488
+ if (!propNodeId) {
489
+ // Property not found, mark as unknown
490
+ hasUnknown = true;
491
+ targetNodeId = '';
492
+ break;
493
+ }
494
+ targetNodeId = propNodeId;
495
+ }
496
+ if (!targetNodeId)
497
+ continue;
498
+ // Trace to literal values
499
+ const literals = await traceToLiterals(backend, targetNodeId);
500
+ for (const lit of literals) {
501
+ if (lit.isUnknown) {
502
+ hasUnknown = true;
503
+ continue;
504
+ }
505
+ totalSources++;
506
+ const valueKey = JSON.stringify(lit.value);
507
+ if (valueMap.has(valueKey)) {
508
+ valueMap.get(valueKey).sources.push(lit.source);
509
+ }
510
+ else {
511
+ valueMap.set(valueKey, {
512
+ value: lit.value,
513
+ sources: [lit.source],
514
+ });
515
+ }
516
+ }
517
+ }
518
+ // Convert map to array
519
+ const possibleValues = Array.from(valueMap.values());
520
+ return {
521
+ sink,
522
+ resolvedCallSites,
523
+ possibleValues,
524
+ statistics: {
525
+ callSites: callSites.length,
526
+ totalSources,
527
+ uniqueValues: possibleValues.length,
528
+ unknownElements: hasUnknown,
529
+ },
530
+ };
531
+ }
532
+ /**
533
+ * Handle sink trace command (--to option)
534
+ */
535
+ async function handleSinkTrace(backend, sinkSpec, projectPath, jsonOutput) {
536
+ // Parse the sink specification
537
+ const sink = parseSinkSpec(sinkSpec);
538
+ // Resolve the sink
539
+ const result = await resolveSink(backend, sink);
540
+ if (jsonOutput) {
541
+ console.log(JSON.stringify(result, null, 2));
542
+ return;
543
+ }
544
+ // Human-readable output
545
+ console.log(`Sink: ${sink.raw}`);
546
+ console.log(`Resolved to ${result.statistics.callSites} call site(s)`);
547
+ console.log('');
548
+ if (result.possibleValues.length === 0) {
549
+ if (result.statistics.unknownElements) {
550
+ console.log('Possible values: <unknown> (runtime/parameter values)');
551
+ }
552
+ else {
553
+ console.log('No values found');
554
+ }
555
+ return;
556
+ }
557
+ console.log('Possible values:');
558
+ for (const pv of result.possibleValues) {
559
+ const sourcesCount = pv.sources.length;
560
+ console.log(` - ${JSON.stringify(pv.value)} (${sourcesCount} source${sourcesCount === 1 ? '' : 's'})`);
561
+ for (const src of pv.sources.slice(0, 3)) {
562
+ const relativePath = src.file.startsWith(projectPath)
563
+ ? src.file.substring(projectPath.length + 1)
564
+ : src.file;
565
+ console.log(` <- ${relativePath}:${src.line}`);
566
+ }
567
+ if (pv.sources.length > 3) {
568
+ console.log(` ... and ${pv.sources.length - 3} more`);
569
+ }
570
+ }
571
+ if (result.statistics.unknownElements) {
572
+ console.log('');
573
+ console.log('Note: Some values could not be determined (runtime/parameter inputs)');
574
+ }
575
+ }
576
+ // =============================================================================
577
+ // ROUTE-BASED TRACE IMPLEMENTATION (REG-326)
578
+ // =============================================================================
579
+ /**
580
+ * Find route by pattern.
581
+ *
582
+ * Supports:
583
+ * - "METHOD /path" format (e.g., "GET /status")
584
+ * - "/path" format (e.g., "/status")
585
+ *
586
+ * Matching strategy:
587
+ * 1. Try exact "METHOD PATH" match
588
+ * 2. Try "/PATH" only match (any method)
589
+ *
590
+ * @param backend - Graph backend
591
+ * @param pattern - Route pattern (with or without method)
592
+ * @returns Route node or null if not found
593
+ */
594
+ async function findRouteByPattern(backend, pattern) {
595
+ const trimmed = pattern.trim();
596
+ for await (const node of backend.queryNodes({ type: 'http:route' })) {
597
+ const method = node.method || '';
598
+ const path = node.path || '';
599
+ // Match "METHOD /path"
600
+ if (`${method} ${path}` === trimmed) {
601
+ return {
602
+ id: node.id,
603
+ type: node.type || 'http:route',
604
+ name: `${method} ${path}`,
605
+ file: node.file || '',
606
+ line: node.line
607
+ };
608
+ }
609
+ // Match "/path" only (ignore method)
610
+ if (path === trimmed) {
611
+ return {
612
+ id: node.id,
613
+ type: node.type || 'http:route',
614
+ name: `${method} ${path}`,
615
+ file: node.file || '',
616
+ line: node.line
617
+ };
618
+ }
619
+ }
620
+ return null;
621
+ }
622
+ /**
623
+ * Handle route-based trace (--from-route option).
624
+ *
625
+ * Flow:
626
+ * 1. Find route by pattern
627
+ * 2. Get RESPONDS_WITH edges from route
628
+ * 3. For each response node: call traceValues()
629
+ * 4. Format and display results grouped by response call
630
+ *
631
+ * @param backend - Graph backend
632
+ * @param pattern - Route pattern (e.g., "GET /status" or "/status")
633
+ * @param projectPath - Project root path
634
+ * @param jsonOutput - Whether to output as JSON
635
+ * @param maxDepth - Maximum trace depth (default 10)
636
+ */
637
+ async function handleRouteTrace(backend, pattern, projectPath, jsonOutput, maxDepth = 10) {
638
+ // Find route
639
+ const route = await findRouteByPattern(backend, pattern);
640
+ if (!route) {
641
+ console.log(`Route not found: ${pattern}`);
642
+ console.log('');
643
+ console.log('Hint: Use "grafema query" to list available routes');
644
+ return;
645
+ }
646
+ // Get RESPONDS_WITH edges
647
+ const respondsWithEdges = await backend.getOutgoingEdges(route.id, ['RESPONDS_WITH']);
648
+ if (respondsWithEdges.length === 0) {
649
+ if (jsonOutput) {
650
+ console.log(JSON.stringify({
651
+ route: {
652
+ name: route.name,
653
+ file: route.file,
654
+ line: route.line
655
+ },
656
+ responses: [],
657
+ message: 'No response data found'
658
+ }, null, 2));
659
+ }
660
+ else {
661
+ console.log(`Route: ${route.name} (${route.file}:${route.line || '?'})`);
662
+ console.log('');
663
+ console.log('No response data found for this route.');
664
+ console.log('');
665
+ console.log('Hint: Make sure ExpressResponseAnalyzer is in your config.');
666
+ }
667
+ return;
668
+ }
669
+ // Build response data
670
+ const responses = [];
671
+ // Trace each response
672
+ for (let i = 0; i < respondsWithEdges.length; i++) {
673
+ const edge = respondsWithEdges[i];
674
+ const responseNode = await backend.getNode(edge.dst);
675
+ if (!responseNode)
676
+ continue;
677
+ const responseMethod = edge.metadata?.responseMethod || 'unknown';
678
+ // Trace values from this response node
679
+ const traced = await traceValues(backend, responseNode.id, {
680
+ maxDepth,
681
+ followDerivesFrom: true,
682
+ detectNondeterministic: true
683
+ });
684
+ // Format traced values
685
+ const sources = await Promise.all(traced.map(async (t) => {
686
+ const relativePath = t.source.file.startsWith(projectPath)
687
+ ? t.source.file.substring(projectPath.length + 1)
688
+ : t.source.file;
689
+ if (t.isUnknown) {
690
+ return {
691
+ type: 'UNKNOWN',
692
+ reason: t.reason || 'runtime input',
693
+ file: relativePath,
694
+ line: t.source.line,
695
+ id: t.source.id
696
+ };
697
+ }
698
+ else if (t.value !== undefined) {
699
+ return {
700
+ type: 'LITERAL',
701
+ value: t.value,
702
+ file: relativePath,
703
+ line: t.source.line,
704
+ id: t.source.id
705
+ };
706
+ }
707
+ else {
708
+ // Look up node to get type and name
709
+ const sourceNode = await backend.getNode(t.source.id);
710
+ return {
711
+ type: sourceNode?.type || 'VALUE',
712
+ name: sourceNode?.name || '<unnamed>',
713
+ file: relativePath,
714
+ line: t.source.line,
715
+ id: t.source.id
716
+ };
717
+ }
718
+ }));
719
+ responses.push({
720
+ index: i + 1,
721
+ method: responseMethod,
722
+ line: responseNode.line || 0,
723
+ sources: sources.length > 0 ? sources : []
724
+ });
725
+ if (!jsonOutput) {
726
+ // Display human-readable output
727
+ console.log(`Response ${i + 1} (res.${responseMethod} at line ${responseNode.line || '?'}):`);
728
+ if (sources.length === 0) {
729
+ console.log(' No data sources found (response may be external or complex)');
730
+ }
731
+ else {
732
+ console.log(' Data sources:');
733
+ for (const src of sources) {
734
+ if (src.type === 'UNKNOWN') {
735
+ console.log(` [UNKNOWN] ${src.reason} at ${src.file}:${src.line}`);
736
+ }
737
+ else if (src.type === 'LITERAL') {
738
+ console.log(` [LITERAL] ${JSON.stringify(src.value)} at ${src.file}:${src.line}`);
739
+ }
740
+ else {
741
+ console.log(` [${src.type}] ${src.name} at ${src.file}:${src.line}`);
742
+ }
743
+ }
744
+ }
745
+ console.log('');
746
+ }
747
+ }
748
+ // Output results
749
+ if (jsonOutput) {
750
+ console.log(JSON.stringify({
751
+ route: {
752
+ name: route.name,
753
+ file: route.file,
754
+ line: route.line
755
+ },
756
+ responses
757
+ }, null, 2));
758
+ }
759
+ else {
760
+ // Human-readable output header
761
+ if (responses.length > 0 && !jsonOutput) {
762
+ // Already printed above, just for clarity
763
+ }
764
+ }
765
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Types command - List all node types in the graph
3
+ *
4
+ * Shows all node types present in the analyzed codebase with counts.
5
+ * Useful for:
6
+ * - Discovering what types exist (standard and custom)
7
+ * - Understanding graph composition
8
+ * - Finding types to use with --type flag
9
+ */
10
+ import { Command } from 'commander';
11
+ export declare const typesCommand: Command;
12
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/commands/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,eAAO,MAAM,YAAY,SAuErB,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Types command - List all node types in the graph
3
+ *
4
+ * Shows all node types present in the analyzed codebase with counts.
5
+ * Useful for:
6
+ * - Discovering what types exist (standard and custom)
7
+ * - Understanding graph composition
8
+ * - Finding types to use with --type flag
9
+ */
10
+ import { Command } from 'commander';
11
+ import { resolve, join } from 'path';
12
+ import { existsSync } from 'fs';
13
+ import { RFDBServerBackend } from '@grafema/core';
14
+ import { exitWithError } from '../utils/errorFormatter.js';
15
+ export const typesCommand = new Command('types')
16
+ .description('List all node types in the graph')
17
+ .option('-p, --project <path>', 'Project path', '.')
18
+ .option('-j, --json', 'Output as JSON')
19
+ .option('-s, --sort <by>', 'Sort by: count (default) or name', 'count')
20
+ .addHelpText('after', `
21
+ Examples:
22
+ grafema types List all node types with counts
23
+ grafema types --json Output as JSON for scripting
24
+ grafema types --sort name Sort alphabetically by type name
25
+ grafema types -s count Sort by count (default, descending)
26
+
27
+ Use with query --type:
28
+ grafema types # See available types
29
+ grafema query --type jsx:component "Button" # Query specific type
30
+ `)
31
+ .action(async (options) => {
32
+ const projectPath = resolve(options.project);
33
+ const grafemaDir = join(projectPath, '.grafema');
34
+ const dbPath = join(grafemaDir, 'graph.rfdb');
35
+ if (!existsSync(dbPath)) {
36
+ exitWithError('No graph database found', ['Run: grafema analyze']);
37
+ }
38
+ const backend = new RFDBServerBackend({ dbPath });
39
+ await backend.connect();
40
+ try {
41
+ const nodeCounts = await backend.countNodesByType();
42
+ const entries = Object.entries(nodeCounts);
43
+ if (entries.length === 0) {
44
+ console.log('No nodes in graph. Run: grafema analyze');
45
+ return;
46
+ }
47
+ // Sort entries
48
+ const sortedEntries = options.sort === 'name'
49
+ ? entries.sort((a, b) => a[0].localeCompare(b[0]))
50
+ : entries.sort((a, b) => b[1] - a[1]); // count descending
51
+ if (options.json) {
52
+ const result = {
53
+ types: sortedEntries.map(([type, count]) => ({ type, count })),
54
+ totalTypes: sortedEntries.length,
55
+ totalNodes: sortedEntries.reduce((sum, [, count]) => sum + count, 0),
56
+ };
57
+ console.log(JSON.stringify(result, null, 2));
58
+ }
59
+ else {
60
+ console.log('Node Types in Graph:');
61
+ console.log('');
62
+ // Calculate max type length for alignment
63
+ const maxTypeLen = Math.max(...sortedEntries.map(([type]) => type.length));
64
+ for (const [type, count] of sortedEntries) {
65
+ const paddedType = type.padEnd(maxTypeLen);
66
+ const formattedCount = count.toLocaleString();
67
+ console.log(` ${paddedType} ${formattedCount}`);
68
+ }
69
+ console.log('');
70
+ const totalNodes = sortedEntries.reduce((sum, [, count]) => sum + count, 0);
71
+ console.log(`Total: ${sortedEntries.length} types, ${totalNodes.toLocaleString()} nodes`);
72
+ console.log('');
73
+ console.log('Tip: Use grafema query --type <type> "pattern" to search within a type');
74
+ }
75
+ }
76
+ finally {
77
+ await backend.close();
78
+ }
79
+ });
@@ -29,7 +29,20 @@ export interface DisplayableNode {
29
29
  file: string;
30
30
  /** Line number (optional) */
31
31
  line?: number;
32
+ /** HTTP method (for http:route, http:request) */
33
+ method?: string;
34
+ /** Path or URL (for http:route, http:request) */
35
+ path?: string;
36
+ /** URL (for http:request) */
37
+ url?: string;
32
38
  }
39
+ /**
40
+ * Get the display name for a node based on its type.
41
+ *
42
+ * HTTP nodes use method + path/url instead of name.
43
+ * Other nodes use their name field.
44
+ */
45
+ export declare function getNodeDisplayName(node: DisplayableNode): string;
33
46
  /**
34
47
  * Format a node for primary display (multi-line)
35
48
  *