@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/dist/commands/get.js
CHANGED
|
@@ -16,6 +16,13 @@ export const getCommand = new Command('get')
|
|
|
16
16
|
.argument('<semantic-id>', 'Semantic ID of the node (e.g., "file.js->scope->TYPE->name")')
|
|
17
17
|
.option('-p, --project <path>', 'Project path', '.')
|
|
18
18
|
.option('-j, --json', 'Output as JSON')
|
|
19
|
+
.addHelpText('after', `
|
|
20
|
+
Examples:
|
|
21
|
+
grafema get "src/auth.js->authenticate->FUNCTION" Get function node
|
|
22
|
+
grafema get "src/models/User.js->User->CLASS" Get class node
|
|
23
|
+
grafema get "src/api.js->config->VARIABLE" Get variable node
|
|
24
|
+
grafema get "src/auth.js->authenticate->FUNCTION" -j Output as JSON with edges
|
|
25
|
+
`)
|
|
19
26
|
.action(async (semanticId, options) => {
|
|
20
27
|
const projectPath = resolve(options.project);
|
|
21
28
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -54,12 +61,12 @@ export const getCommand = new Command('get')
|
|
|
54
61
|
async function outputJSON(backend, node, incomingEdges, outgoingEdges) {
|
|
55
62
|
// Fetch target node names for all edges
|
|
56
63
|
const incomingWithNames = await Promise.all(incomingEdges.map(async (edge) => ({
|
|
57
|
-
edgeType: edge.
|
|
64
|
+
edgeType: edge.type || 'UNKNOWN',
|
|
58
65
|
targetId: edge.src,
|
|
59
66
|
targetName: await getNodeName(backend, edge.src),
|
|
60
67
|
})));
|
|
61
68
|
const outgoingWithNames = await Promise.all(outgoingEdges.map(async (edge) => ({
|
|
62
|
-
edgeType: edge.
|
|
69
|
+
edgeType: edge.type || 'UNKNOWN',
|
|
63
70
|
targetId: edge.dst,
|
|
64
71
|
targetName: await getNodeName(backend, edge.dst),
|
|
65
72
|
})));
|
|
@@ -93,6 +100,9 @@ async function outputText(backend, node, incomingEdges, outgoingEdges, projectPa
|
|
|
93
100
|
name: node.name || '',
|
|
94
101
|
file: node.file || '',
|
|
95
102
|
line: node.line,
|
|
103
|
+
method: node.method,
|
|
104
|
+
path: node.path,
|
|
105
|
+
url: node.url,
|
|
96
106
|
};
|
|
97
107
|
// Display node details
|
|
98
108
|
console.log(formatNodeDisplay(nodeInfo, { projectPath }));
|
|
@@ -124,7 +134,7 @@ async function displayEdges(backend, direction, edges, getTargetId) {
|
|
|
124
134
|
// Group edges by type
|
|
125
135
|
const byType = new Map();
|
|
126
136
|
for (const edge of edges) {
|
|
127
|
-
const edgeType = edge.
|
|
137
|
+
const edgeType = edge.type || 'UNKNOWN';
|
|
128
138
|
const targetId = getTargetId(edge);
|
|
129
139
|
const targetName = await getNodeName(backend, targetId);
|
|
130
140
|
if (!byType.has(edgeType)) {
|
|
@@ -173,11 +183,13 @@ async function getNodeName(backend, nodeId) {
|
|
|
173
183
|
return '';
|
|
174
184
|
}
|
|
175
185
|
/**
|
|
176
|
-
* Extract metadata fields (exclude standard fields)
|
|
186
|
+
* Extract metadata fields (exclude standard and display fields)
|
|
177
187
|
*/
|
|
178
188
|
function getMetadataFields(node) {
|
|
179
189
|
const standardFields = new Set([
|
|
180
190
|
'id', 'type', 'nodeType', 'name', 'file', 'line',
|
|
191
|
+
// Display fields shown in primary line for HTTP nodes
|
|
192
|
+
'method', 'path', 'url',
|
|
181
193
|
]);
|
|
182
194
|
const metadata = {};
|
|
183
195
|
for (const [key, value] of Object.entries(node)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/commands/impact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"impact.d.ts","sourceRoot":"","sources":["../../src/commands/impact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,aAAa,SA6DtB,CAAC"}
|
package/dist/commands/impact.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Command } from 'commander';
|
|
|
9
9
|
import { resolve, join, dirname } from 'path';
|
|
10
10
|
import { relative } from 'path';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
|
-
import { RFDBServerBackend } from '@grafema/core';
|
|
12
|
+
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
13
13
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
14
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
15
|
export const impactCommand = new Command('impact')
|
|
@@ -18,6 +18,14 @@ export const impactCommand = new Command('impact')
|
|
|
18
18
|
.option('-p, --project <path>', 'Project path', '.')
|
|
19
19
|
.option('-j, --json', 'Output as JSON')
|
|
20
20
|
.option('-d, --depth <n>', 'Max traversal depth', '10')
|
|
21
|
+
.addHelpText('after', `
|
|
22
|
+
Examples:
|
|
23
|
+
grafema impact "authenticate" Analyze impact of changing authenticate
|
|
24
|
+
grafema impact "function login" Impact of specific function
|
|
25
|
+
grafema impact "class UserService" Impact of class changes
|
|
26
|
+
grafema impact "validate" -d 3 Limit analysis depth to 3 levels
|
|
27
|
+
grafema impact "auth" --json Output impact analysis as JSON
|
|
28
|
+
`)
|
|
21
29
|
.action(async (pattern, options) => {
|
|
22
30
|
const projectPath = resolve(options.project);
|
|
23
31
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -107,10 +115,21 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
|
|
|
107
115
|
const affectedModules = new Map();
|
|
108
116
|
const callChains = [];
|
|
109
117
|
const visited = new Set();
|
|
118
|
+
// If target is a CLASS, aggregate callers from all methods
|
|
119
|
+
let targetIds;
|
|
120
|
+
if (target.type === 'CLASS') {
|
|
121
|
+
const methodIds = await getClassMethods(backend, target.id);
|
|
122
|
+
targetIds = [target.id, ...methodIds];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
targetIds = [target.id];
|
|
126
|
+
}
|
|
110
127
|
// BFS to find all callers
|
|
111
|
-
const queue =
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
const queue = targetIds.map(id => ({
|
|
129
|
+
id,
|
|
130
|
+
depth: 0,
|
|
131
|
+
chain: [target.name]
|
|
132
|
+
}));
|
|
114
133
|
while (queue.length > 0) {
|
|
115
134
|
const { id, depth, chain } = queue.shift();
|
|
116
135
|
if (visited.has(id))
|
|
@@ -124,13 +143,17 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
|
|
|
124
143
|
const containingCalls = await findCallsToNode(backend, id);
|
|
125
144
|
for (const callNode of containingCalls) {
|
|
126
145
|
// Find the function containing this call
|
|
127
|
-
const container = await
|
|
146
|
+
const container = await findContainingFunctionCore(backend, callNode.id);
|
|
128
147
|
if (container && !visited.has(container.id)) {
|
|
148
|
+
// Filter out internal callers (methods of the same class)
|
|
149
|
+
if (target.type === 'CLASS' && targetIds.includes(container.id)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
129
152
|
const caller = {
|
|
130
153
|
id: container.id,
|
|
131
154
|
type: container.type,
|
|
132
155
|
name: container.name,
|
|
133
|
-
file: container.file,
|
|
156
|
+
file: container.file || '',
|
|
134
157
|
line: container.line,
|
|
135
158
|
};
|
|
136
159
|
if (depth === 0) {
|
|
@@ -166,6 +189,25 @@ async function analyzeImpact(backend, target, maxDepth, projectPath) {
|
|
|
166
189
|
callChains,
|
|
167
190
|
};
|
|
168
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Get method IDs for a class
|
|
194
|
+
*/
|
|
195
|
+
async function getClassMethods(backend, classId) {
|
|
196
|
+
const methods = [];
|
|
197
|
+
try {
|
|
198
|
+
const edges = await backend.getOutgoingEdges(classId, ['CONTAINS']);
|
|
199
|
+
for (const edge of edges) {
|
|
200
|
+
const node = await backend.getNode(edge.dst);
|
|
201
|
+
if (node && node.type === 'FUNCTION') {
|
|
202
|
+
methods.push(node.id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Ignore errors
|
|
208
|
+
}
|
|
209
|
+
return methods;
|
|
210
|
+
}
|
|
169
211
|
/**
|
|
170
212
|
* Find CALL nodes that reference a target
|
|
171
213
|
*/
|
|
@@ -192,50 +234,6 @@ async function findCallsToNode(backend, targetId) {
|
|
|
192
234
|
}
|
|
193
235
|
return calls;
|
|
194
236
|
}
|
|
195
|
-
/**
|
|
196
|
-
* Find the function that contains a call node
|
|
197
|
-
*
|
|
198
|
-
* Path: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
|
|
199
|
-
*/
|
|
200
|
-
async function findContainingFunction(backend, nodeId, maxDepth = 15) {
|
|
201
|
-
const visited = new Set();
|
|
202
|
-
const queue = [{ id: nodeId, depth: 0 }];
|
|
203
|
-
while (queue.length > 0) {
|
|
204
|
-
const { id, depth } = queue.shift();
|
|
205
|
-
if (visited.has(id) || depth > maxDepth)
|
|
206
|
-
continue;
|
|
207
|
-
visited.add(id);
|
|
208
|
-
try {
|
|
209
|
-
// Get incoming edges: CONTAINS, HAS_SCOPE
|
|
210
|
-
const edges = await backend.getIncomingEdges(id, null);
|
|
211
|
-
for (const edge of edges) {
|
|
212
|
-
const edgeType = edge.edgeType || edge.type;
|
|
213
|
-
// Only follow structural edges
|
|
214
|
-
if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
|
|
215
|
-
continue;
|
|
216
|
-
const parent = await backend.getNode(edge.src);
|
|
217
|
-
if (!parent || visited.has(parent.id))
|
|
218
|
-
continue;
|
|
219
|
-
const parentType = parent.type;
|
|
220
|
-
// FUNCTION, CLASS, or MODULE (for top-level calls)
|
|
221
|
-
if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
|
|
222
|
-
return {
|
|
223
|
-
id: parent.id,
|
|
224
|
-
type: parentType,
|
|
225
|
-
name: parent.name || '',
|
|
226
|
-
file: parent.file || '',
|
|
227
|
-
line: parent.line,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
queue.push({ id: parent.id, depth: depth + 1 });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
// Ignore
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
237
|
/**
|
|
240
238
|
* Get module path relative to project
|
|
241
239
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyGpC,eAAO,MAAM,WAAW,SAyFpB,CAAC"}
|
package/dist/commands/init.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { resolve, join } from 'path';
|
|
6
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
7
|
-
import {
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { createInterface } from 'readline';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
8
10
|
import { stringify as stringifyYAML } from 'yaml';
|
|
9
11
|
import { DEFAULT_CONFIG } from '@grafema/core';
|
|
12
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
10
13
|
/**
|
|
11
14
|
* Generate config.yaml content with commented future features.
|
|
12
15
|
* Only includes implemented features (plugins).
|
|
@@ -26,21 +29,79 @@ function generateConfigYAML() {
|
|
|
26
29
|
# Documentation: https://github.com/grafema/grafema#configuration
|
|
27
30
|
|
|
28
31
|
${yaml}
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
+
# File filtering patterns (optional)
|
|
33
|
+
# By default, Grafema follows imports from package.json entry points.
|
|
34
|
+
# Use these patterns to control which files are analyzed:
|
|
32
35
|
#
|
|
33
|
-
# include:
|
|
36
|
+
# include: # Only analyze files matching these patterns
|
|
34
37
|
# - "src/**/*.{ts,js,tsx,jsx}"
|
|
35
|
-
#
|
|
38
|
+
#
|
|
39
|
+
# exclude: # Skip files matching these patterns (takes precedence over include)
|
|
36
40
|
# - "**/*.test.ts"
|
|
37
|
-
# - "
|
|
41
|
+
# - "**/__tests__/**"
|
|
42
|
+
# - "**/node_modules/**"
|
|
38
43
|
`;
|
|
39
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Ask user a yes/no question. Returns true for yes (default), false for no.
|
|
47
|
+
*/
|
|
48
|
+
function askYesNo(question) {
|
|
49
|
+
const rl = createInterface({
|
|
50
|
+
input: process.stdin,
|
|
51
|
+
output: process.stdout,
|
|
52
|
+
});
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
rl.question(question, (answer) => {
|
|
55
|
+
rl.close();
|
|
56
|
+
// Default yes (empty answer or 'y' or 'yes')
|
|
57
|
+
const normalized = answer.toLowerCase().trim();
|
|
58
|
+
resolve(normalized !== 'n' && normalized !== 'no');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run grafema analyze in the given project path.
|
|
64
|
+
* Returns the exit code of the analyze process.
|
|
65
|
+
*/
|
|
66
|
+
function runAnalyze(projectPath) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const cliPath = join(__dirname, '..', 'cli.js');
|
|
69
|
+
const child = spawn('node', [cliPath, 'analyze', projectPath], {
|
|
70
|
+
stdio: 'inherit', // Pass through all I/O for user to see progress
|
|
71
|
+
});
|
|
72
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
73
|
+
child.on('error', () => resolve(1));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Print next steps after init.
|
|
78
|
+
*/
|
|
79
|
+
function printNextSteps() {
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log('Next steps:');
|
|
82
|
+
console.log(' 1. Review config: code .grafema/config.yaml');
|
|
83
|
+
console.log(' 2. Build graph: grafema analyze');
|
|
84
|
+
console.log(' 3. Explore: grafema overview');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if running in interactive mode.
|
|
88
|
+
* Interactive if stdin is TTY and --yes flag not provided.
|
|
89
|
+
*/
|
|
90
|
+
function isInteractive(options) {
|
|
91
|
+
return options.yes !== true && process.stdin.isTTY === true;
|
|
92
|
+
}
|
|
40
93
|
export const initCommand = new Command('init')
|
|
41
94
|
.description('Initialize Grafema in current project')
|
|
42
95
|
.argument('[path]', 'Project path', '.')
|
|
43
96
|
.option('-f, --force', 'Overwrite existing config')
|
|
97
|
+
.option('-y, --yes', 'Skip prompts (non-interactive mode)')
|
|
98
|
+
.addHelpText('after', `
|
|
99
|
+
Examples:
|
|
100
|
+
grafema init Initialize in current directory
|
|
101
|
+
grafema init ./my-project Initialize in specific directory
|
|
102
|
+
grafema init --force Overwrite existing configuration
|
|
103
|
+
grafema init --yes Skip prompts, auto-run analyze
|
|
104
|
+
`)
|
|
44
105
|
.action(async (path, options) => {
|
|
45
106
|
const projectPath = resolve(path);
|
|
46
107
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -49,10 +110,15 @@ export const initCommand = new Command('init')
|
|
|
49
110
|
const tsconfigPath = join(projectPath, 'tsconfig.json');
|
|
50
111
|
// Check package.json
|
|
51
112
|
if (!existsSync(packageJsonPath)) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
113
|
+
console.error('✗ Grafema currently supports JavaScript/TypeScript projects only.');
|
|
114
|
+
console.error(` No package.json found in ${projectPath}`);
|
|
115
|
+
console.error('');
|
|
116
|
+
console.error(' Supported: Node.js, React, Express, Next.js, Vue, Angular, etc.');
|
|
117
|
+
console.error(' Coming soon: Python, Go, Rust');
|
|
118
|
+
console.error('');
|
|
119
|
+
console.error(' If this IS a JS/TS project, create package.json first:');
|
|
120
|
+
console.error(' npm init -y');
|
|
121
|
+
process.exit(1);
|
|
56
122
|
}
|
|
57
123
|
console.log('✓ Found package.json');
|
|
58
124
|
// Detect TypeScript
|
|
@@ -68,8 +134,7 @@ export const initCommand = new Command('init')
|
|
|
68
134
|
console.log('');
|
|
69
135
|
console.log('✓ Grafema already initialized');
|
|
70
136
|
console.log(' → Use --force to overwrite config');
|
|
71
|
-
|
|
72
|
-
console.log('Next: Run "grafema analyze" to build the code graph');
|
|
137
|
+
printNextSteps();
|
|
73
138
|
return;
|
|
74
139
|
}
|
|
75
140
|
// Create .grafema directory
|
|
@@ -89,6 +154,19 @@ export const initCommand = new Command('init')
|
|
|
89
154
|
console.log('✓ Updated .gitignore');
|
|
90
155
|
}
|
|
91
156
|
}
|
|
92
|
-
|
|
93
|
-
|
|
157
|
+
printNextSteps();
|
|
158
|
+
// Prompt to run analyze in interactive mode
|
|
159
|
+
if (isInteractive(options)) {
|
|
160
|
+
console.log('');
|
|
161
|
+
const runNow = await askYesNo('Run analysis now? [Y/n] ');
|
|
162
|
+
if (runNow) {
|
|
163
|
+
console.log('');
|
|
164
|
+
console.log('Starting analysis...');
|
|
165
|
+
console.log('');
|
|
166
|
+
const exitCode = await runAnalyze(projectPath);
|
|
167
|
+
if (exitCode !== 0) {
|
|
168
|
+
process.exit(exitCode);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
94
172
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command - List nodes by type
|
|
3
|
+
*
|
|
4
|
+
* Unix-style listing of nodes in the graph. Similar to `ls` for files,
|
|
5
|
+
* but for code graph nodes.
|
|
6
|
+
*
|
|
7
|
+
* Use cases:
|
|
8
|
+
* - "Show me all HTTP routes in this project"
|
|
9
|
+
* - "List all functions" (with limit for large codebases)
|
|
10
|
+
* - "What Socket.IO events are defined?"
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
export declare const lsCommand: Command;
|
|
14
|
+
//# sourceMappingURL=ls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../src/commands/ls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BpC,eAAO,MAAM,SAAS,SA2FlB,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command - List nodes by type
|
|
3
|
+
*
|
|
4
|
+
* Unix-style listing of nodes in the graph. Similar to `ls` for files,
|
|
5
|
+
* but for code graph nodes.
|
|
6
|
+
*
|
|
7
|
+
* Use cases:
|
|
8
|
+
* - "Show me all HTTP routes in this project"
|
|
9
|
+
* - "List all functions" (with limit for large codebases)
|
|
10
|
+
* - "What Socket.IO events are defined?"
|
|
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 { exitWithError } from '../utils/errorFormatter.js';
|
|
17
|
+
export const lsCommand = new Command('ls')
|
|
18
|
+
.description('List nodes by type')
|
|
19
|
+
.requiredOption('-t, --type <nodeType>', 'Node type to list (required)')
|
|
20
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
21
|
+
.option('-j, --json', 'Output as JSON')
|
|
22
|
+
.option('-l, --limit <n>', 'Limit results (default: 50)', '50')
|
|
23
|
+
.addHelpText('after', `
|
|
24
|
+
Examples:
|
|
25
|
+
grafema ls --type FUNCTION List functions (up to 50)
|
|
26
|
+
grafema ls --type http:route List all HTTP routes
|
|
27
|
+
grafema ls --type http:request List all HTTP requests (fetch/axios)
|
|
28
|
+
grafema ls -t socketio:event List Socket.IO events
|
|
29
|
+
grafema ls --type CLASS -l 100 List up to 100 classes
|
|
30
|
+
grafema ls --type jsx:component --json Output as JSON
|
|
31
|
+
|
|
32
|
+
Discover available types:
|
|
33
|
+
grafema types List all types with counts
|
|
34
|
+
`)
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
const projectPath = resolve(options.project);
|
|
37
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
38
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
39
|
+
if (!existsSync(dbPath)) {
|
|
40
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
41
|
+
}
|
|
42
|
+
const backend = new RFDBServerBackend({ dbPath });
|
|
43
|
+
await backend.connect();
|
|
44
|
+
try {
|
|
45
|
+
const limit = parseInt(options.limit, 10);
|
|
46
|
+
const nodeType = options.type;
|
|
47
|
+
// Check if type exists in graph
|
|
48
|
+
const typeCounts = await backend.countNodesByType();
|
|
49
|
+
if (!typeCounts[nodeType]) {
|
|
50
|
+
const availableTypes = Object.keys(typeCounts).sort();
|
|
51
|
+
exitWithError(`No nodes of type "${nodeType}" found`, [
|
|
52
|
+
'Available types:',
|
|
53
|
+
...availableTypes.slice(0, 10).map(t => ` ${t}`),
|
|
54
|
+
availableTypes.length > 10 ? ` ... and ${availableTypes.length - 10} more` : '',
|
|
55
|
+
'',
|
|
56
|
+
'Run: grafema types to see all types with counts',
|
|
57
|
+
].filter(Boolean));
|
|
58
|
+
}
|
|
59
|
+
// Collect nodes
|
|
60
|
+
const nodes = [];
|
|
61
|
+
for await (const node of backend.queryNodes({ nodeType: nodeType })) {
|
|
62
|
+
nodes.push({
|
|
63
|
+
id: node.id,
|
|
64
|
+
type: node.type || nodeType,
|
|
65
|
+
name: node.name || '',
|
|
66
|
+
file: node.file || '',
|
|
67
|
+
line: node.line,
|
|
68
|
+
method: node.method,
|
|
69
|
+
path: node.path,
|
|
70
|
+
url: node.url,
|
|
71
|
+
event: node.event,
|
|
72
|
+
});
|
|
73
|
+
if (nodes.length >= limit)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const totalCount = typeCounts[nodeType];
|
|
77
|
+
const showing = nodes.length;
|
|
78
|
+
if (options.json) {
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
type: nodeType,
|
|
81
|
+
nodes,
|
|
82
|
+
showing,
|
|
83
|
+
total: totalCount,
|
|
84
|
+
}, null, 2));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(`[${nodeType}] (${showing}${showing < totalCount ? ` of ${totalCount}` : ''}):`);
|
|
88
|
+
console.log('');
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
const display = formatNodeForList(node, nodeType, projectPath);
|
|
91
|
+
console.log(` ${display}`);
|
|
92
|
+
}
|
|
93
|
+
if (showing < totalCount) {
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(` ... ${totalCount - showing} more. Use --limit ${totalCount} to see all.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await backend.close();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Format a node for list display based on its type.
|
|
105
|
+
* Different types show different fields.
|
|
106
|
+
*/
|
|
107
|
+
function formatNodeForList(node, nodeType, projectPath) {
|
|
108
|
+
const relFile = node.file ? relative(projectPath, node.file) : '';
|
|
109
|
+
const loc = node.line ? `${relFile}:${node.line}` : relFile;
|
|
110
|
+
// HTTP routes: METHOD PATH (location)
|
|
111
|
+
if (nodeType === 'http:route' && node.method && node.path) {
|
|
112
|
+
return `${node.method.padEnd(6)} ${node.path} (${loc})`;
|
|
113
|
+
}
|
|
114
|
+
// HTTP requests: METHOD URL (location)
|
|
115
|
+
if (nodeType === 'http:request') {
|
|
116
|
+
const method = (node.method || 'GET').padEnd(6);
|
|
117
|
+
const url = node.url || 'dynamic';
|
|
118
|
+
return `${method} ${url} (${loc})`;
|
|
119
|
+
}
|
|
120
|
+
// Socket.IO events: event_name
|
|
121
|
+
if (nodeType === 'socketio:event') {
|
|
122
|
+
return node.name || node.id;
|
|
123
|
+
}
|
|
124
|
+
// Socket.IO emit/on: event (location)
|
|
125
|
+
if (nodeType === 'socketio:emit' || nodeType === 'socketio:on') {
|
|
126
|
+
const event = node.event || node.name || 'unknown';
|
|
127
|
+
return `${event} (${loc})`;
|
|
128
|
+
}
|
|
129
|
+
// Default: name (location)
|
|
130
|
+
const name = node.name || node.id;
|
|
131
|
+
return loc ? `${name} (${loc})` : name;
|
|
132
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../src/commands/overview.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../src/commands/overview.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,eAAO,MAAM,eAAe,SAyGxB,CAAC"}
|
|
@@ -10,6 +10,12 @@ export const overviewCommand = new Command('overview')
|
|
|
10
10
|
.description('Show project overview and statistics')
|
|
11
11
|
.option('-p, --project <path>', 'Project path', '.')
|
|
12
12
|
.option('-j, --json', 'Output as JSON')
|
|
13
|
+
.addHelpText('after', `
|
|
14
|
+
Examples:
|
|
15
|
+
grafema overview Show project dashboard
|
|
16
|
+
grafema overview --json Output statistics as JSON
|
|
17
|
+
grafema overview -p ./app Overview for specific project
|
|
18
|
+
`)
|
|
13
19
|
.action(async (options) => {
|
|
14
20
|
const projectPath = resolve(options.project);
|
|
15
21
|
const grafemaDir = join(projectPath, '.grafema');
|
|
@@ -46,6 +52,7 @@ export const overviewCommand = new Command('overview')
|
|
|
46
52
|
console.log('External Interactions:');
|
|
47
53
|
const httpRoutes = stats.nodesByType['http:route'] || 0;
|
|
48
54
|
const dbQueries = stats.nodesByType['db:query'] || 0;
|
|
55
|
+
const socketEvents = stats.nodesByType['socketio:event'] || 0;
|
|
49
56
|
const socketEmit = stats.nodesByType['socketio:emit'] || 0;
|
|
50
57
|
const socketOn = stats.nodesByType['socketio:on'] || 0;
|
|
51
58
|
const events = stats.nodesByType['event:listener'] || 0;
|
|
@@ -53,15 +60,21 @@ export const overviewCommand = new Command('overview')
|
|
|
53
60
|
console.log(`├─ HTTP routes: ${httpRoutes}`);
|
|
54
61
|
if (dbQueries > 0)
|
|
55
62
|
console.log(`├─ Database queries: ${dbQueries}`);
|
|
56
|
-
if (
|
|
63
|
+
if (socketEvents > 0) {
|
|
64
|
+
// New format showing event count prominently
|
|
65
|
+
console.log(`├─ Socket.IO: ${socketEvents} events (${socketEmit} emit, ${socketOn} listeners)`);
|
|
66
|
+
}
|
|
67
|
+
else if (socketEmit + socketOn > 0) {
|
|
68
|
+
// Fallback for graphs analyzed before REG-209
|
|
57
69
|
console.log(`├─ Socket.IO: ${socketEmit} emit, ${socketOn} listeners`);
|
|
70
|
+
}
|
|
58
71
|
if (events > 0)
|
|
59
72
|
console.log(`├─ Event listeners: ${events}`);
|
|
60
73
|
// Check for external module refs
|
|
61
74
|
const externalModules = stats.nodesByType['EXTERNAL_MODULE'] || 0;
|
|
62
75
|
if (externalModules > 0)
|
|
63
76
|
console.log(`└─ External modules: ${externalModules}`);
|
|
64
|
-
if (httpRoutes + dbQueries + socketEmit + socketOn + events + externalModules === 0) {
|
|
77
|
+
if (httpRoutes + dbQueries + socketEvents + socketEmit + socketOn + events + externalModules === 0) {
|
|
65
78
|
console.log('└─ (none detected)');
|
|
66
79
|
}
|
|
67
80
|
console.log('');
|
package/dist/commands/query.d.ts
CHANGED
|
@@ -9,5 +9,103 @@
|
|
|
9
9
|
* For raw Datalog queries, use --raw flag
|
|
10
10
|
*/
|
|
11
11
|
import { Command } from 'commander';
|
|
12
|
+
/**
|
|
13
|
+
* Parsed query with optional scope constraints.
|
|
14
|
+
*
|
|
15
|
+
* Supports patterns like:
|
|
16
|
+
* "response" -> { name: "response" }
|
|
17
|
+
* "variable response" -> { type: "VARIABLE", name: "response" }
|
|
18
|
+
* "response in fetchData" -> { name: "response", scopes: ["fetchData"] }
|
|
19
|
+
* "response in src/app.ts" -> { name: "response", file: "src/app.ts" }
|
|
20
|
+
* "response in catch in fetchData" -> { name: "response", scopes: ["fetchData", "catch"] }
|
|
21
|
+
*/
|
|
22
|
+
export interface ParsedQuery {
|
|
23
|
+
/** Node type (e.g., "FUNCTION", "VARIABLE") or null for any */
|
|
24
|
+
type: string | null;
|
|
25
|
+
/** Node name to search (partial match) */
|
|
26
|
+
name: string;
|
|
27
|
+
/** File scope - filter to nodes in this file */
|
|
28
|
+
file: string | null;
|
|
29
|
+
/** Scope chain - filter to nodes inside these scopes (function/class/block names) */
|
|
30
|
+
scopes: string[];
|
|
31
|
+
}
|
|
12
32
|
export declare const queryCommand: Command;
|
|
33
|
+
/**
|
|
34
|
+
* Parse search pattern with scope support.
|
|
35
|
+
*
|
|
36
|
+
* Grammar:
|
|
37
|
+
* query := [type] name [" in " scope]*
|
|
38
|
+
* type := "function" | "class" | "variable" | etc.
|
|
39
|
+
* scope := <filename> | <functionName>
|
|
40
|
+
*
|
|
41
|
+
* File scope detection: contains "/" or ends with .ts/.js/.tsx/.jsx
|
|
42
|
+
* Function scope detection: anything else
|
|
43
|
+
*
|
|
44
|
+
* IMPORTANT: Only split on " in " (space-padded) to avoid matching names like "signin"
|
|
45
|
+
*
|
|
46
|
+
* Examples:
|
|
47
|
+
* "response" -> { type: null, name: "response", file: null, scopes: [] }
|
|
48
|
+
* "variable response in fetchData" -> { type: "VARIABLE", name: "response", file: null, scopes: ["fetchData"] }
|
|
49
|
+
* "response in src/app.ts" -> { type: null, name: "response", file: "src/app.ts", scopes: [] }
|
|
50
|
+
* "error in catch in fetchData in src/app.ts" -> { type: null, name: "error", file: "src/app.ts", scopes: ["fetchData", "catch"] }
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseQuery(pattern: string): ParsedQuery;
|
|
53
|
+
/**
|
|
54
|
+
* Detect if a scope string looks like a file path.
|
|
55
|
+
*
|
|
56
|
+
* Heuristics:
|
|
57
|
+
* - Contains "/" -> file path
|
|
58
|
+
* - Ends with .ts, .js, .tsx, .jsx, .mjs, .cjs -> file path
|
|
59
|
+
*
|
|
60
|
+
* Examples:
|
|
61
|
+
* "src/app.ts" -> true
|
|
62
|
+
* "app.js" -> true
|
|
63
|
+
* "fetchData" -> false
|
|
64
|
+
* "UserService" -> false
|
|
65
|
+
* "catch" -> false
|
|
66
|
+
*/
|
|
67
|
+
export declare function isFileScope(scope: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a semantic ID matches the given scope constraints.
|
|
70
|
+
*
|
|
71
|
+
* Uses parseSemanticId from @grafema/core for robust ID parsing.
|
|
72
|
+
*
|
|
73
|
+
* Scope matching rules:
|
|
74
|
+
* - File scope: semantic ID must match the file path (full or basename)
|
|
75
|
+
* - Function/class scope: semantic ID must contain the scope in its scopePath
|
|
76
|
+
* - Multiple scopes: ALL must match (AND logic)
|
|
77
|
+
* - Scope order: independent - all scopes just need to be present
|
|
78
|
+
*
|
|
79
|
+
* Examples:
|
|
80
|
+
* ID: "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
81
|
+
* Matches: scopes=["fetchData"] -> true
|
|
82
|
+
* Matches: scopes=["try"] -> true (matches "try#0")
|
|
83
|
+
* Matches: scopes=["fetchData", "try"] -> true (both present)
|
|
84
|
+
* Matches: scopes=["processData"] -> false (not in ID)
|
|
85
|
+
*
|
|
86
|
+
* @param semanticId - The full semantic ID to check
|
|
87
|
+
* @param file - File scope (null for any file)
|
|
88
|
+
* @param scopes - Array of scope names to match
|
|
89
|
+
* @returns true if ID matches all constraints
|
|
90
|
+
*/
|
|
91
|
+
export declare function matchesScope(semanticId: string, file: string | null, scopes: string[]): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Extract human-readable scope context from a semantic ID.
|
|
94
|
+
*
|
|
95
|
+
* Parses the ID and returns a description of the scope chain.
|
|
96
|
+
*
|
|
97
|
+
* Examples:
|
|
98
|
+
* "src/app.ts->fetchData->try#0->VARIABLE->response"
|
|
99
|
+
* -> "inside fetchData, inside try block"
|
|
100
|
+
*
|
|
101
|
+
* "src/app.ts->UserService->login->VARIABLE->token"
|
|
102
|
+
* -> "inside UserService, inside login"
|
|
103
|
+
*
|
|
104
|
+
* "src/app.ts->global->FUNCTION->main"
|
|
105
|
+
* -> null (no interesting scope)
|
|
106
|
+
*
|
|
107
|
+
* @param semanticId - The semantic ID to parse
|
|
108
|
+
* @returns Human-readable scope context or null
|
|
109
|
+
*/
|
|
110
|
+
export declare function extractScopeContext(semanticId: string): string | null;
|
|
13
111
|
//# sourceMappingURL=query.d.ts.map
|