@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,300 @@
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
+ import { resolve, join, dirname } from 'path';
11
+ import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
12
+ import { spawn } from 'child_process';
13
+ import { fileURLToPath } from 'url';
14
+ import { setTimeout as sleep } from 'timers/promises';
15
+ import { RFDBClient } from '@grafema/core';
16
+ import { exitWithError } from '../utils/errorFormatter.js';
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ /**
20
+ * Find RFDB server binary in order of preference:
21
+ * 1. @grafema/rfdb npm package
22
+ * 2. rust-engine/target/release (monorepo development)
23
+ * 3. rust-engine/target/debug
24
+ */
25
+ function findServerBinary() {
26
+ // 1. Check @grafema/rfdb npm package
27
+ try {
28
+ const rfdbPkg = require.resolve('@grafema/rfdb');
29
+ const rfdbDir = dirname(rfdbPkg);
30
+ const platform = process.platform;
31
+ const arch = process.arch;
32
+ let platformDir;
33
+ if (platform === 'darwin') {
34
+ platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
35
+ }
36
+ else if (platform === 'linux') {
37
+ platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
38
+ }
39
+ else {
40
+ platformDir = `${platform}-${arch}`;
41
+ }
42
+ const npmBinary = join(rfdbDir, 'prebuilt', platformDir, 'rfdb-server');
43
+ if (existsSync(npmBinary)) {
44
+ return npmBinary;
45
+ }
46
+ }
47
+ catch {
48
+ // @grafema/rfdb not installed
49
+ }
50
+ // 2. Check rust-engine in monorepo
51
+ // From packages/cli/dist/commands -> project root is 4 levels up
52
+ const projectRoot = join(__dirname, '../../../..');
53
+ const releaseBinary = join(projectRoot, 'rust-engine/target/release/rfdb-server');
54
+ if (existsSync(releaseBinary)) {
55
+ return releaseBinary;
56
+ }
57
+ // 3. Check debug build
58
+ const debugBinary = join(projectRoot, 'rust-engine/target/debug/rfdb-server');
59
+ if (existsSync(debugBinary)) {
60
+ return debugBinary;
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * Check if server is running by attempting to ping it
66
+ */
67
+ async function isServerRunning(socketPath) {
68
+ if (!existsSync(socketPath)) {
69
+ return { running: false };
70
+ }
71
+ const client = new RFDBClient(socketPath);
72
+ // Suppress error events (we handle via try/catch)
73
+ client.on('error', () => { });
74
+ try {
75
+ await client.connect();
76
+ const version = await client.ping();
77
+ await client.close();
78
+ return { running: true, version: version || undefined };
79
+ }
80
+ catch {
81
+ // Socket exists but can't connect - stale socket
82
+ return { running: false };
83
+ }
84
+ }
85
+ /**
86
+ * Get paths for a project
87
+ */
88
+ function getProjectPaths(projectPath) {
89
+ const grafemaDir = join(projectPath, '.grafema');
90
+ const socketPath = join(grafemaDir, 'rfdb.sock');
91
+ const dbPath = join(grafemaDir, 'graph.rfdb');
92
+ const pidPath = join(grafemaDir, 'rfdb.pid');
93
+ return { grafemaDir, socketPath, dbPath, pidPath };
94
+ }
95
+ // Create main server command with subcommands
96
+ export const serverCommand = new Command('server')
97
+ .description('Manage RFDB server lifecycle');
98
+ // grafema server start
99
+ serverCommand
100
+ .command('start')
101
+ .description('Start the RFDB server')
102
+ .option('-p, --project <path>', 'Project path', '.')
103
+ .action(async (options) => {
104
+ const projectPath = resolve(options.project);
105
+ const { grafemaDir, socketPath, dbPath, pidPath } = getProjectPaths(projectPath);
106
+ // Check if grafema is initialized
107
+ if (!existsSync(grafemaDir)) {
108
+ exitWithError('Grafema not initialized', [
109
+ 'Run: grafema init',
110
+ 'Or: grafema analyze (initializes automatically)'
111
+ ]);
112
+ }
113
+ // Check if server already running
114
+ const status = await isServerRunning(socketPath);
115
+ if (status.running) {
116
+ console.log(`Server already running at ${socketPath}`);
117
+ if (status.version) {
118
+ console.log(` Version: ${status.version}`);
119
+ }
120
+ return;
121
+ }
122
+ // Remove stale socket if exists
123
+ if (existsSync(socketPath)) {
124
+ unlinkSync(socketPath);
125
+ }
126
+ // Find server binary
127
+ const binaryPath = findServerBinary();
128
+ if (!binaryPath) {
129
+ exitWithError('RFDB server binary not found', [
130
+ 'Install: npm install @grafema/rfdb',
131
+ 'Or build: cargo build --release --bin rfdb-server'
132
+ ]);
133
+ }
134
+ console.log(`Starting RFDB server...`);
135
+ console.log(` Database: ${dbPath}`);
136
+ console.log(` Socket: ${socketPath}`);
137
+ // Start server
138
+ const serverProcess = spawn(binaryPath, [dbPath, '--socket', socketPath], {
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ detached: true,
141
+ });
142
+ // Don't let server process prevent parent from exiting
143
+ serverProcess.unref();
144
+ // Write PID file
145
+ if (serverProcess.pid) {
146
+ writeFileSync(pidPath, String(serverProcess.pid));
147
+ }
148
+ // Wait for socket to appear
149
+ let attempts = 0;
150
+ while (!existsSync(socketPath) && attempts < 50) {
151
+ await sleep(100);
152
+ attempts++;
153
+ }
154
+ if (!existsSync(socketPath)) {
155
+ exitWithError('Server failed to start', [
156
+ 'Check if database path is valid',
157
+ 'Check server logs for errors'
158
+ ]);
159
+ }
160
+ // Verify server is responsive
161
+ const verifyStatus = await isServerRunning(socketPath);
162
+ if (!verifyStatus.running) {
163
+ exitWithError('Server started but not responding', [
164
+ 'Check server logs for errors'
165
+ ]);
166
+ }
167
+ console.log('');
168
+ console.log(`Server started successfully`);
169
+ if (verifyStatus.version) {
170
+ console.log(` Version: ${verifyStatus.version}`);
171
+ }
172
+ if (serverProcess.pid) {
173
+ console.log(` PID: ${serverProcess.pid}`);
174
+ }
175
+ });
176
+ // grafema server stop
177
+ serverCommand
178
+ .command('stop')
179
+ .description('Stop the RFDB server')
180
+ .option('-p, --project <path>', 'Project path', '.')
181
+ .action(async (options) => {
182
+ const projectPath = resolve(options.project);
183
+ const { socketPath, pidPath } = getProjectPaths(projectPath);
184
+ // Check if server is running
185
+ const status = await isServerRunning(socketPath);
186
+ if (!status.running) {
187
+ console.log('Server not running');
188
+ // Clean up stale socket and PID file
189
+ if (existsSync(socketPath)) {
190
+ unlinkSync(socketPath);
191
+ }
192
+ if (existsSync(pidPath)) {
193
+ unlinkSync(pidPath);
194
+ }
195
+ return;
196
+ }
197
+ console.log('Stopping RFDB server...');
198
+ // Send shutdown command
199
+ const client = new RFDBClient(socketPath);
200
+ // Suppress error events (server closes connection on shutdown)
201
+ client.on('error', () => { });
202
+ try {
203
+ await client.connect();
204
+ await client.shutdown();
205
+ }
206
+ catch {
207
+ // Expected - server closes connection
208
+ }
209
+ // Wait for socket to disappear
210
+ let attempts = 0;
211
+ while (existsSync(socketPath) && attempts < 30) {
212
+ await sleep(100);
213
+ attempts++;
214
+ }
215
+ // Clean up PID file
216
+ if (existsSync(pidPath)) {
217
+ unlinkSync(pidPath);
218
+ }
219
+ console.log('Server stopped');
220
+ });
221
+ // grafema server status
222
+ serverCommand
223
+ .command('status')
224
+ .description('Check RFDB server status')
225
+ .option('-p, --project <path>', 'Project path', '.')
226
+ .option('-j, --json', 'Output as JSON')
227
+ .action(async (options) => {
228
+ const projectPath = resolve(options.project);
229
+ const { grafemaDir, socketPath, dbPath, pidPath } = getProjectPaths(projectPath);
230
+ // Check if grafema is initialized
231
+ const initialized = existsSync(grafemaDir);
232
+ // Check server status
233
+ const status = await isServerRunning(socketPath);
234
+ // Read PID if available
235
+ let pid = null;
236
+ if (existsSync(pidPath)) {
237
+ try {
238
+ pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
239
+ }
240
+ catch {
241
+ // Ignore read errors
242
+ }
243
+ }
244
+ // Get stats if running
245
+ let nodeCount;
246
+ let edgeCount;
247
+ if (status.running) {
248
+ const client = new RFDBClient(socketPath);
249
+ client.on('error', () => { }); // Suppress error events
250
+ try {
251
+ await client.connect();
252
+ nodeCount = await client.nodeCount();
253
+ edgeCount = await client.edgeCount();
254
+ await client.close();
255
+ }
256
+ catch {
257
+ // Ignore errors
258
+ }
259
+ }
260
+ if (options.json) {
261
+ console.log(JSON.stringify({
262
+ initialized,
263
+ running: status.running,
264
+ version: status.version || null,
265
+ socketPath: initialized ? socketPath : null,
266
+ dbPath: initialized ? dbPath : null,
267
+ pid,
268
+ nodeCount,
269
+ edgeCount,
270
+ }, null, 2));
271
+ return;
272
+ }
273
+ // Text output
274
+ if (!initialized) {
275
+ console.log('Grafema not initialized');
276
+ console.log(' Run: grafema init');
277
+ return;
278
+ }
279
+ if (status.running) {
280
+ console.log('RFDB server is running');
281
+ console.log(` Socket: ${socketPath}`);
282
+ if (status.version) {
283
+ console.log(` Version: ${status.version}`);
284
+ }
285
+ if (pid) {
286
+ console.log(` PID: ${pid}`);
287
+ }
288
+ if (nodeCount !== undefined && edgeCount !== undefined) {
289
+ console.log(` Nodes: ${nodeCount}`);
290
+ console.log(` Edges: ${edgeCount}`);
291
+ }
292
+ }
293
+ else {
294
+ console.log('RFDB server is not running');
295
+ console.log(` Socket: ${socketPath}`);
296
+ if (existsSync(socketPath)) {
297
+ console.log(' (stale socket file exists)');
298
+ }
299
+ }
300
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Stats command - Show project statistics
3
+ */
4
+ import { Command } from 'commander';
5
+ export declare const statsCommand: Command;
6
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/commands/stats.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,YAAY,SA+CrB,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Stats command - Show project statistics
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 statsCommand = new Command('stats')
10
+ .description('Show project statistics')
11
+ .option('-p, --project <path>', 'Project path', '.')
12
+ .option('-j, --json', 'Output as JSON')
13
+ .option('-t, --types', 'Show breakdown by type')
14
+ .action(async (options) => {
15
+ const projectPath = resolve(options.project);
16
+ const grafemaDir = join(projectPath, '.grafema');
17
+ const dbPath = join(grafemaDir, 'graph.rfdb');
18
+ if (!existsSync(dbPath)) {
19
+ exitWithError('No graph database found', ['Run: grafema analyze']);
20
+ }
21
+ const backend = new RFDBServerBackend({ dbPath });
22
+ await backend.connect();
23
+ try {
24
+ const stats = await backend.getStats();
25
+ if (options.json) {
26
+ console.log(JSON.stringify(stats, null, 2));
27
+ }
28
+ else {
29
+ console.log('Graph Statistics');
30
+ console.log('================');
31
+ console.log(`Total nodes: ${stats.nodeCount}`);
32
+ console.log(`Total edges: ${stats.edgeCount}`);
33
+ if (options.types) {
34
+ console.log('');
35
+ console.log('Nodes by type:');
36
+ const sortedNodes = Object.entries(stats.nodesByType).sort((a, b) => b[1] - a[1]);
37
+ for (const [type, count] of sortedNodes) {
38
+ console.log(` ${type}: ${count}`);
39
+ }
40
+ console.log('');
41
+ console.log('Edges by type:');
42
+ const sortedEdges = Object.entries(stats.edgesByType).sort((a, b) => b[1] - a[1]);
43
+ for (const [type, count] of sortedEdges) {
44
+ console.log(` ${type}: ${count}`);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ finally {
50
+ await backend.close();
51
+ }
52
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Trace command - Data flow analysis
3
+ *
4
+ * Usage:
5
+ * grafema trace "userId from authenticate"
6
+ * grafema trace "config"
7
+ */
8
+ import { Command } from 'commander';
9
+ export declare const traceCommand: Command;
10
+ //# sourceMappingURL=trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/commands/trace.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,eAAO,MAAM,YAAY,SAuFrB,CAAC"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Trace command - Data flow analysis
3
+ *
4
+ * Usage:
5
+ * grafema trace "userId from authenticate"
6
+ * grafema trace "config"
7
+ */
8
+ import { Command } from 'commander';
9
+ import { resolve, join } from 'path';
10
+ import { existsSync } from 'fs';
11
+ import { RFDBServerBackend, parseSemanticId } from '@grafema/core';
12
+ import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
13
+ import { exitWithError } from '../utils/errorFormatter.js';
14
+ export const traceCommand = new Command('trace')
15
+ .description('Trace data flow for a variable')
16
+ .argument('<pattern>', 'Pattern: "varName from functionName" or just "varName"')
17
+ .option('-p, --project <path>', 'Project path', '.')
18
+ .option('-j, --json', 'Output as JSON')
19
+ .option('-d, --depth <n>', 'Max trace depth', '10')
20
+ .action(async (pattern, options) => {
21
+ const projectPath = resolve(options.project);
22
+ const grafemaDir = join(projectPath, '.grafema');
23
+ const dbPath = join(grafemaDir, 'graph.rfdb');
24
+ if (!existsSync(dbPath)) {
25
+ exitWithError('No graph database found', ['Run: grafema analyze']);
26
+ }
27
+ const backend = new RFDBServerBackend({ dbPath });
28
+ await backend.connect();
29
+ try {
30
+ // Parse pattern: "varName from functionName" or just "varName"
31
+ const { varName, scopeName } = parseTracePattern(pattern);
32
+ const maxDepth = parseInt(options.depth, 10);
33
+ console.log(`Tracing ${varName}${scopeName ? ` from ${scopeName}` : ''}...`);
34
+ console.log('');
35
+ // Find starting variable(s)
36
+ const variables = await findVariables(backend, varName, scopeName);
37
+ if (variables.length === 0) {
38
+ console.log(`No variable "${varName}" found${scopeName ? ` in ${scopeName}` : ''}`);
39
+ return;
40
+ }
41
+ // Trace each variable
42
+ for (const variable of variables) {
43
+ console.log(formatNodeDisplay(variable, { projectPath }));
44
+ console.log('');
45
+ // Trace backwards through ASSIGNED_FROM
46
+ const backwardTrace = await traceBackward(backend, variable.id, maxDepth);
47
+ if (backwardTrace.length > 0) {
48
+ console.log('Data sources (where value comes from):');
49
+ displayTrace(backwardTrace, projectPath, ' ');
50
+ }
51
+ // Trace forward through ASSIGNED_FROM (where this value flows to)
52
+ const forwardTrace = await traceForward(backend, variable.id, maxDepth);
53
+ if (forwardTrace.length > 0) {
54
+ console.log('');
55
+ console.log('Data sinks (where value flows to):');
56
+ displayTrace(forwardTrace, projectPath, ' ');
57
+ }
58
+ // Show value domain if available
59
+ const sources = await getValueSources(backend, variable.id);
60
+ if (sources.length > 0) {
61
+ console.log('');
62
+ console.log('Possible values:');
63
+ for (const src of sources) {
64
+ if (src.type === 'LITERAL' && src.value !== undefined) {
65
+ console.log(` • ${JSON.stringify(src.value)} (literal)`);
66
+ }
67
+ else if (src.type === 'PARAMETER') {
68
+ console.log(` • <parameter ${src.name}> (runtime input)`);
69
+ }
70
+ else if (src.type === 'CALL') {
71
+ console.log(` • <return from ${src.name || 'call'}> (computed)`);
72
+ }
73
+ else {
74
+ console.log(` • <${src.type.toLowerCase()}> ${src.name || ''}`);
75
+ }
76
+ }
77
+ }
78
+ if (variables.length > 1) {
79
+ console.log('');
80
+ console.log('---');
81
+ }
82
+ }
83
+ if (options.json) {
84
+ // TODO: structured JSON output
85
+ }
86
+ }
87
+ finally {
88
+ await backend.close();
89
+ }
90
+ });
91
+ /**
92
+ * Parse trace pattern
93
+ */
94
+ function parseTracePattern(pattern) {
95
+ const fromMatch = pattern.match(/^(.+?)\s+from\s+(.+)$/i);
96
+ if (fromMatch) {
97
+ return { varName: fromMatch[1].trim(), scopeName: fromMatch[2].trim() };
98
+ }
99
+ return { varName: pattern.trim(), scopeName: null };
100
+ }
101
+ /**
102
+ * Find variables by name, optionally scoped to a function
103
+ */
104
+ async function findVariables(backend, varName, scopeName) {
105
+ const results = [];
106
+ const lowerScopeName = scopeName ? scopeName.toLowerCase() : null;
107
+ // Search VARIABLE, CONSTANT, PARAMETER
108
+ for (const nodeType of ['VARIABLE', 'CONSTANT', 'PARAMETER']) {
109
+ for await (const node of backend.queryNodes({ nodeType: nodeType })) {
110
+ const name = node.name || '';
111
+ if (name.toLowerCase() === varName.toLowerCase()) {
112
+ // If scope specified, check if variable is in that scope
113
+ if (scopeName) {
114
+ const parsed = parseSemanticId(node.id);
115
+ if (!parsed)
116
+ continue; // Skip nodes with invalid IDs
117
+ // Check if scopeName appears anywhere in the scope chain
118
+ if (!parsed.scopePath.some(s => s.toLowerCase() === lowerScopeName)) {
119
+ continue;
120
+ }
121
+ }
122
+ results.push({
123
+ id: node.id,
124
+ type: node.type || nodeType,
125
+ name: name,
126
+ file: node.file || '',
127
+ line: node.line,
128
+ });
129
+ if (results.length >= 5)
130
+ break;
131
+ }
132
+ }
133
+ if (results.length >= 5)
134
+ break;
135
+ }
136
+ return results;
137
+ }
138
+ /**
139
+ * Trace backward through ASSIGNED_FROM edges
140
+ */
141
+ async function traceBackward(backend, startId, maxDepth) {
142
+ const trace = [];
143
+ const visited = new Set();
144
+ const queue = [{ id: startId, depth: 0 }];
145
+ while (queue.length > 0) {
146
+ const { id, depth } = queue.shift();
147
+ if (visited.has(id) || depth > maxDepth)
148
+ continue;
149
+ visited.add(id);
150
+ try {
151
+ const edges = await backend.getOutgoingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
152
+ for (const edge of edges) {
153
+ const targetNode = await backend.getNode(edge.dst);
154
+ if (!targetNode)
155
+ continue;
156
+ const nodeInfo = {
157
+ id: targetNode.id,
158
+ type: targetNode.type || 'UNKNOWN',
159
+ name: targetNode.name || '',
160
+ file: targetNode.file || '',
161
+ line: targetNode.line,
162
+ value: targetNode.value,
163
+ };
164
+ trace.push({
165
+ node: nodeInfo,
166
+ edgeType: edge.edgeType || edge.type,
167
+ depth: depth + 1,
168
+ });
169
+ // Continue tracing unless we hit a leaf
170
+ const leafTypes = ['LITERAL', 'PARAMETER', 'EXTERNAL_MODULE'];
171
+ if (!leafTypes.includes(nodeInfo.type)) {
172
+ queue.push({ id: targetNode.id, depth: depth + 1 });
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // Ignore errors
178
+ }
179
+ }
180
+ return trace;
181
+ }
182
+ /**
183
+ * Trace forward - find what uses this variable
184
+ */
185
+ async function traceForward(backend, startId, maxDepth) {
186
+ const trace = [];
187
+ const visited = new Set();
188
+ const queue = [{ id: startId, depth: 0 }];
189
+ while (queue.length > 0) {
190
+ const { id, depth } = queue.shift();
191
+ if (visited.has(id) || depth > maxDepth)
192
+ continue;
193
+ visited.add(id);
194
+ try {
195
+ // Find nodes that get their value FROM this node
196
+ const edges = await backend.getIncomingEdges(id, ['ASSIGNED_FROM', 'DERIVES_FROM']);
197
+ for (const edge of edges) {
198
+ const sourceNode = await backend.getNode(edge.src);
199
+ if (!sourceNode)
200
+ continue;
201
+ const nodeInfo = {
202
+ id: sourceNode.id,
203
+ type: sourceNode.type || 'UNKNOWN',
204
+ name: sourceNode.name || '',
205
+ file: sourceNode.file || '',
206
+ line: sourceNode.line,
207
+ };
208
+ trace.push({
209
+ node: nodeInfo,
210
+ edgeType: edge.edgeType || edge.type,
211
+ depth: depth + 1,
212
+ });
213
+ // Continue forward
214
+ if (depth < maxDepth - 1) {
215
+ queue.push({ id: sourceNode.id, depth: depth + 1 });
216
+ }
217
+ }
218
+ }
219
+ catch {
220
+ // Ignore errors
221
+ }
222
+ }
223
+ return trace;
224
+ }
225
+ /**
226
+ * Get immediate value sources (for "possible values" display)
227
+ */
228
+ async function getValueSources(backend, nodeId) {
229
+ const sources = [];
230
+ try {
231
+ const edges = await backend.getOutgoingEdges(nodeId, ['ASSIGNED_FROM']);
232
+ for (const edge of edges.slice(0, 5)) {
233
+ const node = await backend.getNode(edge.dst);
234
+ if (node) {
235
+ sources.push({
236
+ id: node.id,
237
+ type: node.type || 'UNKNOWN',
238
+ name: node.name || '',
239
+ file: node.file || '',
240
+ line: node.line,
241
+ value: node.value,
242
+ });
243
+ }
244
+ }
245
+ }
246
+ catch {
247
+ // Ignore
248
+ }
249
+ return sources;
250
+ }
251
+ /**
252
+ * Display trace results with semantic IDs
253
+ */
254
+ function displayTrace(trace, _projectPath, indent) {
255
+ // Group by depth
256
+ const byDepth = new Map();
257
+ for (const step of trace) {
258
+ if (!byDepth.has(step.depth)) {
259
+ byDepth.set(step.depth, []);
260
+ }
261
+ byDepth.get(step.depth).push(step);
262
+ }
263
+ for (const [_depth, steps] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
264
+ for (const step of steps) {
265
+ const valueStr = step.node.value !== undefined ? ` = ${JSON.stringify(step.node.value)}` : '';
266
+ console.log(`${indent}<- ${step.node.name || step.node.type} (${step.node.type})${valueStr}`);
267
+ console.log(`${indent} ${formatNodeInline(step.node)}`);
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,28 @@
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
+ export interface CodePreviewOptions {
8
+ file: string;
9
+ line: number;
10
+ contextBefore?: number;
11
+ contextAfter?: number;
12
+ }
13
+ export interface CodePreviewResult {
14
+ lines: string[];
15
+ startLine: number;
16
+ endLine: number;
17
+ }
18
+ /**
19
+ * Get a code preview snippet from a source file.
20
+ * Returns lines around the specified line number with context.
21
+ */
22
+ export declare function getCodePreview(options: CodePreviewOptions): CodePreviewResult | null;
23
+ /**
24
+ * Format code preview lines with line numbers for display.
25
+ * Returns an array of formatted strings like " 42 | code here"
26
+ */
27
+ export declare function formatCodePreview(preview: CodePreviewResult, highlightLine?: number): string[];
28
+ //# sourceMappingURL=codePreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codePreview.d.ts","sourceRoot":"","sources":["../../src/utils/codePreview.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,iBAAiB,GAAG,IAAI,CA0BpF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,iBAAiB,EAC1B,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,EAAE,CAeV"}