@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,335 @@
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
+
10
+ import { Command } from 'commander';
11
+ import { resolve, join, dirname } from 'path';
12
+ import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
13
+ import { spawn } from 'child_process';
14
+ import { fileURLToPath } from 'url';
15
+ import { setTimeout as sleep } from 'timers/promises';
16
+ import { RFDBClient } from '@grafema/core';
17
+ import { exitWithError } from '../utils/errorFormatter.js';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ /**
23
+ * Find RFDB server binary in order of preference:
24
+ * 1. @grafema/rfdb npm package
25
+ * 2. rust-engine/target/release (monorepo development)
26
+ * 3. rust-engine/target/debug
27
+ */
28
+ function findServerBinary(): string | null {
29
+ // 1. Check @grafema/rfdb npm package
30
+ try {
31
+ const rfdbPkg = require.resolve('@grafema/rfdb');
32
+ const rfdbDir = dirname(rfdbPkg);
33
+ const platform = process.platform;
34
+ const arch = process.arch;
35
+
36
+ let platformDir: string;
37
+ if (platform === 'darwin') {
38
+ platformDir = arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
39
+ } else if (platform === 'linux') {
40
+ platformDir = arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
41
+ } else {
42
+ platformDir = `${platform}-${arch}`;
43
+ }
44
+
45
+ const npmBinary = join(rfdbDir, 'prebuilt', platformDir, 'rfdb-server');
46
+ if (existsSync(npmBinary)) {
47
+ return npmBinary;
48
+ }
49
+ } catch {
50
+ // @grafema/rfdb not installed
51
+ }
52
+
53
+ // 2. Check rust-engine in monorepo
54
+ // From packages/cli/dist/commands -> project root is 4 levels up
55
+ const projectRoot = join(__dirname, '../../../..');
56
+ const releaseBinary = join(projectRoot, 'rust-engine/target/release/rfdb-server');
57
+ if (existsSync(releaseBinary)) {
58
+ return releaseBinary;
59
+ }
60
+
61
+ // 3. Check debug build
62
+ const debugBinary = join(projectRoot, 'rust-engine/target/debug/rfdb-server');
63
+ if (existsSync(debugBinary)) {
64
+ return debugBinary;
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Check if server is running by attempting to ping it
72
+ */
73
+ async function isServerRunning(socketPath: string): Promise<{ running: boolean; version?: string }> {
74
+ if (!existsSync(socketPath)) {
75
+ return { running: false };
76
+ }
77
+
78
+ const client = new RFDBClient(socketPath);
79
+ // Suppress error events (we handle via try/catch)
80
+ client.on('error', () => {});
81
+
82
+ try {
83
+ await client.connect();
84
+ const version = await client.ping();
85
+ await client.close();
86
+ return { running: true, version: version || undefined };
87
+ } catch {
88
+ // Socket exists but can't connect - stale socket
89
+ return { running: false };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get paths for a project
95
+ */
96
+ function getProjectPaths(projectPath: string) {
97
+ const grafemaDir = join(projectPath, '.grafema');
98
+ const socketPath = join(grafemaDir, 'rfdb.sock');
99
+ const dbPath = join(grafemaDir, 'graph.rfdb');
100
+ const pidPath = join(grafemaDir, 'rfdb.pid');
101
+ return { grafemaDir, socketPath, dbPath, pidPath };
102
+ }
103
+
104
+ // Create main server command with subcommands
105
+ export const serverCommand = new Command('server')
106
+ .description('Manage RFDB server lifecycle');
107
+
108
+ // grafema server start
109
+ serverCommand
110
+ .command('start')
111
+ .description('Start the RFDB server')
112
+ .option('-p, --project <path>', 'Project path', '.')
113
+ .action(async (options: { project: string }) => {
114
+ const projectPath = resolve(options.project);
115
+ const { grafemaDir, socketPath, dbPath, pidPath } = getProjectPaths(projectPath);
116
+
117
+ // Check if grafema is initialized
118
+ if (!existsSync(grafemaDir)) {
119
+ exitWithError('Grafema not initialized', [
120
+ 'Run: grafema init',
121
+ 'Or: grafema analyze (initializes automatically)'
122
+ ]);
123
+ }
124
+
125
+ // Check if server already running
126
+ const status = await isServerRunning(socketPath);
127
+ if (status.running) {
128
+ console.log(`Server already running at ${socketPath}`);
129
+ if (status.version) {
130
+ console.log(` Version: ${status.version}`);
131
+ }
132
+ return;
133
+ }
134
+
135
+ // Remove stale socket if exists
136
+ if (existsSync(socketPath)) {
137
+ unlinkSync(socketPath);
138
+ }
139
+
140
+ // Find server binary
141
+ const binaryPath = findServerBinary();
142
+ if (!binaryPath) {
143
+ exitWithError('RFDB server binary not found', [
144
+ 'Install: npm install @grafema/rfdb',
145
+ 'Or build: cargo build --release --bin rfdb-server'
146
+ ]);
147
+ }
148
+
149
+ console.log(`Starting RFDB server...`);
150
+ console.log(` Database: ${dbPath}`);
151
+ console.log(` Socket: ${socketPath}`);
152
+
153
+ // Start server
154
+ const serverProcess = spawn(binaryPath, [dbPath, '--socket', socketPath], {
155
+ stdio: ['ignore', 'pipe', 'pipe'],
156
+ detached: true,
157
+ });
158
+
159
+ // Don't let server process prevent parent from exiting
160
+ serverProcess.unref();
161
+
162
+ // Write PID file
163
+ if (serverProcess.pid) {
164
+ writeFileSync(pidPath, String(serverProcess.pid));
165
+ }
166
+
167
+ // Wait for socket to appear
168
+ let attempts = 0;
169
+ while (!existsSync(socketPath) && attempts < 50) {
170
+ await sleep(100);
171
+ attempts++;
172
+ }
173
+
174
+ if (!existsSync(socketPath)) {
175
+ exitWithError('Server failed to start', [
176
+ 'Check if database path is valid',
177
+ 'Check server logs for errors'
178
+ ]);
179
+ }
180
+
181
+ // Verify server is responsive
182
+ const verifyStatus = await isServerRunning(socketPath);
183
+ if (!verifyStatus.running) {
184
+ exitWithError('Server started but not responding', [
185
+ 'Check server logs for errors'
186
+ ]);
187
+ }
188
+
189
+ console.log('');
190
+ console.log(`Server started successfully`);
191
+ if (verifyStatus.version) {
192
+ console.log(` Version: ${verifyStatus.version}`);
193
+ }
194
+ if (serverProcess.pid) {
195
+ console.log(` PID: ${serverProcess.pid}`);
196
+ }
197
+ });
198
+
199
+ // grafema server stop
200
+ serverCommand
201
+ .command('stop')
202
+ .description('Stop the RFDB server')
203
+ .option('-p, --project <path>', 'Project path', '.')
204
+ .action(async (options: { project: string }) => {
205
+ const projectPath = resolve(options.project);
206
+ const { socketPath, pidPath } = getProjectPaths(projectPath);
207
+
208
+ // Check if server is running
209
+ const status = await isServerRunning(socketPath);
210
+ if (!status.running) {
211
+ console.log('Server not running');
212
+ // Clean up stale socket and PID file
213
+ if (existsSync(socketPath)) {
214
+ unlinkSync(socketPath);
215
+ }
216
+ if (existsSync(pidPath)) {
217
+ unlinkSync(pidPath);
218
+ }
219
+ return;
220
+ }
221
+
222
+ console.log('Stopping RFDB server...');
223
+
224
+ // Send shutdown command
225
+ const client = new RFDBClient(socketPath);
226
+ // Suppress error events (server closes connection on shutdown)
227
+ client.on('error', () => {});
228
+
229
+ try {
230
+ await client.connect();
231
+ await client.shutdown();
232
+ } catch {
233
+ // Expected - server closes connection
234
+ }
235
+
236
+ // Wait for socket to disappear
237
+ let attempts = 0;
238
+ while (existsSync(socketPath) && attempts < 30) {
239
+ await sleep(100);
240
+ attempts++;
241
+ }
242
+
243
+ // Clean up PID file
244
+ if (existsSync(pidPath)) {
245
+ unlinkSync(pidPath);
246
+ }
247
+
248
+ console.log('Server stopped');
249
+ });
250
+
251
+ // grafema server status
252
+ serverCommand
253
+ .command('status')
254
+ .description('Check RFDB server status')
255
+ .option('-p, --project <path>', 'Project path', '.')
256
+ .option('-j, --json', 'Output as JSON')
257
+ .action(async (options: { project: string; json?: boolean }) => {
258
+ const projectPath = resolve(options.project);
259
+ const { grafemaDir, socketPath, dbPath, pidPath } = getProjectPaths(projectPath);
260
+
261
+ // Check if grafema is initialized
262
+ const initialized = existsSync(grafemaDir);
263
+
264
+ // Check server status
265
+ const status = await isServerRunning(socketPath);
266
+
267
+ // Read PID if available
268
+ let pid: number | null = null;
269
+ if (existsSync(pidPath)) {
270
+ try {
271
+ pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
272
+ } catch {
273
+ // Ignore read errors
274
+ }
275
+ }
276
+
277
+ // Get stats if running
278
+ let nodeCount: number | undefined;
279
+ let edgeCount: number | undefined;
280
+ if (status.running) {
281
+ const client = new RFDBClient(socketPath);
282
+ client.on('error', () => {}); // Suppress error events
283
+
284
+ try {
285
+ await client.connect();
286
+ nodeCount = await client.nodeCount();
287
+ edgeCount = await client.edgeCount();
288
+ await client.close();
289
+ } catch {
290
+ // Ignore errors
291
+ }
292
+ }
293
+
294
+ if (options.json) {
295
+ console.log(JSON.stringify({
296
+ initialized,
297
+ running: status.running,
298
+ version: status.version || null,
299
+ socketPath: initialized ? socketPath : null,
300
+ dbPath: initialized ? dbPath : null,
301
+ pid,
302
+ nodeCount,
303
+ edgeCount,
304
+ }, null, 2));
305
+ return;
306
+ }
307
+
308
+ // Text output
309
+ if (!initialized) {
310
+ console.log('Grafema not initialized');
311
+ console.log(' Run: grafema init');
312
+ return;
313
+ }
314
+
315
+ if (status.running) {
316
+ console.log('RFDB server is running');
317
+ console.log(` Socket: ${socketPath}`);
318
+ if (status.version) {
319
+ console.log(` Version: ${status.version}`);
320
+ }
321
+ if (pid) {
322
+ console.log(` PID: ${pid}`);
323
+ }
324
+ if (nodeCount !== undefined && edgeCount !== undefined) {
325
+ console.log(` Nodes: ${nodeCount}`);
326
+ console.log(` Edges: ${edgeCount}`);
327
+ }
328
+ } else {
329
+ console.log('RFDB server is not running');
330
+ console.log(` Socket: ${socketPath}`);
331
+ if (existsSync(socketPath)) {
332
+ console.log(' (stale socket file exists)');
333
+ }
334
+ }
335
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Stats command - Show project statistics
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { resolve, join } from 'path';
7
+ import { existsSync } from 'fs';
8
+ import { RFDBServerBackend } from '@grafema/core';
9
+ import { exitWithError } from '../utils/errorFormatter.js';
10
+
11
+ export const statsCommand = new Command('stats')
12
+ .description('Show project statistics')
13
+ .option('-p, --project <path>', 'Project path', '.')
14
+ .option('-j, --json', 'Output as JSON')
15
+ .option('-t, --types', 'Show breakdown by type')
16
+ .action(async (options: { project: string; json?: boolean; types?: boolean }) => {
17
+ const projectPath = resolve(options.project);
18
+ const grafemaDir = join(projectPath, '.grafema');
19
+ const dbPath = join(grafemaDir, 'graph.rfdb');
20
+
21
+ if (!existsSync(dbPath)) {
22
+ exitWithError('No graph database found', ['Run: grafema analyze']);
23
+ }
24
+
25
+ const backend = new RFDBServerBackend({ dbPath });
26
+ await backend.connect();
27
+
28
+ try {
29
+ const stats = await backend.getStats();
30
+
31
+ if (options.json) {
32
+ console.log(JSON.stringify(stats, null, 2));
33
+ } else {
34
+ console.log('Graph Statistics');
35
+ console.log('================');
36
+ console.log(`Total nodes: ${stats.nodeCount}`);
37
+ console.log(`Total edges: ${stats.edgeCount}`);
38
+
39
+ if (options.types) {
40
+ console.log('');
41
+ console.log('Nodes by type:');
42
+ const sortedNodes = Object.entries(stats.nodesByType).sort((a, b) => b[1] - a[1]);
43
+ for (const [type, count] of sortedNodes) {
44
+ console.log(` ${type}: ${count}`);
45
+ }
46
+
47
+ console.log('');
48
+ console.log('Edges by type:');
49
+ const sortedEdges = Object.entries(stats.edgesByType).sort((a, b) => b[1] - a[1]);
50
+ for (const [type, count] of sortedEdges) {
51
+ console.log(` ${type}: ${count}`);
52
+ }
53
+ }
54
+ }
55
+ } finally {
56
+ await backend.close();
57
+ }
58
+ });