@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,341 @@
1
+ /**
2
+ * Trace command - Data flow analysis
3
+ *
4
+ * Usage:
5
+ * grafema trace "userId from authenticate"
6
+ * grafema trace "config"
7
+ */
8
+
9
+ import { Command } from 'commander';
10
+ import { resolve, join } from 'path';
11
+ import { existsSync } from 'fs';
12
+ import { RFDBServerBackend, parseSemanticId } from '@grafema/core';
13
+ import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
14
+ import { exitWithError } from '../utils/errorFormatter.js';
15
+
16
+ interface TraceOptions {
17
+ project: string;
18
+ json?: boolean;
19
+ depth: string;
20
+ }
21
+
22
+ interface NodeInfo {
23
+ id: string;
24
+ type: string;
25
+ name: string;
26
+ file: string;
27
+ line?: number;
28
+ value?: unknown;
29
+ }
30
+
31
+ interface TraceStep {
32
+ node: NodeInfo;
33
+ edgeType: string;
34
+ depth: number;
35
+ }
36
+
37
+ export const traceCommand = new Command('trace')
38
+ .description('Trace data flow for a variable')
39
+ .argument('<pattern>', 'Pattern: "varName from functionName" or just "varName"')
40
+ .option('-p, --project <path>', 'Project path', '.')
41
+ .option('-j, --json', 'Output as JSON')
42
+ .option('-d, --depth <n>', 'Max trace depth', '10')
43
+ .action(async (pattern: string, options: TraceOptions) => {
44
+ const projectPath = resolve(options.project);
45
+ const grafemaDir = join(projectPath, '.grafema');
46
+ const dbPath = join(grafemaDir, 'graph.rfdb');
47
+
48
+ if (!existsSync(dbPath)) {
49
+ exitWithError('No graph database found', ['Run: grafema analyze']);
50
+ }
51
+
52
+ const backend = new RFDBServerBackend({ dbPath });
53
+ await backend.connect();
54
+
55
+ try {
56
+ // Parse pattern: "varName from functionName" or just "varName"
57
+ const { varName, scopeName } = parseTracePattern(pattern);
58
+ const maxDepth = parseInt(options.depth, 10);
59
+
60
+ console.log(`Tracing ${varName}${scopeName ? ` from ${scopeName}` : ''}...`);
61
+ console.log('');
62
+
63
+ // Find starting variable(s)
64
+ const variables = await findVariables(backend, varName, scopeName);
65
+
66
+ if (variables.length === 0) {
67
+ console.log(`No variable "${varName}" found${scopeName ? ` in ${scopeName}` : ''}`);
68
+ return;
69
+ }
70
+
71
+ // Trace each variable
72
+ for (const variable of variables) {
73
+ console.log(formatNodeDisplay(variable, { projectPath }));
74
+ console.log('');
75
+
76
+ // Trace backwards through ASSIGNED_FROM
77
+ const backwardTrace = await traceBackward(backend, variable.id, maxDepth);
78
+
79
+ if (backwardTrace.length > 0) {
80
+ console.log('Data sources (where value comes from):');
81
+ displayTrace(backwardTrace, projectPath, ' ');
82
+ }
83
+
84
+ // Trace forward through ASSIGNED_FROM (where this value flows to)
85
+ const forwardTrace = await traceForward(backend, variable.id, maxDepth);
86
+
87
+ if (forwardTrace.length > 0) {
88
+ console.log('');
89
+ console.log('Data sinks (where value flows to):');
90
+ displayTrace(forwardTrace, projectPath, ' ');
91
+ }
92
+
93
+ // Show value domain if available
94
+ const sources = await getValueSources(backend, variable.id);
95
+ if (sources.length > 0) {
96
+ console.log('');
97
+ console.log('Possible values:');
98
+ for (const src of sources) {
99
+ if (src.type === 'LITERAL' && src.value !== undefined) {
100
+ console.log(` • ${JSON.stringify(src.value)} (literal)`);
101
+ } else if (src.type === 'PARAMETER') {
102
+ console.log(` • <parameter ${src.name}> (runtime input)`);
103
+ } else if (src.type === 'CALL') {
104
+ console.log(` • <return from ${src.name || 'call'}> (computed)`);
105
+ } else {
106
+ console.log(` • <${src.type.toLowerCase()}> ${src.name || ''}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ if (variables.length > 1) {
112
+ console.log('');
113
+ console.log('---');
114
+ }
115
+ }
116
+
117
+ if (options.json) {
118
+ // TODO: structured JSON output
119
+ }
120
+
121
+ } finally {
122
+ await backend.close();
123
+ }
124
+ });
125
+
126
+ /**
127
+ * Parse trace pattern
128
+ */
129
+ function parseTracePattern(pattern: string): { varName: string; scopeName: string | null } {
130
+ const fromMatch = pattern.match(/^(.+?)\s+from\s+(.+)$/i);
131
+ if (fromMatch) {
132
+ return { varName: fromMatch[1].trim(), scopeName: fromMatch[2].trim() };
133
+ }
134
+ return { varName: pattern.trim(), scopeName: null };
135
+ }
136
+
137
+ /**
138
+ * Find variables by name, optionally scoped to a function
139
+ */
140
+ async function findVariables(
141
+ backend: RFDBServerBackend,
142
+ varName: string,
143
+ scopeName: string | null
144
+ ): Promise<NodeInfo[]> {
145
+ const results: NodeInfo[] = [];
146
+ const lowerScopeName = scopeName ? scopeName.toLowerCase() : null;
147
+
148
+ // Search VARIABLE, CONSTANT, PARAMETER
149
+ for (const nodeType of ['VARIABLE', 'CONSTANT', 'PARAMETER']) {
150
+ for await (const node of backend.queryNodes({ nodeType: nodeType as any })) {
151
+ const name = node.name || '';
152
+ if (name.toLowerCase() === varName.toLowerCase()) {
153
+ // If scope specified, check if variable is in that scope
154
+ if (scopeName) {
155
+ const parsed = parseSemanticId(node.id);
156
+ if (!parsed) continue; // Skip nodes with invalid IDs
157
+
158
+ // Check if scopeName appears anywhere in the scope chain
159
+ if (!parsed.scopePath.some(s => s.toLowerCase() === lowerScopeName)) {
160
+ continue;
161
+ }
162
+ }
163
+
164
+ results.push({
165
+ id: node.id,
166
+ type: node.type || nodeType,
167
+ name: name,
168
+ file: node.file || '',
169
+ line: node.line,
170
+ });
171
+
172
+ if (results.length >= 5) break;
173
+ }
174
+ }
175
+ if (results.length >= 5) break;
176
+ }
177
+
178
+ return results;
179
+ }
180
+
181
+ /**
182
+ * Trace backward through ASSIGNED_FROM edges
183
+ */
184
+ async function traceBackward(
185
+ backend: RFDBServerBackend,
186
+ startId: string,
187
+ maxDepth: number
188
+ ): Promise<TraceStep[]> {
189
+ const trace: TraceStep[] = [];
190
+ const visited = new Set<string>();
191
+ const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
192
+
193
+ while (queue.length > 0) {
194
+ const { id, depth } = queue.shift()!;
195
+
196
+ if (visited.has(id) || depth > maxDepth) continue;
197
+ visited.add(id);
198
+
199
+ try {
200
+ const edges = await backend.getOutgoingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
201
+
202
+ for (const edge of edges) {
203
+ const targetNode = await backend.getNode(edge.dst);
204
+ if (!targetNode) continue;
205
+
206
+ const nodeInfo: NodeInfo = {
207
+ id: targetNode.id,
208
+ type: targetNode.type || 'UNKNOWN',
209
+ name: targetNode.name || '',
210
+ file: targetNode.file || '',
211
+ line: targetNode.line,
212
+ value: targetNode.value,
213
+ };
214
+
215
+ trace.push({
216
+ node: nodeInfo,
217
+ edgeType: edge.edgeType || edge.type,
218
+ depth: depth + 1,
219
+ });
220
+
221
+ // Continue tracing unless we hit a leaf
222
+ const leafTypes = ['LITERAL', 'PARAMETER', 'EXTERNAL_MODULE'];
223
+ if (!leafTypes.includes(nodeInfo.type)) {
224
+ queue.push({ id: targetNode.id, depth: depth + 1 });
225
+ }
226
+ }
227
+ } catch {
228
+ // Ignore errors
229
+ }
230
+ }
231
+
232
+ return trace;
233
+ }
234
+
235
+ /**
236
+ * Trace forward - find what uses this variable
237
+ */
238
+ async function traceForward(
239
+ backend: RFDBServerBackend,
240
+ startId: string,
241
+ maxDepth: number
242
+ ): Promise<TraceStep[]> {
243
+ const trace: TraceStep[] = [];
244
+ const visited = new Set<string>();
245
+ const queue: Array<{ id: string; depth: number }> = [{ id: startId, depth: 0 }];
246
+
247
+ while (queue.length > 0) {
248
+ const { id, depth } = queue.shift()!;
249
+
250
+ if (visited.has(id) || depth > maxDepth) continue;
251
+ visited.add(id);
252
+
253
+ try {
254
+ // Find nodes that get their value FROM this node
255
+ const edges = await backend.getIncomingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
256
+
257
+ for (const edge of edges) {
258
+ const sourceNode = await backend.getNode(edge.src);
259
+ if (!sourceNode) continue;
260
+
261
+ const nodeInfo: NodeInfo = {
262
+ id: sourceNode.id,
263
+ type: sourceNode.type || 'UNKNOWN',
264
+ name: sourceNode.name || '',
265
+ file: sourceNode.file || '',
266
+ line: sourceNode.line,
267
+ };
268
+
269
+ trace.push({
270
+ node: nodeInfo,
271
+ edgeType: edge.edgeType || edge.type,
272
+ depth: depth + 1,
273
+ });
274
+
275
+ // Continue forward
276
+ if (depth < maxDepth - 1) {
277
+ queue.push({ id: sourceNode.id, depth: depth + 1 });
278
+ }
279
+ }
280
+ } catch {
281
+ // Ignore errors
282
+ }
283
+ }
284
+
285
+ return trace;
286
+ }
287
+
288
+ /**
289
+ * Get immediate value sources (for "possible values" display)
290
+ */
291
+ async function getValueSources(
292
+ backend: RFDBServerBackend,
293
+ nodeId: string
294
+ ): Promise<NodeInfo[]> {
295
+ const sources: NodeInfo[] = [];
296
+
297
+ try {
298
+ const edges = await backend.getOutgoingEdges(nodeId, ['ASSIGNED_FROM']);
299
+
300
+ for (const edge of edges.slice(0, 5)) {
301
+ const node = await backend.getNode(edge.dst);
302
+ if (node) {
303
+ sources.push({
304
+ id: node.id,
305
+ type: node.type || 'UNKNOWN',
306
+ name: node.name || '',
307
+ file: node.file || '',
308
+ line: node.line,
309
+ value: node.value,
310
+ });
311
+ }
312
+ }
313
+ } catch {
314
+ // Ignore
315
+ }
316
+
317
+ return sources;
318
+ }
319
+
320
+ /**
321
+ * Display trace results with semantic IDs
322
+ */
323
+ function displayTrace(trace: TraceStep[], _projectPath: string, indent: string): void {
324
+ // Group by depth
325
+ const byDepth = new Map<number, TraceStep[]>();
326
+ for (const step of trace) {
327
+ if (!byDepth.has(step.depth)) {
328
+ byDepth.set(step.depth, []);
329
+ }
330
+ byDepth.get(step.depth)!.push(step);
331
+ }
332
+
333
+ for (const [_depth, steps] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
334
+ for (const step of steps) {
335
+ const valueStr = step.node.value !== undefined ? ` = ${JSON.stringify(step.node.value)}` : '';
336
+ console.log(`${indent}<- ${step.node.name || step.node.type} (${step.node.type})${valueStr}`);
337
+ console.log(`${indent} ${formatNodeInline(step.node)}`);
338
+ }
339
+ }
340
+ }
341
+
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Code Preview Utility
3
+ *
4
+ * Reads source files and extracts code snippets around a given line number
5
+ * for displaying in the explorer UI.
6
+ */
7
+
8
+ import { readFileSync, existsSync } from 'fs';
9
+
10
+ export interface CodePreviewOptions {
11
+ file: string;
12
+ line: number;
13
+ contextBefore?: number; // default: 2
14
+ contextAfter?: number; // default: 12
15
+ }
16
+
17
+ export interface CodePreviewResult {
18
+ lines: string[];
19
+ startLine: number;
20
+ endLine: number;
21
+ }
22
+
23
+ /**
24
+ * Get a code preview snippet from a source file.
25
+ * Returns lines around the specified line number with context.
26
+ */
27
+ export function getCodePreview(options: CodePreviewOptions): CodePreviewResult | null {
28
+ const { file, line, contextBefore = 2, contextAfter = 12 } = options;
29
+
30
+ if (!existsSync(file)) {
31
+ return null;
32
+ }
33
+
34
+ try {
35
+ const content = readFileSync(file, 'utf-8');
36
+ const allLines = content.split('\n');
37
+
38
+ // Calculate range (1-indexed)
39
+ const startLine = Math.max(1, line - contextBefore);
40
+ const endLine = Math.min(allLines.length, line + contextAfter);
41
+
42
+ // Extract lines (convert to 0-indexed for array access)
43
+ const lines = allLines.slice(startLine - 1, endLine);
44
+
45
+ return {
46
+ lines,
47
+ startLine,
48
+ endLine
49
+ };
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Format code preview lines with line numbers for display.
57
+ * Returns an array of formatted strings like " 42 | code here"
58
+ */
59
+ export function formatCodePreview(
60
+ preview: CodePreviewResult,
61
+ highlightLine?: number
62
+ ): string[] {
63
+ const { lines, startLine } = preview;
64
+ const maxLineNum = startLine + lines.length - 1;
65
+ const lineNumWidth = String(maxLineNum).length;
66
+
67
+ return lines.map((line, index) => {
68
+ const lineNum = startLine + index;
69
+ const paddedNum = String(lineNum).padStart(lineNumWidth, ' ');
70
+ const prefix = highlightLine === lineNum ? '>' : ' ';
71
+
72
+ // Truncate very long lines
73
+ const displayLine = line.length > 80 ? line.slice(0, 77) + '...' : line;
74
+
75
+ return `${prefix}${paddedNum} | ${displayLine}`;
76
+ });
77
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Standardized error formatting for CLI commands - REG-157
3
+ *
4
+ * Provides consistent error messages across all CLI commands.
5
+ * Format:
6
+ * ✗ Main error message (1 line, concise)
7
+ *
8
+ * → Next action 1
9
+ * → Next action 2
10
+ */
11
+
12
+ /**
13
+ * Print a standardized error message and exit.
14
+ *
15
+ * @param title - Main error message (should be under 80 chars)
16
+ * @param nextSteps - Optional array of actionable suggestions
17
+ * @returns never - always calls process.exit(1)
18
+ *
19
+ * @example
20
+ * exitWithError('No graph database found', [
21
+ * 'Run: grafema analyze'
22
+ * ]);
23
+ */
24
+ export function exitWithError(title: string, nextSteps?: string[]): never {
25
+ console.error(`✗ ${title}`);
26
+
27
+ if (nextSteps && nextSteps.length > 0) {
28
+ console.error('');
29
+ for (const step of nextSteps) {
30
+ console.error(`→ ${step}`);
31
+ }
32
+ }
33
+
34
+ process.exit(1);
35
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Node display formatting utilities - REG-125
3
+ *
4
+ * Provides consistent formatting for node display across all CLI commands.
5
+ * Semantic IDs are shown as the PRIMARY identifier, with location as secondary.
6
+ */
7
+
8
+ import { relative } from 'path';
9
+
10
+ /**
11
+ * Format options for node display
12
+ */
13
+ export interface FormatNodeOptions {
14
+ /** Project path for relative file paths */
15
+ projectPath: string;
16
+ /** Include location line (default: true) */
17
+ showLocation?: boolean;
18
+ /** Prefix for each line (default: '') */
19
+ indent?: string;
20
+ }
21
+
22
+ /**
23
+ * Node information required for display
24
+ */
25
+ export interface DisplayableNode {
26
+ /** Semantic ID (e.g., "auth/service.ts->AuthService->FUNCTION->authenticate") */
27
+ id: string;
28
+ /** Node type (e.g., "FUNCTION", "CLASS") */
29
+ type: string;
30
+ /** Human-readable name */
31
+ name: string;
32
+ /** Absolute file path */
33
+ file: string;
34
+ /** Line number (optional) */
35
+ line?: number;
36
+ }
37
+
38
+ /**
39
+ * Format a node for primary display (multi-line)
40
+ *
41
+ * Output format:
42
+ * [FUNCTION] authenticate
43
+ * ID: auth/service.ts->AuthService->FUNCTION->authenticate
44
+ * Location: auth/service.ts:42
45
+ */
46
+ export function formatNodeDisplay(node: DisplayableNode, options: FormatNodeOptions): string {
47
+ const { projectPath, showLocation = true, indent = '' } = options;
48
+ const lines: string[] = [];
49
+
50
+ // Line 1: [TYPE] name
51
+ lines.push(`${indent}[${node.type}] ${node.name}`);
52
+
53
+ // Line 2: ID (semantic ID)
54
+ lines.push(`${indent} ID: ${node.id}`);
55
+
56
+ // Line 3: Location (optional)
57
+ if (showLocation) {
58
+ const loc = formatLocation(node.file, node.line, projectPath);
59
+ if (loc) {
60
+ lines.push(`${indent} Location: ${loc}`);
61
+ }
62
+ }
63
+
64
+ return lines.join('\n');
65
+ }
66
+
67
+ /**
68
+ * Format a node for inline display in lists (semantic ID only)
69
+ *
70
+ * Output format:
71
+ * auth/service.ts->AuthService->FUNCTION->authenticate
72
+ */
73
+ export function formatNodeInline(node: DisplayableNode): string {
74
+ return node.id;
75
+ }
76
+
77
+ /**
78
+ * Format file location relative to project
79
+ */
80
+ export function formatLocation(
81
+ file: string | undefined,
82
+ line: number | undefined,
83
+ projectPath: string
84
+ ): string {
85
+ if (!file) return '';
86
+ const relPath = relative(projectPath, file);
87
+ return line ? `${relPath}:${line}` : relPath;
88
+ }