@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
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for `grafema doctor` command - REG-214
3
+ */
4
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Doctor command - Diagnose Grafema setup issues
3
+ *
4
+ * Checks (in order):
5
+ * 1. Initialization (.grafema directory, config file)
6
+ * 2. Config validity (syntax, plugin names)
7
+ * 3. Entrypoints (service paths exist)
8
+ * 4. Server status (RFDB server running)
9
+ * 5. Database exists and has data
10
+ * 6. Graph statistics
11
+ * 7. Graph connectivity
12
+ * 8. Graph freshness
13
+ * 9. Version information
14
+ */
15
+ import { Command } from 'commander';
16
+ export declare const doctorCommand: Command;
17
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,eAAO,MAAM,aAAa,SA6DtB,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Doctor command - Diagnose Grafema setup issues
3
+ *
4
+ * Checks (in order):
5
+ * 1. Initialization (.grafema directory, config file)
6
+ * 2. Config validity (syntax, plugin names)
7
+ * 3. Entrypoints (service paths exist)
8
+ * 4. Server status (RFDB server running)
9
+ * 5. Database exists and has data
10
+ * 6. Graph statistics
11
+ * 7. Graph connectivity
12
+ * 8. Graph freshness
13
+ * 9. Version information
14
+ */
15
+ import { Command } from 'commander';
16
+ import { resolve } from 'path';
17
+ import { checkGrafemaInitialized, checkServerStatus, checkConfigValidity, checkEntrypoints, checkDatabaseExists, checkGraphStats, checkConnectivity, checkFreshness, checkVersions, } from './doctor/checks.js';
18
+ import { formatReport, buildJsonReport } from './doctor/output.js';
19
+ export const doctorCommand = new Command('doctor')
20
+ .description('Diagnose Grafema setup issues')
21
+ .option('-p, --project <path>', 'Project path', '.')
22
+ .option('-j, --json', 'Output as JSON')
23
+ .option('-q, --quiet', 'Only show failures')
24
+ .option('-v, --verbose', 'Show detailed diagnostics')
25
+ .addHelpText('after', `
26
+ Examples:
27
+ grafema doctor Run all diagnostic checks
28
+ grafema doctor --verbose Show detailed diagnostics
29
+ grafema doctor --quiet Only show failures
30
+ grafema doctor --json Output diagnostics as JSON
31
+ `)
32
+ .action(async (options) => {
33
+ const projectPath = resolve(options.project);
34
+ const checks = [];
35
+ // Level 1: Prerequisites (fail-fast)
36
+ const initCheck = await checkGrafemaInitialized(projectPath);
37
+ checks.push(initCheck);
38
+ if (initCheck.status === 'fail') {
39
+ // Can't continue without initialization
40
+ outputResults(checks, projectPath, options);
41
+ process.exit(1);
42
+ }
43
+ // Level 2: Configuration
44
+ checks.push(await checkConfigValidity(projectPath));
45
+ checks.push(await checkEntrypoints(projectPath));
46
+ // Server status (needed for Level 3 checks)
47
+ const serverCheck = await checkServerStatus(projectPath);
48
+ checks.push(serverCheck);
49
+ // Level 3: Graph Health (requires database and optionally server)
50
+ checks.push(await checkDatabaseExists(projectPath));
51
+ if (serverCheck.status === 'pass') {
52
+ // Server is running - can do full health checks
53
+ checks.push(await checkGraphStats(projectPath));
54
+ checks.push(await checkConnectivity(projectPath));
55
+ checks.push(await checkFreshness(projectPath));
56
+ }
57
+ // Level 4: Informational
58
+ checks.push(await checkVersions(projectPath));
59
+ // Output results
60
+ outputResults(checks, projectPath, options);
61
+ // Exit code
62
+ const failCount = checks.filter(c => c.status === 'fail').length;
63
+ const warnCount = checks.filter(c => c.status === 'warn').length;
64
+ if (failCount > 0) {
65
+ process.exit(1); // Critical issues
66
+ }
67
+ else if (warnCount > 0) {
68
+ process.exit(2); // Warnings only
69
+ }
70
+ // Exit 0 for all pass
71
+ });
72
+ function outputResults(checks, projectPath, options) {
73
+ if (options.json) {
74
+ const report = buildJsonReport(checks, projectPath);
75
+ console.log(JSON.stringify(report, null, 2));
76
+ }
77
+ else {
78
+ console.log(formatReport(checks, options));
79
+ }
80
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Explain command - Show what nodes exist in a file
3
+ *
4
+ * Purpose: Help users discover what nodes exist in the graph for a file,
5
+ * displaying semantic IDs so users can query them.
6
+ *
7
+ * Use cases:
8
+ * - User can't find a variable they expect to be in the graph
9
+ * - User wants to understand what's been analyzed for a file
10
+ * - User needs semantic IDs to construct queries
11
+ *
12
+ * @see _tasks/REG-177/006-don-revised-plan.md
13
+ */
14
+ import { Command } from 'commander';
15
+ export declare const explainCommand: Command;
16
+ //# sourceMappingURL=explain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,cAAc,SA+GvB,CAAC"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Explain command - Show what nodes exist in a file
3
+ *
4
+ * Purpose: Help users discover what nodes exist in the graph for a file,
5
+ * displaying semantic IDs so users can query them.
6
+ *
7
+ * Use cases:
8
+ * - User can't find a variable they expect to be in the graph
9
+ * - User wants to understand what's been analyzed for a file
10
+ * - User needs semantic IDs to construct queries
11
+ *
12
+ * @see _tasks/REG-177/006-don-revised-plan.md
13
+ */
14
+ import { Command } from 'commander';
15
+ import { resolve, join, relative, normalize } from 'path';
16
+ import { existsSync, realpathSync } from 'fs';
17
+ import { RFDBServerBackend, FileExplainer } from '@grafema/core';
18
+ import { exitWithError } from '../utils/errorFormatter.js';
19
+ export const explainCommand = new Command('explain')
20
+ .description('Show what nodes exist in a file')
21
+ .argument('<file>', 'File path to explain')
22
+ .option('-p, --project <path>', 'Project path', '.')
23
+ .option('-j, --json', 'Output as JSON')
24
+ .addHelpText('after', `
25
+ Examples:
26
+ grafema explain src/app.ts Show all nodes in src/app.ts
27
+ grafema explain src/app.ts --json Output as JSON for scripting
28
+ grafema explain ./src/utils.js Works with relative paths
29
+
30
+ This command helps you:
31
+ 1. Discover what nodes exist in the graph for a file
32
+ 2. Find semantic IDs to use in queries
33
+ 3. Understand scope context (try/catch, conditionals, etc.)
34
+
35
+ If a file shows NOT_ANALYZED:
36
+ - Run: grafema analyze
37
+ - Check if file is excluded in config
38
+ `)
39
+ .action(async (file, options) => {
40
+ const projectPath = resolve(options.project);
41
+ const grafemaDir = join(projectPath, '.grafema');
42
+ const dbPath = join(grafemaDir, 'graph.rfdb');
43
+ // Check database exists
44
+ if (!existsSync(dbPath)) {
45
+ exitWithError('No graph database found', [
46
+ 'Run: grafema init && grafema analyze',
47
+ ]);
48
+ }
49
+ // Normalize and resolve file path
50
+ let filePath = file;
51
+ // Handle relative paths - convert to relative from project root
52
+ if (file.startsWith('./') || file.startsWith('../')) {
53
+ filePath = normalize(file).replace(/^\.\//, '');
54
+ }
55
+ else if (resolve(file) === file) {
56
+ // Absolute path - convert to relative
57
+ filePath = relative(projectPath, file);
58
+ }
59
+ // Resolve to absolute path for graph lookup
60
+ const resolvedPath = resolve(projectPath, filePath);
61
+ if (!existsSync(resolvedPath)) {
62
+ exitWithError(`File not found: ${file}`, [
63
+ 'Check the file path and try again',
64
+ ]);
65
+ }
66
+ // Use realpath to match how graph stores paths (handles symlinks like /tmp -> /private/tmp on macOS)
67
+ const absoluteFilePath = realpathSync(resolvedPath);
68
+ // Keep relative path for display
69
+ const relativeFilePath = relative(projectPath, absoluteFilePath);
70
+ const backend = new RFDBServerBackend({ dbPath });
71
+ await backend.connect();
72
+ try {
73
+ const explainer = new FileExplainer(backend);
74
+ // Query with absolute path since graph stores absolute paths
75
+ const result = await explainer.explain(absoluteFilePath);
76
+ // Override file in result for display purposes (show relative path)
77
+ result.file = relativeFilePath;
78
+ if (options.json) {
79
+ console.log(JSON.stringify(result, null, 2));
80
+ return;
81
+ }
82
+ // Human-readable output
83
+ console.log(`File: ${result.file}`);
84
+ console.log(`Status: ${result.status}`);
85
+ console.log('');
86
+ if (result.status === 'NOT_ANALYZED') {
87
+ console.log('This file has not been analyzed yet.');
88
+ console.log('');
89
+ console.log('To analyze:');
90
+ console.log(' grafema analyze');
91
+ return;
92
+ }
93
+ console.log(`Nodes in graph: ${result.totalCount}`);
94
+ console.log('');
95
+ // Group nodes by type for display
96
+ const nodesByType = groupNodesByType(result.nodes);
97
+ for (const [type, nodes] of Object.entries(nodesByType)) {
98
+ for (const node of nodes) {
99
+ displayNode(node, type, projectPath);
100
+ console.log('');
101
+ }
102
+ }
103
+ // Show summary by type
104
+ console.log('Summary:');
105
+ for (const [type, count] of Object.entries(result.byType).sort()) {
106
+ console.log(` ${type}: ${count}`);
107
+ }
108
+ console.log('');
109
+ console.log('To query a specific node by ID:');
110
+ console.log(' grafema query --raw \'attr(X, "id", "<semantic-id>")\'');
111
+ }
112
+ finally {
113
+ await backend.close();
114
+ }
115
+ });
116
+ /**
117
+ * Group nodes by type for organized display
118
+ */
119
+ function groupNodesByType(nodes) {
120
+ const grouped = {};
121
+ for (const node of nodes) {
122
+ const type = node.type || 'UNKNOWN';
123
+ if (!grouped[type]) {
124
+ grouped[type] = [];
125
+ }
126
+ grouped[type].push(node);
127
+ }
128
+ return grouped;
129
+ }
130
+ /**
131
+ * Display a single node in human-readable format
132
+ */
133
+ function displayNode(node, type, projectPath) {
134
+ // Line 1: [TYPE] name (context)
135
+ const contextSuffix = node.context ? ` (${node.context})` : '';
136
+ console.log(`[${type}] ${node.name || '<anonymous>'}${contextSuffix}`);
137
+ // Line 2: ID (semantic ID for querying)
138
+ console.log(` ID: ${node.id}`);
139
+ // Line 3: Location
140
+ if (node.file) {
141
+ const relPath = relative(projectPath, node.file);
142
+ const loc = node.line ? `${relPath}:${node.line}` : relPath;
143
+ console.log(` Location: ${loc}`);
144
+ }
145
+ }
@@ -1,5 +1,11 @@
1
1
  /**
2
- * Explore command - Interactive TUI for graph navigation
2
+ * Explore command - Interactive TUI or batch mode for graph navigation
3
+ *
4
+ * Interactive mode: grafema explore [start]
5
+ * Batch mode:
6
+ * grafema explore --query "functionName"
7
+ * grafema explore --callers "functionName"
8
+ * grafema explore --callees "functionName"
3
9
  */
4
10
  import { Command } from 'commander';
5
11
  export declare const exploreCommand: Command;
@@ -1 +1 @@
1
- {"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../src/commands/explore.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2/BpC,eAAO,MAAM,cAAc,SAgCvB,CAAC"}
1
+ {"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../src/commands/explore.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAomCpC,eAAO,MAAM,cAAc,SAuEvB,CAAC"}
@@ -1,6 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Explore command - Interactive TUI for graph navigation
3
+ * Explore command - Interactive TUI or batch mode for graph navigation
4
+ *
5
+ * Interactive mode: grafema explore [start]
6
+ * Batch mode:
7
+ * grafema explore --query "functionName"
8
+ * grafema explore --callers "functionName"
9
+ * grafema explore --callees "functionName"
4
10
  */
5
11
  import { Command } from 'commander';
6
12
  import { resolve, join, relative } from 'path';
@@ -8,7 +14,7 @@ import { existsSync } from 'fs';
8
14
  import { execSync } from 'child_process';
9
15
  import { useState, useEffect } from 'react';
10
16
  import { render, Box, Text, useInput, useApp } from 'ink';
11
- import { RFDBServerBackend } from '@grafema/core';
17
+ import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore, findCallsInFunction as findCallsInFunctionCore } from '@grafema/core';
12
18
  import { getCodePreview, formatCodePreview } from '../utils/codePreview.js';
13
19
  import { exitWithError } from '../utils/errorFormatter.js';
14
20
  // Main Explorer Component
@@ -396,10 +402,16 @@ async function getCallers(backend, nodeId, limit) {
396
402
  const callNode = await backend.getNode(edge.src);
397
403
  if (!callNode)
398
404
  continue;
399
- const containingFunc = await findContainingFunction(backend, callNode.id);
405
+ const containingFunc = await findContainingFunctionCore(backend, callNode.id);
400
406
  if (containingFunc && !seen.has(containingFunc.id)) {
401
407
  seen.add(containingFunc.id);
402
- callers.push(containingFunc);
408
+ callers.push({
409
+ id: containingFunc.id,
410
+ type: containingFunc.type,
411
+ name: containingFunc.name,
412
+ file: containingFunc.file || '',
413
+ line: containingFunc.line,
414
+ });
403
415
  }
404
416
  }
405
417
  }
@@ -412,19 +424,21 @@ async function getCallees(backend, nodeId, limit) {
412
424
  const callees = [];
413
425
  const seen = new Set();
414
426
  try {
415
- const callNodes = await findCallsInFunction(backend, nodeId);
416
- for (const callNode of callNodes) {
427
+ // Use shared utility from @grafema/core
428
+ const calls = await findCallsInFunctionCore(backend, nodeId);
429
+ for (const call of calls) {
417
430
  if (callees.length >= limit)
418
431
  break;
419
- const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
420
- for (const edge of callEdges) {
421
- if (callees.length >= limit)
422
- break;
423
- const targetNode = await backend.getNode(edge.dst);
424
- if (!targetNode || seen.has(targetNode.id))
425
- continue;
426
- seen.add(targetNode.id);
427
- callees.push(extractNodeInfo(targetNode));
432
+ // Only include resolved calls with targets
433
+ if (call.resolved && call.target && !seen.has(call.target.id)) {
434
+ seen.add(call.target.id);
435
+ callees.push({
436
+ id: call.target.id,
437
+ type: 'FUNCTION',
438
+ name: call.target.name || '',
439
+ file: call.target.file || '',
440
+ line: call.target.line,
441
+ });
428
442
  }
429
443
  }
430
444
  }
@@ -433,72 +447,6 @@ async function getCallees(backend, nodeId, limit) {
433
447
  }
434
448
  return callees;
435
449
  }
436
- async function findContainingFunction(backend, nodeId) {
437
- const visited = new Set();
438
- const queue = [{ id: nodeId, depth: 0 }];
439
- while (queue.length > 0) {
440
- const { id, depth } = queue.shift();
441
- if (visited.has(id) || depth > 15)
442
- continue;
443
- visited.add(id);
444
- try {
445
- const edges = await backend.getIncomingEdges(id, null);
446
- for (const edge of edges) {
447
- const edgeType = edge.edgeType || edge.type;
448
- if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
449
- continue;
450
- const parent = await backend.getNode(edge.src);
451
- if (!parent || visited.has(parent.id))
452
- continue;
453
- const parentType = parent.type || parent.nodeType;
454
- if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
455
- return extractNodeInfo(parent);
456
- }
457
- queue.push({ id: parent.id, depth: depth + 1 });
458
- }
459
- }
460
- catch {
461
- // Ignore
462
- }
463
- }
464
- return null;
465
- }
466
- async function findCallsInFunction(backend, nodeId) {
467
- const calls = [];
468
- const visited = new Set();
469
- const queue = [{ id: nodeId, depth: 0 }];
470
- while (queue.length > 0) {
471
- const { id, depth } = queue.shift();
472
- if (visited.has(id) || depth > 10)
473
- continue;
474
- visited.add(id);
475
- try {
476
- const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
477
- for (const edge of edges) {
478
- const child = await backend.getNode(edge.dst);
479
- if (!child)
480
- continue;
481
- const childType = child.type || child.nodeType;
482
- if (childType === 'CALL') {
483
- calls.push({
484
- id: child.id,
485
- type: 'CALL',
486
- name: child.name || '',
487
- file: child.file || '',
488
- line: child.line,
489
- });
490
- }
491
- if (childType !== 'FUNCTION' && childType !== 'CLASS') {
492
- queue.push({ id: child.id, depth: depth + 1 });
493
- }
494
- }
495
- }
496
- catch {
497
- // Ignore
498
- }
499
- }
500
- return calls;
501
- }
502
450
  async function searchNode(backend, query) {
503
451
  const results = await searchNodes(backend, query, 1);
504
452
  return results[0] || null;
@@ -608,21 +556,192 @@ async function findStartNode(backend, startName) {
608
556
  }
609
557
  return bestNode;
610
558
  }
611
- // Command
559
+ // =============================================================================
560
+ // Batch Mode Implementation
561
+ // =============================================================================
562
+ /**
563
+ * Run explore in batch mode - for AI agents, CI, and scripts
564
+ */
565
+ async function runBatchExplore(backend, options, projectPath) {
566
+ const depth = parseInt(options.depth || '3', 10) || 3;
567
+ const useJson = options.json || options.format === 'json' || options.format !== 'text';
568
+ try {
569
+ if (options.query) {
570
+ // Search mode
571
+ const results = await searchNodes(backend, options.query, 20);
572
+ outputResults(results, 'search', useJson, projectPath);
573
+ }
574
+ else if (options.callers) {
575
+ // Callers mode
576
+ const target = await searchNode(backend, options.callers);
577
+ if (!target) {
578
+ exitWithError(`Function "${options.callers}" not found`, [
579
+ 'Try: grafema query "partial-name"',
580
+ ]);
581
+ }
582
+ const callers = await getCallersRecursive(backend, target.id, depth);
583
+ outputResults(callers, 'callers', useJson, projectPath, target);
584
+ }
585
+ else if (options.callees) {
586
+ // Callees mode
587
+ const target = await searchNode(backend, options.callees);
588
+ if (!target) {
589
+ exitWithError(`Function "${options.callees}" not found`, [
590
+ 'Try: grafema query "partial-name"',
591
+ ]);
592
+ }
593
+ const callees = await getCalleesRecursive(backend, target.id, depth);
594
+ outputResults(callees, 'callees', useJson, projectPath, target);
595
+ }
596
+ }
597
+ catch (err) {
598
+ exitWithError(`Explore failed: ${err.message}`);
599
+ }
600
+ }
601
+ /**
602
+ * Output results in JSON or text format
603
+ */
604
+ function outputResults(nodes, mode, useJson, projectPath, target) {
605
+ if (useJson) {
606
+ const output = {
607
+ mode,
608
+ target: target ? formatNodeForJson(target, projectPath) : undefined,
609
+ count: nodes.length,
610
+ results: nodes.map(n => formatNodeForJson(n, projectPath)),
611
+ };
612
+ console.log(JSON.stringify(output, null, 2));
613
+ }
614
+ else {
615
+ // Text format
616
+ if (target) {
617
+ console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
618
+ console.log(`File: ${relative(projectPath, target.file)}${target.line ? `:${target.line}` : ''}`);
619
+ console.log('');
620
+ }
621
+ if (nodes.length === 0) {
622
+ console.log(` (no ${mode} found)`);
623
+ }
624
+ else {
625
+ for (const node of nodes) {
626
+ const loc = relative(projectPath, node.file);
627
+ console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
628
+ }
629
+ }
630
+ console.log('');
631
+ console.log(`Total: ${nodes.length}`);
632
+ }
633
+ }
634
+ function formatNodeForJson(node, projectPath) {
635
+ return {
636
+ id: node.id,
637
+ type: node.type,
638
+ name: node.name,
639
+ file: relative(projectPath, node.file),
640
+ line: node.line,
641
+ async: node.async,
642
+ exported: node.exported,
643
+ };
644
+ }
645
+ /**
646
+ * Get callers recursively up to specified depth
647
+ */
648
+ async function getCallersRecursive(backend, nodeId, maxDepth) {
649
+ const results = [];
650
+ const visited = new Set();
651
+ const queue = [{ id: nodeId, depth: 0 }];
652
+ while (queue.length > 0) {
653
+ const { id, depth } = queue.shift();
654
+ if (visited.has(id) || depth > maxDepth)
655
+ continue;
656
+ visited.add(id);
657
+ const callers = await getCallers(backend, id, 50);
658
+ for (const caller of callers) {
659
+ if (!visited.has(caller.id)) {
660
+ results.push(caller);
661
+ if (depth < maxDepth) {
662
+ queue.push({ id: caller.id, depth: depth + 1 });
663
+ }
664
+ }
665
+ }
666
+ }
667
+ return results;
668
+ }
669
+ /**
670
+ * Get callees recursively up to specified depth
671
+ */
672
+ async function getCalleesRecursive(backend, nodeId, maxDepth) {
673
+ const results = [];
674
+ const visited = new Set();
675
+ const queue = [{ id: nodeId, depth: 0 }];
676
+ while (queue.length > 0) {
677
+ const { id, depth } = queue.shift();
678
+ if (visited.has(id) || depth > maxDepth)
679
+ continue;
680
+ visited.add(id);
681
+ const callees = await getCallees(backend, id, 50);
682
+ for (const callee of callees) {
683
+ if (!visited.has(callee.id)) {
684
+ results.push(callee);
685
+ if (depth < maxDepth) {
686
+ queue.push({ id: callee.id, depth: depth + 1 });
687
+ }
688
+ }
689
+ }
690
+ }
691
+ return results;
692
+ }
693
+ // =============================================================================
694
+ // Command Definition
695
+ // =============================================================================
612
696
  export const exploreCommand = new Command('explore')
613
- .description('Interactive graph navigation')
614
- .argument('[start]', 'Starting function name')
697
+ .description('Interactive graph navigation (TUI) or batch query mode')
698
+ .argument('[start]', 'Starting function name (for interactive mode)')
615
699
  .option('-p, --project <path>', 'Project path', '.')
700
+ .option('-q, --query <name>', 'Batch: search for nodes by name')
701
+ .option('--callers <name>', 'Batch: show callers of function')
702
+ .option('--callees <name>', 'Batch: show callees of function')
703
+ .option('-d, --depth <n>', 'Batch: traversal depth', '3')
704
+ .option('-j, --json', 'Output as JSON (default for batch mode)')
705
+ .option('--format <type>', 'Output format: json or text')
706
+ .addHelpText('after', `
707
+ Examples:
708
+ grafema explore Interactive TUI mode
709
+ grafema explore "authenticate" Start TUI at specific function
710
+ grafema explore --query "User" Batch: search for nodes
711
+ grafema explore --callers "login" Batch: show who calls login
712
+ grafema explore --callees "main" Batch: show what main calls
713
+ grafema explore --callers "auth" -d 5 Batch: callers with depth 5
714
+ grafema explore --query "api" --format text Batch: text output
715
+ `)
616
716
  .action(async (start, options) => {
617
717
  const projectPath = resolve(options.project);
618
718
  const grafemaDir = join(projectPath, '.grafema');
619
719
  const dbPath = join(grafemaDir, 'graph.rfdb');
620
720
  if (!existsSync(dbPath)) {
621
- exitWithError('No graph database found', ['Run: grafema analyze']);
721
+ exitWithError('No database found', [
722
+ 'Run: grafema analyze',
723
+ ]);
622
724
  }
623
725
  const backend = new RFDBServerBackend({ dbPath });
624
726
  try {
625
727
  await backend.connect();
728
+ // Detect batch mode
729
+ const isBatchMode = !!(options.query || options.callers || options.callees);
730
+ if (isBatchMode) {
731
+ await runBatchExplore(backend, options, projectPath);
732
+ return;
733
+ }
734
+ // Interactive mode - check TTY
735
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
736
+ if (!isTTY) {
737
+ exitWithError('Interactive mode requires a terminal', [
738
+ 'Batch mode: grafema explore --query "functionName"',
739
+ 'Batch mode: grafema explore --callers "functionName"',
740
+ 'Batch mode: grafema explore --callees "functionName"',
741
+ 'Alternative: grafema query "functionName"',
742
+ 'Alternative: grafema impact "functionName"',
743
+ ]);
744
+ }
626
745
  const startNode = await findStartNode(backend, start || null);
627
746
  const { waitUntilExit } = render(_jsx(Explorer, { backend: backend, startNode: startNode, projectPath: projectPath }));
628
747
  await waitUntilExit();
@@ -1 +1 @@
1
- {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../src/commands/get.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkCpC,eAAO,MAAM,UAAU,SAyCnB,CAAC"}
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../src/commands/get.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoCpC,eAAO,MAAM,UAAU,SAgDnB,CAAC"}