@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/explore.tsx
CHANGED
|
@@ -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
|
|
|
5
11
|
import { Command } from 'commander';
|
|
@@ -8,11 +14,22 @@ import { existsSync } from 'fs';
|
|
|
8
14
|
import { execSync } from 'child_process';
|
|
9
15
|
import React, { 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
|
|
|
15
21
|
// Types
|
|
22
|
+
interface ExploreOptions {
|
|
23
|
+
project: string;
|
|
24
|
+
// Batch mode flags
|
|
25
|
+
query?: string;
|
|
26
|
+
callers?: string;
|
|
27
|
+
callees?: string;
|
|
28
|
+
depth?: string;
|
|
29
|
+
json?: boolean;
|
|
30
|
+
format?: 'json' | 'text';
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
interface NodeInfo {
|
|
17
34
|
id: string;
|
|
18
35
|
type: string;
|
|
@@ -769,11 +786,17 @@ async function getCallers(backend: RFDBServerBackend, nodeId: string, limit: num
|
|
|
769
786
|
const callNode = await backend.getNode(edge.src);
|
|
770
787
|
if (!callNode) continue;
|
|
771
788
|
|
|
772
|
-
const containingFunc = await
|
|
789
|
+
const containingFunc = await findContainingFunctionCore(backend, callNode.id);
|
|
773
790
|
|
|
774
791
|
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
775
792
|
seen.add(containingFunc.id);
|
|
776
|
-
callers.push(
|
|
793
|
+
callers.push({
|
|
794
|
+
id: containingFunc.id,
|
|
795
|
+
type: containingFunc.type,
|
|
796
|
+
name: containingFunc.name,
|
|
797
|
+
file: containingFunc.file || '',
|
|
798
|
+
line: containingFunc.line,
|
|
799
|
+
});
|
|
777
800
|
}
|
|
778
801
|
}
|
|
779
802
|
} catch {
|
|
@@ -788,21 +811,22 @@ async function getCallees(backend: RFDBServerBackend, nodeId: string, limit: num
|
|
|
788
811
|
const seen = new Set<string>();
|
|
789
812
|
|
|
790
813
|
try {
|
|
791
|
-
|
|
814
|
+
// Use shared utility from @grafema/core
|
|
815
|
+
const calls = await findCallsInFunctionCore(backend, nodeId);
|
|
792
816
|
|
|
793
|
-
for (const
|
|
817
|
+
for (const call of calls) {
|
|
794
818
|
if (callees.length >= limit) break;
|
|
795
819
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
820
|
+
// Only include resolved calls with targets
|
|
821
|
+
if (call.resolved && call.target && !seen.has(call.target.id)) {
|
|
822
|
+
seen.add(call.target.id);
|
|
823
|
+
callees.push({
|
|
824
|
+
id: call.target.id,
|
|
825
|
+
type: 'FUNCTION',
|
|
826
|
+
name: call.target.name || '',
|
|
827
|
+
file: call.target.file || '',
|
|
828
|
+
line: call.target.line,
|
|
829
|
+
});
|
|
806
830
|
}
|
|
807
831
|
}
|
|
808
832
|
} catch {
|
|
@@ -812,82 +836,6 @@ async function getCallees(backend: RFDBServerBackend, nodeId: string, limit: num
|
|
|
812
836
|
return callees;
|
|
813
837
|
}
|
|
814
838
|
|
|
815
|
-
async function findContainingFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo | null> {
|
|
816
|
-
const visited = new Set<string>();
|
|
817
|
-
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
818
|
-
|
|
819
|
-
while (queue.length > 0) {
|
|
820
|
-
const { id, depth } = queue.shift()!;
|
|
821
|
-
if (visited.has(id) || depth > 15) continue;
|
|
822
|
-
visited.add(id);
|
|
823
|
-
|
|
824
|
-
try {
|
|
825
|
-
const edges = await backend.getIncomingEdges(id, null);
|
|
826
|
-
|
|
827
|
-
for (const edge of edges) {
|
|
828
|
-
const edgeType = (edge as any).edgeType || (edge as any).type;
|
|
829
|
-
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
|
|
830
|
-
|
|
831
|
-
const parent = await backend.getNode(edge.src);
|
|
832
|
-
if (!parent || visited.has(parent.id)) continue;
|
|
833
|
-
|
|
834
|
-
const parentType = (parent as any).type || (parent as any).nodeType;
|
|
835
|
-
|
|
836
|
-
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
837
|
-
return extractNodeInfo(parent);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
queue.push({ id: parent.id, depth: depth + 1 });
|
|
841
|
-
}
|
|
842
|
-
} catch {
|
|
843
|
-
// Ignore
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
async function findCallsInFunction(backend: RFDBServerBackend, nodeId: string): Promise<NodeInfo[]> {
|
|
851
|
-
const calls: NodeInfo[] = [];
|
|
852
|
-
const visited = new Set<string>();
|
|
853
|
-
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
854
|
-
|
|
855
|
-
while (queue.length > 0) {
|
|
856
|
-
const { id, depth } = queue.shift()!;
|
|
857
|
-
if (visited.has(id) || depth > 10) continue;
|
|
858
|
-
visited.add(id);
|
|
859
|
-
|
|
860
|
-
try {
|
|
861
|
-
const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
|
|
862
|
-
|
|
863
|
-
for (const edge of edges) {
|
|
864
|
-
const child = await backend.getNode(edge.dst);
|
|
865
|
-
if (!child) continue;
|
|
866
|
-
|
|
867
|
-
const childType = (child as any).type || (child as any).nodeType;
|
|
868
|
-
|
|
869
|
-
if (childType === 'CALL') {
|
|
870
|
-
calls.push({
|
|
871
|
-
id: child.id,
|
|
872
|
-
type: 'CALL',
|
|
873
|
-
name: (child as any).name || '',
|
|
874
|
-
file: (child as any).file || '',
|
|
875
|
-
line: (child as any).line,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (childType !== 'FUNCTION' && childType !== 'CLASS') {
|
|
880
|
-
queue.push({ id: child.id, depth: depth + 1 });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
} catch {
|
|
884
|
-
// Ignore
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
return calls;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
839
|
async function searchNode(backend: RFDBServerBackend, query: string): Promise<NodeInfo | null> {
|
|
892
840
|
const results = await searchNodes(backend, query, 1);
|
|
893
841
|
return results[0] || null;
|
|
@@ -1020,18 +968,199 @@ async function findStartNode(backend: RFDBServerBackend, startName: string | nul
|
|
|
1020
968
|
return bestNode;
|
|
1021
969
|
}
|
|
1022
970
|
|
|
1023
|
-
//
|
|
971
|
+
// =============================================================================
|
|
972
|
+
// Batch Mode Implementation
|
|
973
|
+
// =============================================================================
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Run explore in batch mode - for AI agents, CI, and scripts
|
|
977
|
+
*/
|
|
978
|
+
async function runBatchExplore(
|
|
979
|
+
backend: RFDBServerBackend,
|
|
980
|
+
options: ExploreOptions,
|
|
981
|
+
projectPath: string
|
|
982
|
+
): Promise<void> {
|
|
983
|
+
const depth = parseInt(options.depth || '3', 10) || 3;
|
|
984
|
+
const useJson = options.json || options.format === 'json' || options.format !== 'text';
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
if (options.query) {
|
|
988
|
+
// Search mode
|
|
989
|
+
const results = await searchNodes(backend, options.query, 20);
|
|
990
|
+
outputResults(results, 'search', useJson, projectPath);
|
|
991
|
+
} else if (options.callers) {
|
|
992
|
+
// Callers mode
|
|
993
|
+
const target = await searchNode(backend, options.callers);
|
|
994
|
+
if (!target) {
|
|
995
|
+
exitWithError(`Function "${options.callers}" not found`, [
|
|
996
|
+
'Try: grafema query "partial-name"',
|
|
997
|
+
]);
|
|
998
|
+
}
|
|
999
|
+
const callers = await getCallersRecursive(backend, target.id, depth);
|
|
1000
|
+
outputResults(callers, 'callers', useJson, projectPath, target);
|
|
1001
|
+
} else if (options.callees) {
|
|
1002
|
+
// Callees mode
|
|
1003
|
+
const target = await searchNode(backend, options.callees);
|
|
1004
|
+
if (!target) {
|
|
1005
|
+
exitWithError(`Function "${options.callees}" not found`, [
|
|
1006
|
+
'Try: grafema query "partial-name"',
|
|
1007
|
+
]);
|
|
1008
|
+
}
|
|
1009
|
+
const callees = await getCalleesRecursive(backend, target.id, depth);
|
|
1010
|
+
outputResults(callees, 'callees', useJson, projectPath, target);
|
|
1011
|
+
}
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
exitWithError(`Explore failed: ${(err as Error).message}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Output results in JSON or text format
|
|
1019
|
+
*/
|
|
1020
|
+
function outputResults(
|
|
1021
|
+
nodes: NodeInfo[],
|
|
1022
|
+
mode: 'search' | 'callers' | 'callees',
|
|
1023
|
+
useJson: boolean,
|
|
1024
|
+
projectPath: string,
|
|
1025
|
+
target?: NodeInfo
|
|
1026
|
+
): void {
|
|
1027
|
+
if (useJson) {
|
|
1028
|
+
const output = {
|
|
1029
|
+
mode,
|
|
1030
|
+
target: target ? formatNodeForJson(target, projectPath) : undefined,
|
|
1031
|
+
count: nodes.length,
|
|
1032
|
+
results: nodes.map(n => formatNodeForJson(n, projectPath)),
|
|
1033
|
+
};
|
|
1034
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1035
|
+
} else {
|
|
1036
|
+
// Text format
|
|
1037
|
+
if (target) {
|
|
1038
|
+
console.log(`${mode === 'callers' ? 'Callers of' : 'Callees of'}: ${target.name}`);
|
|
1039
|
+
console.log(`File: ${relative(projectPath, target.file)}${target.line ? `:${target.line}` : ''}`);
|
|
1040
|
+
console.log('');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (nodes.length === 0) {
|
|
1044
|
+
console.log(` (no ${mode} found)`);
|
|
1045
|
+
} else {
|
|
1046
|
+
for (const node of nodes) {
|
|
1047
|
+
const loc = relative(projectPath, node.file);
|
|
1048
|
+
console.log(` ${node.type} ${node.name} (${loc}${node.line ? `:${node.line}` : ''})`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
console.log('');
|
|
1053
|
+
console.log(`Total: ${nodes.length}`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function formatNodeForJson(node: NodeInfo, projectPath: string): object {
|
|
1058
|
+
return {
|
|
1059
|
+
id: node.id,
|
|
1060
|
+
type: node.type,
|
|
1061
|
+
name: node.name,
|
|
1062
|
+
file: relative(projectPath, node.file),
|
|
1063
|
+
line: node.line,
|
|
1064
|
+
async: node.async,
|
|
1065
|
+
exported: node.exported,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Get callers recursively up to specified depth
|
|
1071
|
+
*/
|
|
1072
|
+
async function getCallersRecursive(
|
|
1073
|
+
backend: RFDBServerBackend,
|
|
1074
|
+
nodeId: string,
|
|
1075
|
+
maxDepth: number
|
|
1076
|
+
): Promise<NodeInfo[]> {
|
|
1077
|
+
const results: NodeInfo[] = [];
|
|
1078
|
+
const visited = new Set<string>();
|
|
1079
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
1080
|
+
|
|
1081
|
+
while (queue.length > 0) {
|
|
1082
|
+
const { id, depth } = queue.shift()!;
|
|
1083
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
1084
|
+
visited.add(id);
|
|
1085
|
+
|
|
1086
|
+
const callers = await getCallers(backend, id, 50);
|
|
1087
|
+
for (const caller of callers) {
|
|
1088
|
+
if (!visited.has(caller.id)) {
|
|
1089
|
+
results.push(caller);
|
|
1090
|
+
if (depth < maxDepth) {
|
|
1091
|
+
queue.push({ id: caller.id, depth: depth + 1 });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return results;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Get callees recursively up to specified depth
|
|
1102
|
+
*/
|
|
1103
|
+
async function getCalleesRecursive(
|
|
1104
|
+
backend: RFDBServerBackend,
|
|
1105
|
+
nodeId: string,
|
|
1106
|
+
maxDepth: number
|
|
1107
|
+
): Promise<NodeInfo[]> {
|
|
1108
|
+
const results: NodeInfo[] = [];
|
|
1109
|
+
const visited = new Set<string>();
|
|
1110
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
1111
|
+
|
|
1112
|
+
while (queue.length > 0) {
|
|
1113
|
+
const { id, depth } = queue.shift()!;
|
|
1114
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
1115
|
+
visited.add(id);
|
|
1116
|
+
|
|
1117
|
+
const callees = await getCallees(backend, id, 50);
|
|
1118
|
+
for (const callee of callees) {
|
|
1119
|
+
if (!visited.has(callee.id)) {
|
|
1120
|
+
results.push(callee);
|
|
1121
|
+
if (depth < maxDepth) {
|
|
1122
|
+
queue.push({ id: callee.id, depth: depth + 1 });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return results;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// =============================================================================
|
|
1132
|
+
// Command Definition
|
|
1133
|
+
// =============================================================================
|
|
1134
|
+
|
|
1024
1135
|
export const exploreCommand = new Command('explore')
|
|
1025
|
-
.description('Interactive graph navigation')
|
|
1026
|
-
.argument('[start]', 'Starting function name')
|
|
1136
|
+
.description('Interactive graph navigation (TUI) or batch query mode')
|
|
1137
|
+
.argument('[start]', 'Starting function name (for interactive mode)')
|
|
1027
1138
|
.option('-p, --project <path>', 'Project path', '.')
|
|
1028
|
-
.
|
|
1139
|
+
.option('-q, --query <name>', 'Batch: search for nodes by name')
|
|
1140
|
+
.option('--callers <name>', 'Batch: show callers of function')
|
|
1141
|
+
.option('--callees <name>', 'Batch: show callees of function')
|
|
1142
|
+
.option('-d, --depth <n>', 'Batch: traversal depth', '3')
|
|
1143
|
+
.option('-j, --json', 'Output as JSON (default for batch mode)')
|
|
1144
|
+
.option('--format <type>', 'Output format: json or text')
|
|
1145
|
+
.addHelpText('after', `
|
|
1146
|
+
Examples:
|
|
1147
|
+
grafema explore Interactive TUI mode
|
|
1148
|
+
grafema explore "authenticate" Start TUI at specific function
|
|
1149
|
+
grafema explore --query "User" Batch: search for nodes
|
|
1150
|
+
grafema explore --callers "login" Batch: show who calls login
|
|
1151
|
+
grafema explore --callees "main" Batch: show what main calls
|
|
1152
|
+
grafema explore --callers "auth" -d 5 Batch: callers with depth 5
|
|
1153
|
+
grafema explore --query "api" --format text Batch: text output
|
|
1154
|
+
`)
|
|
1155
|
+
.action(async (start: string | undefined, options: ExploreOptions) => {
|
|
1029
1156
|
const projectPath = resolve(options.project);
|
|
1030
1157
|
const grafemaDir = join(projectPath, '.grafema');
|
|
1031
1158
|
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
1032
1159
|
|
|
1033
1160
|
if (!existsSync(dbPath)) {
|
|
1034
|
-
exitWithError('No
|
|
1161
|
+
exitWithError('No database found', [
|
|
1162
|
+
'Run: grafema analyze',
|
|
1163
|
+
]);
|
|
1035
1164
|
}
|
|
1036
1165
|
|
|
1037
1166
|
const backend = new RFDBServerBackend({ dbPath });
|
|
@@ -1039,6 +1168,27 @@ export const exploreCommand = new Command('explore')
|
|
|
1039
1168
|
try {
|
|
1040
1169
|
await backend.connect();
|
|
1041
1170
|
|
|
1171
|
+
// Detect batch mode
|
|
1172
|
+
const isBatchMode = !!(options.query || options.callers || options.callees);
|
|
1173
|
+
|
|
1174
|
+
if (isBatchMode) {
|
|
1175
|
+
await runBatchExplore(backend, options, projectPath);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Interactive mode - check TTY
|
|
1180
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
1181
|
+
|
|
1182
|
+
if (!isTTY) {
|
|
1183
|
+
exitWithError('Interactive mode requires a terminal', [
|
|
1184
|
+
'Batch mode: grafema explore --query "functionName"',
|
|
1185
|
+
'Batch mode: grafema explore --callers "functionName"',
|
|
1186
|
+
'Batch mode: grafema explore --callees "functionName"',
|
|
1187
|
+
'Alternative: grafema query "functionName"',
|
|
1188
|
+
'Alternative: grafema impact "functionName"',
|
|
1189
|
+
]);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1042
1192
|
const startNode = await findStartNode(backend, start || null);
|
|
1043
1193
|
|
|
1044
1194
|
const { waitUntilExit } = render(
|
package/src/commands/get.ts
CHANGED
|
@@ -24,14 +24,16 @@ interface NodeInfo {
|
|
|
24
24
|
name: string;
|
|
25
25
|
file: string;
|
|
26
26
|
line?: number;
|
|
27
|
+
method?: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
url?: string;
|
|
27
30
|
[key: string]: unknown;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
interface Edge {
|
|
31
34
|
src: string;
|
|
32
35
|
dst: string;
|
|
33
|
-
|
|
34
|
-
type?: string;
|
|
36
|
+
type: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
interface EdgeWithName {
|
|
@@ -45,6 +47,13 @@ export const getCommand = new Command('get')
|
|
|
45
47
|
.argument('<semantic-id>', 'Semantic ID of the node (e.g., "file.js->scope->TYPE->name")')
|
|
46
48
|
.option('-p, --project <path>', 'Project path', '.')
|
|
47
49
|
.option('-j, --json', 'Output as JSON')
|
|
50
|
+
.addHelpText('after', `
|
|
51
|
+
Examples:
|
|
52
|
+
grafema get "src/auth.js->authenticate->FUNCTION" Get function node
|
|
53
|
+
grafema get "src/models/User.js->User->CLASS" Get class node
|
|
54
|
+
grafema get "src/api.js->config->VARIABLE" Get variable node
|
|
55
|
+
grafema get "src/auth.js->authenticate->FUNCTION" -j Output as JSON with edges
|
|
56
|
+
`)
|
|
48
57
|
.action(async (semanticId: string, options: GetOptions) => {
|
|
49
58
|
const projectPath = resolve(options.project);
|
|
50
59
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -95,7 +104,7 @@ async function outputJSON(
|
|
|
95
104
|
// Fetch target node names for all edges
|
|
96
105
|
const incomingWithNames = await Promise.all(
|
|
97
106
|
incomingEdges.map(async (edge) => ({
|
|
98
|
-
edgeType: edge.
|
|
107
|
+
edgeType: edge.type || 'UNKNOWN',
|
|
99
108
|
targetId: edge.src,
|
|
100
109
|
targetName: await getNodeName(backend, edge.src),
|
|
101
110
|
}))
|
|
@@ -103,7 +112,7 @@ async function outputJSON(
|
|
|
103
112
|
|
|
104
113
|
const outgoingWithNames = await Promise.all(
|
|
105
114
|
outgoingEdges.map(async (edge) => ({
|
|
106
|
-
edgeType: edge.
|
|
115
|
+
edgeType: edge.type || 'UNKNOWN',
|
|
107
116
|
targetId: edge.dst,
|
|
108
117
|
targetName: await getNodeName(backend, edge.dst),
|
|
109
118
|
}))
|
|
@@ -147,6 +156,9 @@ async function outputText(
|
|
|
147
156
|
name: node.name || '',
|
|
148
157
|
file: node.file || '',
|
|
149
158
|
line: node.line,
|
|
159
|
+
method: node.method,
|
|
160
|
+
path: node.path,
|
|
161
|
+
url: node.url,
|
|
150
162
|
};
|
|
151
163
|
|
|
152
164
|
// Display node details
|
|
@@ -190,7 +202,7 @@ async function displayEdges(
|
|
|
190
202
|
const byType = new Map<string, EdgeWithName[]>();
|
|
191
203
|
|
|
192
204
|
for (const edge of edges) {
|
|
193
|
-
const edgeType = edge.
|
|
205
|
+
const edgeType = edge.type || 'UNKNOWN';
|
|
194
206
|
const targetId = getTargetId(edge);
|
|
195
207
|
const targetName = await getNodeName(backend, targetId);
|
|
196
208
|
|
|
@@ -246,11 +258,13 @@ async function getNodeName(backend: RFDBServerBackend, nodeId: string): Promise<
|
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
/**
|
|
249
|
-
* Extract metadata fields (exclude standard fields)
|
|
261
|
+
* Extract metadata fields (exclude standard and display fields)
|
|
250
262
|
*/
|
|
251
263
|
function getMetadataFields(node: any): Record<string, unknown> {
|
|
252
264
|
const standardFields = new Set([
|
|
253
265
|
'id', 'type', 'nodeType', 'name', 'file', 'line',
|
|
266
|
+
// Display fields shown in primary line for HTTP nodes
|
|
267
|
+
'method', 'path', 'url',
|
|
254
268
|
]);
|
|
255
269
|
|
|
256
270
|
const metadata: Record<string, unknown> = {};
|
package/src/commands/impact.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Command } from 'commander';
|
|
|
10
10
|
import { resolve, join, dirname } from 'path';
|
|
11
11
|
import { relative } from 'path';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
|
-
import { RFDBServerBackend } from '@grafema/core';
|
|
13
|
+
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore, type CallerInfo } from '@grafema/core';
|
|
14
14
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
15
15
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
16
16
|
|
|
@@ -42,6 +42,14 @@ export const impactCommand = new Command('impact')
|
|
|
42
42
|
.option('-p, --project <path>', 'Project path', '.')
|
|
43
43
|
.option('-j, --json', 'Output as JSON')
|
|
44
44
|
.option('-d, --depth <n>', 'Max traversal depth', '10')
|
|
45
|
+
.addHelpText('after', `
|
|
46
|
+
Examples:
|
|
47
|
+
grafema impact "authenticate" Analyze impact of changing authenticate
|
|
48
|
+
grafema impact "function login" Impact of specific function
|
|
49
|
+
grafema impact "class UserService" Impact of class changes
|
|
50
|
+
grafema impact "validate" -d 3 Limit analysis depth to 3 levels
|
|
51
|
+
grafema impact "auth" --json Output impact analysis as JSON
|
|
52
|
+
`)
|
|
45
53
|
.action(async (pattern: string, options: ImpactOptions) => {
|
|
46
54
|
const projectPath = resolve(options.project);
|
|
47
55
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -159,10 +167,21 @@ async function analyzeImpact(
|
|
|
159
167
|
const callChains: string[][] = [];
|
|
160
168
|
const visited = new Set<string>();
|
|
161
169
|
|
|
170
|
+
// If target is a CLASS, aggregate callers from all methods
|
|
171
|
+
let targetIds: string[];
|
|
172
|
+
if (target.type === 'CLASS') {
|
|
173
|
+
const methodIds = await getClassMethods(backend, target.id);
|
|
174
|
+
targetIds = [target.id, ...methodIds];
|
|
175
|
+
} else {
|
|
176
|
+
targetIds = [target.id];
|
|
177
|
+
}
|
|
178
|
+
|
|
162
179
|
// BFS to find all callers
|
|
163
|
-
const queue: Array<{ id: string; depth: number; chain: string[] }> =
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
const queue: Array<{ id: string; depth: number; chain: string[] }> = targetIds.map(id => ({
|
|
181
|
+
id,
|
|
182
|
+
depth: 0,
|
|
183
|
+
chain: [target.name]
|
|
184
|
+
}));
|
|
166
185
|
|
|
167
186
|
while (queue.length > 0) {
|
|
168
187
|
const { id, depth, chain } = queue.shift()!;
|
|
@@ -179,14 +198,19 @@ async function analyzeImpact(
|
|
|
179
198
|
|
|
180
199
|
for (const callNode of containingCalls) {
|
|
181
200
|
// Find the function containing this call
|
|
182
|
-
const container = await
|
|
201
|
+
const container = await findContainingFunctionCore(backend, callNode.id);
|
|
183
202
|
|
|
184
203
|
if (container && !visited.has(container.id)) {
|
|
204
|
+
// Filter out internal callers (methods of the same class)
|
|
205
|
+
if (target.type === 'CLASS' && targetIds.includes(container.id)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
const caller: NodeInfo = {
|
|
186
210
|
id: container.id,
|
|
187
211
|
type: container.type,
|
|
188
212
|
name: container.name,
|
|
189
|
-
file: container.file,
|
|
213
|
+
file: container.file || '',
|
|
190
214
|
line: container.line,
|
|
191
215
|
};
|
|
192
216
|
|
|
@@ -227,6 +251,31 @@ async function analyzeImpact(
|
|
|
227
251
|
};
|
|
228
252
|
}
|
|
229
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Get method IDs for a class
|
|
256
|
+
*/
|
|
257
|
+
async function getClassMethods(
|
|
258
|
+
backend: RFDBServerBackend,
|
|
259
|
+
classId: string
|
|
260
|
+
): Promise<string[]> {
|
|
261
|
+
const methods: string[] = [];
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
265
|
+
|
|
266
|
+
for (const edge of edges) {
|
|
267
|
+
const node = await backend.getNode(edge.dst);
|
|
268
|
+
if (node && node.type === 'FUNCTION') {
|
|
269
|
+
methods.push(node.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Ignore errors
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return methods;
|
|
277
|
+
}
|
|
278
|
+
|
|
230
279
|
/**
|
|
231
280
|
* Find CALL nodes that reference a target
|
|
232
281
|
*/
|
|
@@ -259,61 +308,6 @@ async function findCallsToNode(
|
|
|
259
308
|
return calls;
|
|
260
309
|
}
|
|
261
310
|
|
|
262
|
-
/**
|
|
263
|
-
* Find the function that contains a call node
|
|
264
|
-
*
|
|
265
|
-
* Path: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
|
|
266
|
-
*/
|
|
267
|
-
async function findContainingFunction(
|
|
268
|
-
backend: RFDBServerBackend,
|
|
269
|
-
nodeId: string,
|
|
270
|
-
maxDepth: number = 15
|
|
271
|
-
): Promise<NodeInfo | null> {
|
|
272
|
-
const visited = new Set<string>();
|
|
273
|
-
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
274
|
-
|
|
275
|
-
while (queue.length > 0) {
|
|
276
|
-
const { id, depth } = queue.shift()!;
|
|
277
|
-
|
|
278
|
-
if (visited.has(id) || depth > maxDepth) continue;
|
|
279
|
-
visited.add(id);
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
// Get incoming edges: CONTAINS, HAS_SCOPE
|
|
283
|
-
const edges = await backend.getIncomingEdges(id, null);
|
|
284
|
-
|
|
285
|
-
for (const edge of edges) {
|
|
286
|
-
const edgeType = (edge as any).edgeType || (edge as any).type;
|
|
287
|
-
|
|
288
|
-
// Only follow structural edges
|
|
289
|
-
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
|
|
290
|
-
|
|
291
|
-
const parent = await backend.getNode(edge.src);
|
|
292
|
-
if (!parent || visited.has(parent.id)) continue;
|
|
293
|
-
|
|
294
|
-
const parentType = parent.type;
|
|
295
|
-
|
|
296
|
-
// FUNCTION, CLASS, or MODULE (for top-level calls)
|
|
297
|
-
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
298
|
-
return {
|
|
299
|
-
id: parent.id,
|
|
300
|
-
type: parentType,
|
|
301
|
-
name: parent.name || '',
|
|
302
|
-
file: parent.file || '',
|
|
303
|
-
line: parent.line,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
queue.push({ id: parent.id, depth: depth + 1 });
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
// Ignore
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
311
|
/**
|
|
318
312
|
* Get module path relative to project
|
|
319
313
|
*/
|