@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.
Files changed (66) hide show
  1. package/LICENSE +190 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.d.ts.map +1 -0
  7. package/dist/commands/analyze.js +209 -0
  8. package/dist/commands/check.d.ts +10 -0
  9. package/dist/commands/check.d.ts.map +1 -0
  10. package/dist/commands/check.js +295 -0
  11. package/dist/commands/coverage.d.ts +11 -0
  12. package/dist/commands/coverage.d.ts.map +1 -0
  13. package/dist/commands/coverage.js +96 -0
  14. package/dist/commands/explore.d.ts +6 -0
  15. package/dist/commands/explore.d.ts.map +1 -0
  16. package/dist/commands/explore.js +633 -0
  17. package/dist/commands/get.d.ts +10 -0
  18. package/dist/commands/get.d.ts.map +1 -0
  19. package/dist/commands/get.js +189 -0
  20. package/dist/commands/impact.d.ts +10 -0
  21. package/dist/commands/impact.d.ts.map +1 -0
  22. package/dist/commands/impact.js +313 -0
  23. package/dist/commands/init.d.ts +6 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +94 -0
  26. package/dist/commands/overview.d.ts +6 -0
  27. package/dist/commands/overview.d.ts.map +1 -0
  28. package/dist/commands/overview.js +91 -0
  29. package/dist/commands/query.d.ts +13 -0
  30. package/dist/commands/query.d.ts.map +1 -0
  31. package/dist/commands/query.js +340 -0
  32. package/dist/commands/server.d.ts +11 -0
  33. package/dist/commands/server.d.ts.map +1 -0
  34. package/dist/commands/server.js +300 -0
  35. package/dist/commands/stats.d.ts +6 -0
  36. package/dist/commands/stats.d.ts.map +1 -0
  37. package/dist/commands/stats.js +52 -0
  38. package/dist/commands/trace.d.ts +10 -0
  39. package/dist/commands/trace.d.ts.map +1 -0
  40. package/dist/commands/trace.js +270 -0
  41. package/dist/utils/codePreview.d.ts +28 -0
  42. package/dist/utils/codePreview.d.ts.map +1 -0
  43. package/dist/utils/codePreview.js +51 -0
  44. package/dist/utils/errorFormatter.d.ts +24 -0
  45. package/dist/utils/errorFormatter.d.ts.map +1 -0
  46. package/dist/utils/errorFormatter.js +32 -0
  47. package/dist/utils/formatNode.d.ts +53 -0
  48. package/dist/utils/formatNode.d.ts.map +1 -0
  49. package/dist/utils/formatNode.js +49 -0
  50. package/package.json +54 -0
  51. package/src/cli.ts +41 -0
  52. package/src/commands/analyze.ts +271 -0
  53. package/src/commands/check.ts +379 -0
  54. package/src/commands/coverage.ts +108 -0
  55. package/src/commands/explore.tsx +1056 -0
  56. package/src/commands/get.ts +265 -0
  57. package/src/commands/impact.ts +400 -0
  58. package/src/commands/init.ts +112 -0
  59. package/src/commands/overview.ts +108 -0
  60. package/src/commands/query.ts +425 -0
  61. package/src/commands/server.ts +335 -0
  62. package/src/commands/stats.ts +58 -0
  63. package/src/commands/trace.ts +341 -0
  64. package/src/utils/codePreview.ts +77 -0
  65. package/src/utils/errorFormatter.ts +35 -0
  66. package/src/utils/formatNode.ts +88 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Overview command - Project dashboard
3
+ */
4
+ import { Command } from 'commander';
5
+ import { resolve, join } from 'path';
6
+ import { existsSync } from 'fs';
7
+ import { RFDBServerBackend } from '@grafema/core';
8
+ import { exitWithError } from '../utils/errorFormatter.js';
9
+ export const overviewCommand = new Command('overview')
10
+ .description('Show project overview and statistics')
11
+ .option('-p, --project <path>', 'Project path', '.')
12
+ .option('-j, --json', 'Output as JSON')
13
+ .action(async (options) => {
14
+ const projectPath = resolve(options.project);
15
+ const grafemaDir = join(projectPath, '.grafema');
16
+ const dbPath = join(grafemaDir, 'graph.rfdb');
17
+ if (!existsSync(dbPath)) {
18
+ exitWithError('No graph database found', ['Run: grafema analyze']);
19
+ }
20
+ const backend = new RFDBServerBackend({ dbPath });
21
+ await backend.connect();
22
+ try {
23
+ const stats = await backend.getStats();
24
+ if (options.json) {
25
+ console.log(JSON.stringify(stats, null, 2));
26
+ return;
27
+ }
28
+ // Header
29
+ console.log('');
30
+ console.log('📊 Project Overview');
31
+ console.log('');
32
+ // Code Structure
33
+ console.log('Code Structure:');
34
+ const modules = stats.nodesByType['MODULE'] || 0;
35
+ const functions = stats.nodesByType['FUNCTION'] || 0;
36
+ const classes = stats.nodesByType['CLASS'] || 0;
37
+ const variables = stats.nodesByType['VARIABLE'] || 0;
38
+ const calls = stats.nodesByType['CALL'] || 0;
39
+ console.log(`├─ Modules: ${modules}`);
40
+ console.log(`├─ Functions: ${functions}`);
41
+ console.log(`├─ Classes: ${classes}`);
42
+ console.log(`├─ Variables: ${variables}`);
43
+ console.log(`└─ Call sites: ${calls}`);
44
+ console.log('');
45
+ // External Interactions (namespaced types)
46
+ console.log('External Interactions:');
47
+ const httpRoutes = stats.nodesByType['http:route'] || 0;
48
+ const dbQueries = stats.nodesByType['db:query'] || 0;
49
+ const socketEmit = stats.nodesByType['socketio:emit'] || 0;
50
+ const socketOn = stats.nodesByType['socketio:on'] || 0;
51
+ const events = stats.nodesByType['event:listener'] || 0;
52
+ if (httpRoutes > 0)
53
+ console.log(`├─ HTTP routes: ${httpRoutes}`);
54
+ if (dbQueries > 0)
55
+ console.log(`├─ Database queries: ${dbQueries}`);
56
+ if (socketEmit + socketOn > 0)
57
+ console.log(`├─ Socket.IO: ${socketEmit} emit, ${socketOn} listeners`);
58
+ if (events > 0)
59
+ console.log(`├─ Event listeners: ${events}`);
60
+ // Check for external module refs
61
+ const externalModules = stats.nodesByType['EXTERNAL_MODULE'] || 0;
62
+ if (externalModules > 0)
63
+ console.log(`└─ External modules: ${externalModules}`);
64
+ if (httpRoutes + dbQueries + socketEmit + socketOn + events + externalModules === 0) {
65
+ console.log('└─ (none detected)');
66
+ }
67
+ console.log('');
68
+ // Graph Statistics
69
+ console.log('Graph Statistics:');
70
+ console.log(`├─ Total nodes: ${stats.nodeCount}`);
71
+ console.log(`├─ Total edges: ${stats.edgeCount}`);
72
+ // Show edge breakdown
73
+ const callEdges = stats.edgesByType['CALLS'] || 0;
74
+ const containsEdges = stats.edgesByType['CONTAINS'] || 0;
75
+ const importsEdges = stats.edgesByType['IMPORTS'] || 0;
76
+ console.log(`├─ Calls: ${callEdges}`);
77
+ console.log(`├─ Contains: ${containsEdges}`);
78
+ console.log(`└─ Imports: ${importsEdges}`);
79
+ console.log('');
80
+ // Find most called functions (via incoming CALLS edges)
81
+ // This requires a query - simplified for now
82
+ console.log('Next steps:');
83
+ console.log('→ grafema query "function <name>" Search for a function');
84
+ console.log('→ grafema trace "<var> from <fn>" Trace data flow');
85
+ console.log('→ grafema impact "<target>" Analyze change impact');
86
+ console.log('→ grafema explore Interactive navigation');
87
+ }
88
+ finally {
89
+ await backend.close();
90
+ }
91
+ });
@@ -0,0 +1,13 @@
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
+ import { Command } from 'commander';
12
+ export declare const queryCommand: Command;
13
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/commands/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,eAAO,MAAM,YAAY,SAyFrB,CAAC"}
@@ -0,0 +1,340 @@
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
+ import { Command } from 'commander';
12
+ import { resolve, join } from 'path';
13
+ import { existsSync } from 'fs';
14
+ import { RFDBServerBackend } from '@grafema/core';
15
+ import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
16
+ import { exitWithError } from '../utils/errorFormatter.js';
17
+ export const queryCommand = new Command('query')
18
+ .description('Search the code graph')
19
+ .argument('<pattern>', 'Search pattern: "function X", "class Y", or just "X"')
20
+ .option('-p, --project <path>', 'Project path', '.')
21
+ .option('-j, --json', 'Output as JSON')
22
+ .option('-l, --limit <n>', 'Limit results', '10')
23
+ .option('--raw', 'Execute raw Datalog query')
24
+ .action(async (pattern, options) => {
25
+ const projectPath = resolve(options.project);
26
+ const grafemaDir = join(projectPath, '.grafema');
27
+ const dbPath = join(grafemaDir, 'graph.rfdb');
28
+ if (!existsSync(dbPath)) {
29
+ exitWithError('No graph database found', ['Run: grafema analyze']);
30
+ }
31
+ const backend = new RFDBServerBackend({ dbPath });
32
+ await backend.connect();
33
+ try {
34
+ // Raw Datalog mode
35
+ if (options.raw) {
36
+ await executeRawQuery(backend, pattern, options);
37
+ return;
38
+ }
39
+ // Parse pattern
40
+ const { type, name } = parsePattern(pattern);
41
+ const limit = parseInt(options.limit, 10);
42
+ // Find matching nodes
43
+ const nodes = await findNodes(backend, type, name, limit);
44
+ if (nodes.length === 0) {
45
+ console.log(`No results for "${pattern}"`);
46
+ if (type) {
47
+ console.log(` → Try: grafema query "${name}" (search all types)`);
48
+ }
49
+ return;
50
+ }
51
+ if (options.json) {
52
+ const results = await Promise.all(nodes.map(async (node) => ({
53
+ ...node,
54
+ calledBy: await getCallers(backend, node.id, 5),
55
+ calls: await getCallees(backend, node.id, 5),
56
+ })));
57
+ console.log(JSON.stringify(results, null, 2));
58
+ return;
59
+ }
60
+ // Display results
61
+ for (const node of nodes) {
62
+ console.log('');
63
+ displayNode(node, projectPath);
64
+ // Show callers and callees for functions
65
+ if (node.type === 'FUNCTION' || node.type === 'CLASS') {
66
+ const callers = await getCallers(backend, node.id, 5);
67
+ const callees = await getCallees(backend, node.id, 5);
68
+ if (callers.length > 0) {
69
+ console.log('');
70
+ console.log(`Called by (${callers.length}${callers.length >= 5 ? '+' : ''}):`);
71
+ for (const caller of callers) {
72
+ console.log(` <- ${formatNodeInline(caller)}`);
73
+ }
74
+ }
75
+ if (callees.length > 0) {
76
+ console.log('');
77
+ console.log(`Calls (${callees.length}${callees.length >= 5 ? '+' : ''}):`);
78
+ for (const callee of callees) {
79
+ console.log(` -> ${formatNodeInline(callee)}`);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ if (nodes.length > 1) {
85
+ console.log('');
86
+ console.log(`Found ${nodes.length} results. Use more specific pattern to narrow.`);
87
+ }
88
+ }
89
+ finally {
90
+ await backend.close();
91
+ }
92
+ });
93
+ /**
94
+ * Parse search pattern like "function authenticate" or just "authenticate"
95
+ */
96
+ function parsePattern(pattern) {
97
+ const words = pattern.trim().split(/\s+/);
98
+ if (words.length >= 2) {
99
+ const typeWord = words[0].toLowerCase();
100
+ const name = words.slice(1).join(' ');
101
+ const typeMap = {
102
+ function: 'FUNCTION',
103
+ fn: 'FUNCTION',
104
+ func: 'FUNCTION',
105
+ class: 'CLASS',
106
+ module: 'MODULE',
107
+ variable: 'VARIABLE',
108
+ var: 'VARIABLE',
109
+ const: 'CONSTANT',
110
+ constant: 'CONSTANT',
111
+ };
112
+ if (typeMap[typeWord]) {
113
+ return { type: typeMap[typeWord], name };
114
+ }
115
+ }
116
+ return { type: null, name: pattern.trim() };
117
+ }
118
+ /**
119
+ * Find nodes by type and name
120
+ */
121
+ async function findNodes(backend, type, name, limit) {
122
+ const results = [];
123
+ const searchTypes = type
124
+ ? [type]
125
+ : ['FUNCTION', 'CLASS', 'MODULE', 'VARIABLE', 'CONSTANT'];
126
+ for (const nodeType of searchTypes) {
127
+ for await (const node of backend.queryNodes({ nodeType: nodeType })) {
128
+ const nodeName = node.name || '';
129
+ // Case-insensitive partial match
130
+ if (nodeName.toLowerCase().includes(name.toLowerCase())) {
131
+ results.push({
132
+ id: node.id,
133
+ type: node.type || nodeType,
134
+ name: nodeName,
135
+ file: node.file || '',
136
+ line: node.line,
137
+ });
138
+ if (results.length >= limit)
139
+ break;
140
+ }
141
+ }
142
+ if (results.length >= limit)
143
+ break;
144
+ }
145
+ return results;
146
+ }
147
+ /**
148
+ * Get functions that call this node
149
+ *
150
+ * Logic: FUNCTION ← CONTAINS ← CALL → CALLS → TARGET
151
+ * We need to find CALL nodes that CALLS this target,
152
+ * then find the FUNCTION that CONTAINS each CALL
153
+ */
154
+ async function getCallers(backend, nodeId, limit) {
155
+ const callers = [];
156
+ const seen = new Set();
157
+ try {
158
+ // Find CALL nodes that call this target
159
+ const callEdges = await backend.getIncomingEdges(nodeId, ['CALLS']);
160
+ for (const edge of callEdges) {
161
+ if (callers.length >= limit)
162
+ break;
163
+ const callNode = await backend.getNode(edge.src);
164
+ if (!callNode)
165
+ continue;
166
+ // Find the FUNCTION that contains this CALL
167
+ const containingFunc = await findContainingFunction(backend, callNode.id);
168
+ if (containingFunc && !seen.has(containingFunc.id)) {
169
+ seen.add(containingFunc.id);
170
+ callers.push({
171
+ id: containingFunc.id,
172
+ type: containingFunc.type || 'FUNCTION',
173
+ name: containingFunc.name || '<anonymous>',
174
+ file: containingFunc.file || '',
175
+ line: containingFunc.line,
176
+ });
177
+ }
178
+ }
179
+ }
180
+ catch {
181
+ // Ignore errors
182
+ }
183
+ return callers;
184
+ }
185
+ /**
186
+ * Find the FUNCTION or CLASS that contains a node
187
+ *
188
+ * Path can be: CALL → CONTAINS → SCOPE → CONTAINS → SCOPE → HAS_SCOPE → FUNCTION
189
+ * So we need to follow both CONTAINS and HAS_SCOPE edges
190
+ */
191
+ async function findContainingFunction(backend, nodeId, maxDepth = 15) {
192
+ const visited = new Set();
193
+ const queue = [{ id: nodeId, depth: 0 }];
194
+ while (queue.length > 0) {
195
+ const { id, depth } = queue.shift();
196
+ if (visited.has(id) || depth > maxDepth)
197
+ continue;
198
+ visited.add(id);
199
+ try {
200
+ // Get incoming edges: CONTAINS, HAS_SCOPE, and DECLARES (for variables in functions)
201
+ const edges = await backend.getIncomingEdges(id, null);
202
+ for (const edge of edges) {
203
+ const edgeType = edge.edgeType || edge.type;
204
+ // Only follow structural edges
205
+ if (!['CONTAINS', 'HAS_SCOPE', 'DECLARES'].includes(edgeType))
206
+ continue;
207
+ const parentNode = await backend.getNode(edge.src);
208
+ if (!parentNode || visited.has(parentNode.id))
209
+ continue;
210
+ const parentType = parentNode.type;
211
+ // FUNCTION, CLASS, or MODULE (for top-level calls)
212
+ if (parentType === 'FUNCTION' || parentType === 'CLASS' || parentType === 'MODULE') {
213
+ return {
214
+ id: parentNode.id,
215
+ type: parentType,
216
+ name: parentNode.name || '<anonymous>',
217
+ file: parentNode.file || '',
218
+ line: parentNode.line,
219
+ };
220
+ }
221
+ // Continue searching from this parent
222
+ queue.push({ id: parentNode.id, depth: depth + 1 });
223
+ }
224
+ }
225
+ catch {
226
+ // Ignore errors
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+ /**
232
+ * Get functions that this node calls
233
+ *
234
+ * Logic: FUNCTION → CONTAINS → CALL → CALLS → TARGET
235
+ * Find all CALL nodes inside this function, then find what they call
236
+ */
237
+ async function getCallees(backend, nodeId, limit) {
238
+ const callees = [];
239
+ const seen = new Set();
240
+ try {
241
+ // Find all CALL nodes inside this function (via CONTAINS)
242
+ const callNodes = await findCallsInFunction(backend, nodeId);
243
+ for (const callNode of callNodes) {
244
+ if (callees.length >= limit)
245
+ break;
246
+ // Find what this CALL calls
247
+ const callEdges = await backend.getOutgoingEdges(callNode.id, ['CALLS']);
248
+ for (const edge of callEdges) {
249
+ if (callees.length >= limit)
250
+ break;
251
+ const targetNode = await backend.getNode(edge.dst);
252
+ if (!targetNode || seen.has(targetNode.id))
253
+ continue;
254
+ seen.add(targetNode.id);
255
+ callees.push({
256
+ id: targetNode.id,
257
+ type: targetNode.type || 'UNKNOWN',
258
+ name: targetNode.name || '<anonymous>',
259
+ file: targetNode.file || '',
260
+ line: targetNode.line,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ catch {
266
+ // Ignore errors
267
+ }
268
+ return callees;
269
+ }
270
+ /**
271
+ * Find all CALL nodes inside a function (recursively via CONTAINS)
272
+ */
273
+ async function findCallsInFunction(backend, nodeId, maxDepth = 10) {
274
+ const calls = [];
275
+ const visited = new Set();
276
+ const queue = [{ id: nodeId, depth: 0 }];
277
+ while (queue.length > 0) {
278
+ const { id, depth } = queue.shift();
279
+ if (visited.has(id) || depth > maxDepth)
280
+ continue;
281
+ visited.add(id);
282
+ try {
283
+ // Get children via CONTAINS
284
+ const edges = await backend.getOutgoingEdges(id, ['CONTAINS']);
285
+ for (const edge of edges) {
286
+ const child = await backend.getNode(edge.dst);
287
+ if (!child)
288
+ continue;
289
+ const childType = child.type;
290
+ if (childType === 'CALL') {
291
+ calls.push({
292
+ id: child.id,
293
+ type: 'CALL',
294
+ name: child.name || '',
295
+ file: child.file || '',
296
+ line: child.line,
297
+ });
298
+ }
299
+ // Continue searching in children (but not into nested functions)
300
+ if (childType !== 'FUNCTION' && childType !== 'CLASS') {
301
+ queue.push({ id: child.id, depth: depth + 1 });
302
+ }
303
+ }
304
+ }
305
+ catch {
306
+ // Ignore
307
+ }
308
+ }
309
+ return calls;
310
+ }
311
+ /**
312
+ * Display a node with semantic ID as primary identifier
313
+ */
314
+ function displayNode(node, projectPath) {
315
+ console.log(formatNodeDisplay(node, { projectPath }));
316
+ }
317
+ /**
318
+ * Execute raw Datalog query (backwards compat)
319
+ */
320
+ async function executeRawQuery(backend, query, options) {
321
+ const results = await backend.datalogQuery(query);
322
+ const limit = parseInt(options.limit, 10);
323
+ const limited = results.slice(0, limit);
324
+ if (options.json) {
325
+ console.log(JSON.stringify(limited, null, 2));
326
+ }
327
+ else {
328
+ if (limited.length === 0) {
329
+ console.log('No results.');
330
+ }
331
+ else {
332
+ console.log(`Results (${limited.length}${results.length > limit ? ` of ${results.length}` : ''}):`);
333
+ console.log('');
334
+ for (const result of limited) {
335
+ const bindings = result.bindings.map((b) => `${b.name}=${b.value}`).join(', ');
336
+ console.log(` { ${bindings} }`);
337
+ }
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Server command - Manage RFDB server lifecycle
3
+ *
4
+ * Provides explicit control over the RFDB server process:
5
+ * grafema server start - Start detached server
6
+ * grafema server stop - Stop server gracefully
7
+ * grafema server status - Check if server is running
8
+ */
9
+ import { Command } from 'commander';
10
+ export declare const serverCommand: Command;
11
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+FpC,eAAO,MAAM,aAAa,SACoB,CAAC"}