@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
|
@@ -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
|
|
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"}
|
package/dist/commands/explore.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
//
|
|
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
|
|
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;
|
|
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"}
|