@grafema/cli 0.1.1-alpha
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/LICENSE +190 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +36 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +209 -0
- package/dist/commands/check.d.ts +10 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +295 -0
- package/dist/commands/coverage.d.ts +11 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +96 -0
- package/dist/commands/explore.d.ts +6 -0
- package/dist/commands/explore.d.ts.map +1 -0
- package/dist/commands/explore.js +633 -0
- package/dist/commands/get.d.ts +10 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +189 -0
- package/dist/commands/impact.d.ts +10 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +313 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/overview.d.ts +6 -0
- package/dist/commands/overview.d.ts.map +1 -0
- package/dist/commands/overview.js +91 -0
- package/dist/commands/query.d.ts +13 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +340 -0
- package/dist/commands/server.d.ts +11 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +300 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +52 -0
- package/dist/commands/trace.d.ts +10 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +270 -0
- package/dist/utils/codePreview.d.ts +28 -0
- package/dist/utils/codePreview.d.ts.map +1 -0
- package/dist/utils/codePreview.js +51 -0
- package/dist/utils/errorFormatter.d.ts +24 -0
- package/dist/utils/errorFormatter.d.ts.map +1 -0
- package/dist/utils/errorFormatter.js +32 -0
- package/dist/utils/formatNode.d.ts +53 -0
- package/dist/utils/formatNode.d.ts.map +1 -0
- package/dist/utils/formatNode.js +49 -0
- package/package.json +54 -0
- package/src/cli.ts +41 -0
- package/src/commands/analyze.ts +271 -0
- package/src/commands/check.ts +379 -0
- package/src/commands/coverage.ts +108 -0
- package/src/commands/explore.tsx +1056 -0
- package/src/commands/get.ts +265 -0
- package/src/commands/impact.ts +400 -0
- package/src/commands/init.ts +112 -0
- package/src/commands/overview.ts +108 -0
- package/src/commands/query.ts +425 -0
- package/src/commands/server.ts +335 -0
- package/src/commands/stats.ts +58 -0
- package/src/commands/trace.ts +341 -0
- package/src/utils/codePreview.ts +77 -0
- package/src/utils/errorFormatter.ts +35 -0
- package/src/utils/formatNode.ts +88 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - Initialize Grafema in a project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
8
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
9
|
+
import { stringify as stringifyYAML } from 'yaml';
|
|
10
|
+
import { DEFAULT_CONFIG } from '@grafema/core';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate config.yaml content with commented future features.
|
|
14
|
+
* Only includes implemented features (plugins).
|
|
15
|
+
*/
|
|
16
|
+
function generateConfigYAML(): string {
|
|
17
|
+
// Start with working default config
|
|
18
|
+
const config = {
|
|
19
|
+
// Plugin list (fully implemented)
|
|
20
|
+
plugins: DEFAULT_CONFIG.plugins,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Convert to YAML
|
|
24
|
+
const yaml = stringifyYAML(config, {
|
|
25
|
+
lineWidth: 0, // Don't wrap long lines
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Add header comment
|
|
29
|
+
return `# Grafema Configuration
|
|
30
|
+
# Documentation: https://github.com/grafema/grafema#configuration
|
|
31
|
+
|
|
32
|
+
${yaml}
|
|
33
|
+
# Future: File discovery patterns (not yet implemented)
|
|
34
|
+
# Grafema currently uses entrypoint-based discovery (follows imports from package.json main field)
|
|
35
|
+
# Glob-based include/exclude patterns will be added in a future release
|
|
36
|
+
#
|
|
37
|
+
# include:
|
|
38
|
+
# - "src/**/*.{ts,js,tsx,jsx}"
|
|
39
|
+
# exclude:
|
|
40
|
+
# - "**/*.test.ts"
|
|
41
|
+
# - "node_modules/**"
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface InitOptions {
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const initCommand = new Command('init')
|
|
50
|
+
.description('Initialize Grafema in current project')
|
|
51
|
+
.argument('[path]', 'Project path', '.')
|
|
52
|
+
.option('-f, --force', 'Overwrite existing config')
|
|
53
|
+
.action(async (path: string, options: InitOptions) => {
|
|
54
|
+
const projectPath = resolve(path);
|
|
55
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
56
|
+
const configPath = join(grafemaDir, 'config.yaml');
|
|
57
|
+
const packageJsonPath = join(projectPath, 'package.json');
|
|
58
|
+
const tsconfigPath = join(projectPath, 'tsconfig.json');
|
|
59
|
+
|
|
60
|
+
// Check package.json
|
|
61
|
+
if (!existsSync(packageJsonPath)) {
|
|
62
|
+
exitWithError('No package.json found', [
|
|
63
|
+
'Initialize a project: npm init',
|
|
64
|
+
'Or check you are in the right directory'
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
console.log('✓ Found package.json');
|
|
68
|
+
|
|
69
|
+
// Detect TypeScript
|
|
70
|
+
const isTypeScript = existsSync(tsconfigPath);
|
|
71
|
+
if (isTypeScript) {
|
|
72
|
+
console.log('✓ Detected TypeScript project');
|
|
73
|
+
} else {
|
|
74
|
+
console.log('✓ Detected JavaScript project');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check existing config
|
|
78
|
+
if (existsSync(configPath) && !options.force) {
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log('✓ Grafema already initialized');
|
|
81
|
+
console.log(' → Use --force to overwrite config');
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log('Next: Run "grafema analyze" to build the code graph');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create .grafema directory
|
|
88
|
+
if (!existsSync(grafemaDir)) {
|
|
89
|
+
mkdirSync(grafemaDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write config
|
|
93
|
+
const configContent = generateConfigYAML();
|
|
94
|
+
writeFileSync(configPath, configContent);
|
|
95
|
+
console.log('✓ Created .grafema/config.yaml');
|
|
96
|
+
|
|
97
|
+
// Add to .gitignore if exists
|
|
98
|
+
const gitignorePath = join(projectPath, '.gitignore');
|
|
99
|
+
if (existsSync(gitignorePath)) {
|
|
100
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
101
|
+
if (!gitignore.includes('.grafema/graph.rfdb')) {
|
|
102
|
+
writeFileSync(
|
|
103
|
+
gitignorePath,
|
|
104
|
+
gitignore + '\n# Grafema\n.grafema/graph.rfdb\n.grafema/rfdb.sock\n'
|
|
105
|
+
);
|
|
106
|
+
console.log('✓ Updated .gitignore');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('Next: Run "grafema analyze" to build the code graph');
|
|
112
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview command - Project dashboard
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolve, join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
9
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
10
|
+
|
|
11
|
+
interface NodeStats {
|
|
12
|
+
type: string;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const overviewCommand = new Command('overview')
|
|
17
|
+
.description('Show project overview and statistics')
|
|
18
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
19
|
+
.option('-j, --json', 'Output as JSON')
|
|
20
|
+
.action(async (options: { project: string; json?: boolean }) => {
|
|
21
|
+
const projectPath = resolve(options.project);
|
|
22
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
23
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
24
|
+
|
|
25
|
+
if (!existsSync(dbPath)) {
|
|
26
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
30
|
+
await backend.connect();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const stats = await backend.getStats();
|
|
34
|
+
|
|
35
|
+
if (options.json) {
|
|
36
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Header
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('📊 Project Overview');
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Code Structure
|
|
46
|
+
console.log('Code Structure:');
|
|
47
|
+
const modules = stats.nodesByType['MODULE'] || 0;
|
|
48
|
+
const functions = stats.nodesByType['FUNCTION'] || 0;
|
|
49
|
+
const classes = stats.nodesByType['CLASS'] || 0;
|
|
50
|
+
const variables = stats.nodesByType['VARIABLE'] || 0;
|
|
51
|
+
const calls = stats.nodesByType['CALL'] || 0;
|
|
52
|
+
|
|
53
|
+
console.log(`├─ Modules: ${modules}`);
|
|
54
|
+
console.log(`├─ Functions: ${functions}`);
|
|
55
|
+
console.log(`├─ Classes: ${classes}`);
|
|
56
|
+
console.log(`├─ Variables: ${variables}`);
|
|
57
|
+
console.log(`└─ Call sites: ${calls}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
// External Interactions (namespaced types)
|
|
61
|
+
console.log('External Interactions:');
|
|
62
|
+
const httpRoutes = stats.nodesByType['http:route'] || 0;
|
|
63
|
+
const dbQueries = stats.nodesByType['db:query'] || 0;
|
|
64
|
+
const socketEmit = stats.nodesByType['socketio:emit'] || 0;
|
|
65
|
+
const socketOn = stats.nodesByType['socketio:on'] || 0;
|
|
66
|
+
const events = stats.nodesByType['event:listener'] || 0;
|
|
67
|
+
|
|
68
|
+
if (httpRoutes > 0) console.log(`├─ HTTP routes: ${httpRoutes}`);
|
|
69
|
+
if (dbQueries > 0) console.log(`├─ Database queries: ${dbQueries}`);
|
|
70
|
+
if (socketEmit + socketOn > 0) console.log(`├─ Socket.IO: ${socketEmit} emit, ${socketOn} listeners`);
|
|
71
|
+
if (events > 0) console.log(`├─ Event listeners: ${events}`);
|
|
72
|
+
|
|
73
|
+
// Check for external module refs
|
|
74
|
+
const externalModules = stats.nodesByType['EXTERNAL_MODULE'] || 0;
|
|
75
|
+
if (externalModules > 0) console.log(`└─ External modules: ${externalModules}`);
|
|
76
|
+
|
|
77
|
+
if (httpRoutes + dbQueries + socketEmit + socketOn + events + externalModules === 0) {
|
|
78
|
+
console.log('└─ (none detected)');
|
|
79
|
+
}
|
|
80
|
+
console.log('');
|
|
81
|
+
|
|
82
|
+
// Graph Statistics
|
|
83
|
+
console.log('Graph Statistics:');
|
|
84
|
+
console.log(`├─ Total nodes: ${stats.nodeCount}`);
|
|
85
|
+
console.log(`├─ Total edges: ${stats.edgeCount}`);
|
|
86
|
+
|
|
87
|
+
// Show edge breakdown
|
|
88
|
+
const callEdges = stats.edgesByType['CALLS'] || 0;
|
|
89
|
+
const containsEdges = stats.edgesByType['CONTAINS'] || 0;
|
|
90
|
+
const importsEdges = stats.edgesByType['IMPORTS'] || 0;
|
|
91
|
+
|
|
92
|
+
console.log(`├─ Calls: ${callEdges}`);
|
|
93
|
+
console.log(`├─ Contains: ${containsEdges}`);
|
|
94
|
+
console.log(`└─ Imports: ${importsEdges}`);
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// Find most called functions (via incoming CALLS edges)
|
|
98
|
+
// This requires a query - simplified for now
|
|
99
|
+
console.log('Next steps:');
|
|
100
|
+
console.log('→ grafema query "function <name>" Search for a function');
|
|
101
|
+
console.log('→ grafema trace "<var> from <fn>" Trace data flow');
|
|
102
|
+
console.log('→ grafema impact "<target>" Analyze change impact');
|
|
103
|
+
console.log('→ grafema explore Interactive navigation');
|
|
104
|
+
|
|
105
|
+
} finally {
|
|
106
|
+
await backend.close();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query command - Search the code graph
|
|
3
|
+
*
|
|
4
|
+
* Supports patterns like:
|
|
5
|
+
* grafema query "function authenticate"
|
|
6
|
+
* grafema query "class UserService"
|
|
7
|
+
* grafema query "authenticate" (searches all types)
|
|
8
|
+
*
|
|
9
|
+
* For raw Datalog queries, use --raw flag
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { resolve, join, relative } from 'path';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import { RFDBServerBackend } from '@grafema/core';
|
|
16
|
+
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
17
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
|
+
|
|
19
|
+
interface QueryOptions {
|
|
20
|
+
project: string;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
limit: string;
|
|
23
|
+
raw?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface NodeInfo {
|
|
27
|
+
id: string;
|
|
28
|
+
type: string;
|
|
29
|
+
name: string;
|
|
30
|
+
file: string;
|
|
31
|
+
line?: number;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const queryCommand = new Command('query')
|
|
36
|
+
.description('Search the code graph')
|
|
37
|
+
.argument('<pattern>', 'Search pattern: "function X", "class Y", or just "X"')
|
|
38
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
39
|
+
.option('-j, --json', 'Output as JSON')
|
|
40
|
+
.option('-l, --limit <n>', 'Limit results', '10')
|
|
41
|
+
.option('--raw', 'Execute raw Datalog query')
|
|
42
|
+
.action(async (pattern: string, options: QueryOptions) => {
|
|
43
|
+
const projectPath = resolve(options.project);
|
|
44
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
45
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
46
|
+
|
|
47
|
+
if (!existsSync(dbPath)) {
|
|
48
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
52
|
+
await backend.connect();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Raw Datalog mode
|
|
56
|
+
if (options.raw) {
|
|
57
|
+
await executeRawQuery(backend, pattern, options);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse pattern
|
|
62
|
+
const { type, name } = parsePattern(pattern);
|
|
63
|
+
const limit = parseInt(options.limit, 10);
|
|
64
|
+
|
|
65
|
+
// Find matching nodes
|
|
66
|
+
const nodes = await findNodes(backend, type, name, limit);
|
|
67
|
+
|
|
68
|
+
if (nodes.length === 0) {
|
|
69
|
+
console.log(`No results for "${pattern}"`);
|
|
70
|
+
if (type) {
|
|
71
|
+
console.log(` → Try: grafema query "${name}" (search all types)`);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.json) {
|
|
77
|
+
const results = await Promise.all(
|
|
78
|
+
nodes.map(async (node) => ({
|
|
79
|
+
...node,
|
|
80
|
+
calledBy: await getCallers(backend, node.id, 5),
|
|
81
|
+
calls: await getCallees(backend, node.id, 5),
|
|
82
|
+
}))
|
|
83
|
+
);
|
|
84
|
+
console.log(JSON.stringify(results, null, 2));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Display results
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
console.log('');
|
|
91
|
+
displayNode(node, projectPath);
|
|
92
|
+
|
|
93
|
+
// Show callers and callees for functions
|
|
94
|
+
if (node.type === 'FUNCTION' || node.type === 'CLASS') {
|
|
95
|
+
const callers = await getCallers(backend, node.id, 5);
|
|
96
|
+
const callees = await getCallees(backend, node.id, 5);
|
|
97
|
+
|
|
98
|
+
if (callers.length > 0) {
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(`Called by (${callers.length}${callers.length >= 5 ? '+' : ''}):`);
|
|
101
|
+
for (const caller of callers) {
|
|
102
|
+
console.log(` <- ${formatNodeInline(caller)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (callees.length > 0) {
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(`Calls (${callees.length}${callees.length >= 5 ? '+' : ''}):`);
|
|
109
|
+
for (const callee of callees) {
|
|
110
|
+
console.log(` -> ${formatNodeInline(callee)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (nodes.length > 1) {
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(`Found ${nodes.length} results. Use more specific pattern to narrow.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} finally {
|
|
122
|
+
await backend.close();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse search pattern like "function authenticate" or just "authenticate"
|
|
128
|
+
*/
|
|
129
|
+
function parsePattern(pattern: string): { type: string | null; name: string } {
|
|
130
|
+
const words = pattern.trim().split(/\s+/);
|
|
131
|
+
|
|
132
|
+
if (words.length >= 2) {
|
|
133
|
+
const typeWord = words[0].toLowerCase();
|
|
134
|
+
const name = words.slice(1).join(' ');
|
|
135
|
+
|
|
136
|
+
const typeMap: Record<string, string> = {
|
|
137
|
+
function: 'FUNCTION',
|
|
138
|
+
fn: 'FUNCTION',
|
|
139
|
+
func: 'FUNCTION',
|
|
140
|
+
class: 'CLASS',
|
|
141
|
+
module: 'MODULE',
|
|
142
|
+
variable: 'VARIABLE',
|
|
143
|
+
var: 'VARIABLE',
|
|
144
|
+
const: 'CONSTANT',
|
|
145
|
+
constant: 'CONSTANT',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (typeMap[typeWord]) {
|
|
149
|
+
return { type: typeMap[typeWord], name };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { type: null, name: pattern.trim() };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find nodes by type and name
|
|
158
|
+
*/
|
|
159
|
+
async function findNodes(
|
|
160
|
+
backend: RFDBServerBackend,
|
|
161
|
+
type: string | null,
|
|
162
|
+
name: string,
|
|
163
|
+
limit: number
|
|
164
|
+
): Promise<NodeInfo[]> {
|
|
165
|
+
const results: NodeInfo[] = [];
|
|
166
|
+
const searchTypes = type
|
|
167
|
+
? [type]
|
|
168
|
+
: ['FUNCTION', 'CLASS', 'MODULE', 'VARIABLE', 'CONSTANT'];
|
|
169
|
+
|
|
170
|
+
for (const nodeType of searchTypes) {
|
|
171
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
|
|
172
|
+
const nodeName = node.name || '';
|
|
173
|
+
// Case-insensitive partial match
|
|
174
|
+
if (nodeName.toLowerCase().includes(name.toLowerCase())) {
|
|
175
|
+
results.push({
|
|
176
|
+
id: node.id,
|
|
177
|
+
type: node.type || nodeType,
|
|
178
|
+
name: nodeName,
|
|
179
|
+
file: node.file || '',
|
|
180
|
+
line: node.line,
|
|
181
|
+
});
|
|
182
|
+
if (results.length >= limit) break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (results.length >= limit) break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get functions that call this node
|
|
193
|
+
*
|
|
194
|
+
* Logic: FUNCTION ← CONTAINS ← CALL → CALLS → TARGET
|
|
195
|
+
* We need to find CALL nodes that CALLS this target,
|
|
196
|
+
* then find the FUNCTION that CONTAINS each CALL
|
|
197
|
+
*/
|
|
198
|
+
async function getCallers(
|
|
199
|
+
backend: RFDBServerBackend,
|
|
200
|
+
nodeId: string,
|
|
201
|
+
limit: number
|
|
202
|
+
): Promise<NodeInfo[]> {
|
|
203
|
+
const callers: NodeInfo[] = [];
|
|
204
|
+
const seen = new Set<string>();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Find CALL nodes that call this target
|
|
208
|
+
const callEdges = await backend.getIncomingEdges(nodeId, ['CALLS']);
|
|
209
|
+
|
|
210
|
+
for (const edge of callEdges) {
|
|
211
|
+
if (callers.length >= limit) break;
|
|
212
|
+
|
|
213
|
+
const callNode = await backend.getNode(edge.src);
|
|
214
|
+
if (!callNode) continue;
|
|
215
|
+
|
|
216
|
+
// Find the FUNCTION that contains this CALL
|
|
217
|
+
const containingFunc = await findContainingFunction(backend, callNode.id);
|
|
218
|
+
|
|
219
|
+
if (containingFunc && !seen.has(containingFunc.id)) {
|
|
220
|
+
seen.add(containingFunc.id);
|
|
221
|
+
callers.push({
|
|
222
|
+
id: containingFunc.id,
|
|
223
|
+
type: containingFunc.type || 'FUNCTION',
|
|
224
|
+
name: containingFunc.name || '<anonymous>',
|
|
225
|
+
file: containingFunc.file || '',
|
|
226
|
+
line: containingFunc.line,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Ignore errors
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return callers;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Find the FUNCTION or CLASS that contains a node
|
|
239
|
+
*
|
|
240
|
+
* Path can be: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
|
|
241
|
+
* So we need to follow both CONTAINS and HAS_SCOPE edges
|
|
242
|
+
*/
|
|
243
|
+
async function findContainingFunction(
|
|
244
|
+
backend: RFDBServerBackend,
|
|
245
|
+
nodeId: string,
|
|
246
|
+
maxDepth: number = 15
|
|
247
|
+
): Promise<NodeInfo | null> {
|
|
248
|
+
const visited = new Set<string>();
|
|
249
|
+
const queue: Array<{ id: string; depth: number }> = [{ id: nodeId, depth: 0 }];
|
|
250
|
+
|
|
251
|
+
while (queue.length > 0) {
|
|
252
|
+
const { id, depth } = queue.shift()!;
|
|
253
|
+
|
|
254
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
255
|
+
visited.add(id);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
|
|
259
|
+
const edges = await backend.getIncomingEdges(id, null);
|
|
260
|
+
|
|
261
|
+
for (const edge of edges) {
|
|
262
|
+
const edgeType = (edge as any).edgeType || (edge as any).type;
|
|
263
|
+
|
|
264
|
+
// Only follow structural edges
|
|
265
|
+
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType)) continue;
|
|
266
|
+
|
|
267
|
+
const parentNode = await backend.getNode(edge.src);
|
|
268
|
+
if (!parentNode || visited.has(parentNode.id)) continue;
|
|
269
|
+
|
|
270
|
+
const parentType = parentNode.type;
|
|
271
|
+
|
|
272
|
+
// FUNCTION, CLASS, or MODULE (for top-level calls)
|
|
273
|
+
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
274
|
+
return {
|
|
275
|
+
id: parentNode.id,
|
|
276
|
+
type: parentType,
|
|
277
|
+
name: parentNode.name || '<anonymous>',
|
|
278
|
+
file: parentNode.file || '',
|
|
279
|
+
line: parentNode.line,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Continue searching from this parent
|
|
284
|
+
queue.push({ id: parentNode.id, depth: depth + 1 });
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Ignore errors
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get functions that this node calls
|
|
296
|
+
*
|
|
297
|
+
* Logic: FUNCTION → CONTAINS → CALL → CALLS → TARGET
|
|
298
|
+
* Find all CALL nodes inside this function, then find what they call
|
|
299
|
+
*/
|
|
300
|
+
async function getCallees(
|
|
301
|
+
backend: RFDBServerBackend,
|
|
302
|
+
nodeId: string,
|
|
303
|
+
limit: number
|
|
304
|
+
): Promise<NodeInfo[]> {
|
|
305
|
+
const callees: NodeInfo[] = [];
|
|
306
|
+
const seen = new Set<string>();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Find all CALL nodes inside this function (via CONTAINS)
|
|
310
|
+
const callNodes = await findCallsInFunction(backend, nodeId);
|
|
311
|
+
|
|
312
|
+
for (const callNode of callNodes) {
|
|
313
|
+
if (callees.length >= limit) break;
|
|
314
|
+
|
|
315
|
+
// Find what this CALL calls
|
|
316
|
+
const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
|
|
317
|
+
|
|
318
|
+
for (const edge of callEdges) {
|
|
319
|
+
if (callees.length >= limit) break;
|
|
320
|
+
|
|
321
|
+
const targetNode = await backend.getNode(edge.dst);
|
|
322
|
+
if (!targetNode || seen.has(targetNode.id)) continue;
|
|
323
|
+
|
|
324
|
+
seen.add(targetNode.id);
|
|
325
|
+
callees.push({
|
|
326
|
+
id: targetNode.id,
|
|
327
|
+
type: targetNode.type || 'UNKNOWN',
|
|
328
|
+
name: targetNode.name || '<anonymous>',
|
|
329
|
+
file: targetNode.file || '',
|
|
330
|
+
line: targetNode.line,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// Ignore errors
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return callees;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Find all CALL nodes inside a function (recursively via CONTAINS)
|
|
343
|
+
*/
|
|
344
|
+
async function findCallsInFunction(
|
|
345
|
+
backend: RFDBServerBackend,
|
|
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 }];
|
|
352
|
+
|
|
353
|
+
while (queue.length > 0) {
|
|
354
|
+
const { id, depth } = queue.shift()!;
|
|
355
|
+
|
|
356
|
+
if (visited.has(id) || depth > maxDepth) continue;
|
|
357
|
+
visited.add(id);
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
// Get children via CONTAINS
|
|
361
|
+
const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
|
|
362
|
+
|
|
363
|
+
for (const edge of edges) {
|
|
364
|
+
const child = await backend.getNode(edge.dst);
|
|
365
|
+
if (!child) continue;
|
|
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
|
+
}
|
|
378
|
+
|
|
379
|
+
// Continue searching in children (but not into nested functions)
|
|
380
|
+
if (childType !== 'FUNCTION' && childType !== 'CLASS') {
|
|
381
|
+
queue.push({ id: child.id, depth: depth + 1 });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// Ignore
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return calls;
|
|
390
|
+
}
|
|
391
|
+
|
|
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 }));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Execute raw Datalog query (backwards compat)
|
|
401
|
+
*/
|
|
402
|
+
async function executeRawQuery(
|
|
403
|
+
backend: RFDBServerBackend,
|
|
404
|
+
query: string,
|
|
405
|
+
options: QueryOptions
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
const results = await backend.datalogQuery(query);
|
|
408
|
+
const limit = parseInt(options.limit, 10);
|
|
409
|
+
const limited = results.slice(0, limit);
|
|
410
|
+
|
|
411
|
+
if (options.json) {
|
|
412
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
413
|
+
} else {
|
|
414
|
+
if (limited.length === 0) {
|
|
415
|
+
console.log('No results.');
|
|
416
|
+
} else {
|
|
417
|
+
console.log(`Results (${limited.length}${results.length > limit ? ` of ${results.length}` : ''}):`);
|
|
418
|
+
console.log('');
|
|
419
|
+
for (const result of limited) {
|
|
420
|
+
const bindings = result.bindings.map((b) => `${b.name}=${b.value}`).join(', ');
|
|
421
|
+
console.log(` { ${bindings} }`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|