@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.
Files changed (79) hide show
  1. package/dist/cli.js +10 -0
  2. package/dist/commands/analyze.d.ts.map +1 -1
  3. package/dist/commands/analyze.js +69 -11
  4. package/dist/commands/check.d.ts +6 -0
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +177 -1
  7. package/dist/commands/coverage.d.ts.map +1 -1
  8. package/dist/commands/coverage.js +7 -0
  9. package/dist/commands/doctor/checks.d.ts +55 -0
  10. package/dist/commands/doctor/checks.d.ts.map +1 -0
  11. package/dist/commands/doctor/checks.js +534 -0
  12. package/dist/commands/doctor/output.d.ts +20 -0
  13. package/dist/commands/doctor/output.d.ts.map +1 -0
  14. package/dist/commands/doctor/output.js +94 -0
  15. package/dist/commands/doctor/types.d.ts +42 -0
  16. package/dist/commands/doctor/types.d.ts.map +1 -0
  17. package/dist/commands/doctor/types.js +4 -0
  18. package/dist/commands/doctor.d.ts +17 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +80 -0
  21. package/dist/commands/explain.d.ts +16 -0
  22. package/dist/commands/explain.d.ts.map +1 -0
  23. package/dist/commands/explain.js +145 -0
  24. package/dist/commands/explore.d.ts +7 -1
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +204 -85
  27. package/dist/commands/get.d.ts.map +1 -1
  28. package/dist/commands/get.js +16 -4
  29. package/dist/commands/impact.d.ts.map +1 -1
  30. package/dist/commands/impact.js +48 -50
  31. package/dist/commands/init.d.ts.map +1 -1
  32. package/dist/commands/init.js +93 -15
  33. package/dist/commands/ls.d.ts +14 -0
  34. package/dist/commands/ls.d.ts.map +1 -0
  35. package/dist/commands/ls.js +132 -0
  36. package/dist/commands/overview.d.ts.map +1 -1
  37. package/dist/commands/overview.js +15 -2
  38. package/dist/commands/query.d.ts +98 -0
  39. package/dist/commands/query.d.ts.map +1 -1
  40. package/dist/commands/query.js +549 -136
  41. package/dist/commands/schema.d.ts +13 -0
  42. package/dist/commands/schema.d.ts.map +1 -0
  43. package/dist/commands/schema.js +279 -0
  44. package/dist/commands/server.d.ts.map +1 -1
  45. package/dist/commands/server.js +13 -6
  46. package/dist/commands/stats.d.ts.map +1 -1
  47. package/dist/commands/stats.js +7 -0
  48. package/dist/commands/trace.d.ts +73 -0
  49. package/dist/commands/trace.d.ts.map +1 -1
  50. package/dist/commands/trace.js +500 -5
  51. package/dist/commands/types.d.ts +12 -0
  52. package/dist/commands/types.d.ts.map +1 -0
  53. package/dist/commands/types.js +79 -0
  54. package/dist/utils/formatNode.d.ts +13 -0
  55. package/dist/utils/formatNode.d.ts.map +1 -1
  56. package/dist/utils/formatNode.js +35 -2
  57. package/package.json +3 -3
  58. package/src/cli.ts +10 -0
  59. package/src/commands/analyze.ts +84 -9
  60. package/src/commands/check.ts +201 -0
  61. package/src/commands/coverage.ts +7 -0
  62. package/src/commands/doctor/checks.ts +612 -0
  63. package/src/commands/doctor/output.ts +115 -0
  64. package/src/commands/doctor/types.ts +45 -0
  65. package/src/commands/doctor.ts +106 -0
  66. package/src/commands/explain.ts +173 -0
  67. package/src/commands/explore.tsx +247 -97
  68. package/src/commands/get.ts +20 -6
  69. package/src/commands/impact.ts +55 -61
  70. package/src/commands/init.ts +101 -14
  71. package/src/commands/ls.ts +166 -0
  72. package/src/commands/overview.ts +15 -2
  73. package/src/commands/query.ts +643 -149
  74. package/src/commands/schema.ts +345 -0
  75. package/src/commands/server.ts +13 -6
  76. package/src/commands/stats.ts +7 -0
  77. package/src/commands/trace.ts +647 -6
  78. package/src/commands/types.ts +94 -0
  79. package/src/utils/formatNode.ts +42 -2
@@ -0,0 +1,612 @@
1
+ /**
2
+ * Diagnostic check functions for `grafema doctor` command - REG-214
3
+ *
4
+ * Checks are organized in levels:
5
+ * - Level 1: Prerequisites (fail-fast) - checkGrafemaInitialized, checkServerStatus
6
+ * - Level 2: Configuration - checkConfigValidity, checkEntrypoints
7
+ * - Level 3: Graph Health - checkDatabaseExists, checkGraphStats, checkConnectivity, checkFreshness
8
+ * - Level 4: Informational - checkVersions
9
+ */
10
+
11
+ import { existsSync, readFileSync, statSync } from 'fs';
12
+ import { join, dirname } from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import { createRequire } from 'module';
15
+ import {
16
+ RFDBServerBackend,
17
+ RFDBClient,
18
+ loadConfig,
19
+ GraphFreshnessChecker,
20
+ } from '@grafema/core';
21
+ import type { DoctorCheckResult } from './types.js';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+
26
+ // Valid built-in plugin names (for config validation)
27
+ const VALID_PLUGIN_NAMES = new Set([
28
+ // Discovery
29
+ 'SimpleProjectDiscovery', 'MonorepoServiceDiscovery', 'WorkspaceDiscovery',
30
+ // Indexing
31
+ 'JSModuleIndexer', 'RustModuleIndexer',
32
+ // Analysis
33
+ 'JSASTAnalyzer', 'ExpressRouteAnalyzer', 'SocketIOAnalyzer', 'DatabaseAnalyzer',
34
+ 'FetchAnalyzer', 'ServiceLayerAnalyzer', 'ReactAnalyzer', 'RustAnalyzer',
35
+ // Enrichment
36
+ 'MethodCallResolver', 'AliasTracker', 'ValueDomainAnalyzer', 'MountPointResolver',
37
+ 'PrefixEvaluator', 'InstanceOfResolver', 'ImportExportLinker', 'HTTPConnectionEnricher',
38
+ 'RustFFIEnricher',
39
+ // Validation
40
+ 'CallResolverValidator', 'EvalBanValidator', 'SQLInjectionValidator', 'ShadowingDetector',
41
+ 'GraphConnectivityValidator', 'DataFlowValidator', 'TypeScriptDeadCodeValidator',
42
+ ]);
43
+
44
+ // =============================================================================
45
+ // Level 1: Prerequisites (fail-fast)
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Check if .grafema directory exists with config file.
50
+ * FAIL if not initialized.
51
+ */
52
+ export async function checkGrafemaInitialized(
53
+ projectPath: string
54
+ ): Promise<DoctorCheckResult> {
55
+ const grafemaDir = join(projectPath, '.grafema');
56
+ const configYaml = join(grafemaDir, 'config.yaml');
57
+ const configJson = join(grafemaDir, 'config.json');
58
+
59
+ if (!existsSync(grafemaDir)) {
60
+ return {
61
+ name: 'initialization',
62
+ status: 'fail',
63
+ message: '.grafema directory not found',
64
+ recommendation: 'Run: grafema init',
65
+ };
66
+ }
67
+
68
+ if (!existsSync(configYaml) && !existsSync(configJson)) {
69
+ return {
70
+ name: 'initialization',
71
+ status: 'fail',
72
+ message: 'Config file not found',
73
+ recommendation: 'Run: grafema init',
74
+ };
75
+ }
76
+
77
+ const configFile = existsSync(configYaml) ? 'config.yaml' : 'config.json';
78
+ const deprecated = configFile === 'config.json';
79
+
80
+ return {
81
+ name: 'initialization',
82
+ status: deprecated ? 'warn' : 'pass',
83
+ message: `Config file: .grafema/${configFile}`,
84
+ recommendation: deprecated ? 'Run: grafema init --force (migrate to YAML)' : undefined,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Check if RFDB server is running and responsive.
90
+ * WARN if not running (server starts on-demand during analyze).
91
+ */
92
+ export async function checkServerStatus(
93
+ projectPath: string
94
+ ): Promise<DoctorCheckResult> {
95
+ const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
96
+
97
+ if (!existsSync(socketPath)) {
98
+ return {
99
+ name: 'server',
100
+ status: 'warn',
101
+ message: 'RFDB server not running',
102
+ recommendation: 'Run: grafema analyze (starts server automatically)',
103
+ };
104
+ }
105
+
106
+ const client = new RFDBClient(socketPath);
107
+ client.on('error', () => {}); // Suppress error events
108
+
109
+ try {
110
+ await client.connect();
111
+ const version = await client.ping();
112
+ await client.close();
113
+
114
+ return {
115
+ name: 'server',
116
+ status: 'pass',
117
+ message: `Server: connected (RFDB ${version || 'unknown'})`,
118
+ details: { version, socketPath },
119
+ };
120
+ } catch {
121
+ return {
122
+ name: 'server',
123
+ status: 'warn',
124
+ message: 'Server socket exists but not responding (stale)',
125
+ recommendation: 'Run: grafema analyze (will restart server)',
126
+ };
127
+ }
128
+ }
129
+
130
+ // =============================================================================
131
+ // Level 2: Configuration Validity
132
+ // =============================================================================
133
+
134
+ /**
135
+ * Validate config file syntax and structure.
136
+ * Uses existing loadConfig() which throws on errors.
137
+ */
138
+ export async function checkConfigValidity(
139
+ projectPath: string
140
+ ): Promise<DoctorCheckResult> {
141
+ try {
142
+ // Silent logger to suppress warnings during validation
143
+ const config = loadConfig(projectPath, { warn: () => {} });
144
+
145
+ // Check for unknown plugins
146
+ const unknownPlugins: string[] = [];
147
+ const phases = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'] as const;
148
+
149
+ for (const phase of phases) {
150
+ const plugins = config.plugins[phase] || [];
151
+ for (const name of plugins) {
152
+ if (!VALID_PLUGIN_NAMES.has(name)) {
153
+ unknownPlugins.push(name);
154
+ }
155
+ }
156
+ }
157
+
158
+ if (unknownPlugins.length > 0) {
159
+ return {
160
+ name: 'config',
161
+ status: 'warn',
162
+ message: `Unknown plugin(s): ${unknownPlugins.join(', ')}`,
163
+ recommendation: 'Check plugin names for typos. Run: grafema doctor --verbose for available plugins',
164
+ details: { unknownPlugins },
165
+ };
166
+ }
167
+
168
+ const totalPlugins = phases.reduce(
169
+ (sum, phase) => sum + (config.plugins[phase]?.length || 0), 0
170
+ );
171
+
172
+ return {
173
+ name: 'config',
174
+ status: 'pass',
175
+ message: `Config valid: ${totalPlugins} plugins configured`,
176
+ details: { pluginCount: totalPlugins, services: config.services.length },
177
+ };
178
+ } catch (err) {
179
+ const error = err instanceof Error ? err : new Error(String(err));
180
+ return {
181
+ name: 'config',
182
+ status: 'fail',
183
+ message: `Config error: ${error.message}`,
184
+ recommendation: 'Fix config.yaml syntax or run: grafema init --force',
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Check that entrypoints can be resolved.
191
+ * For config-defined services, validates that entrypoint files exist.
192
+ */
193
+ export async function checkEntrypoints(
194
+ projectPath: string
195
+ ): Promise<DoctorCheckResult> {
196
+ let config;
197
+ try {
198
+ config = loadConfig(projectPath, { warn: () => {} });
199
+ } catch {
200
+ // Config loading failed - already reported by checkConfigValidity
201
+ return {
202
+ name: 'entrypoints',
203
+ status: 'skip',
204
+ message: 'Skipped (config error)',
205
+ };
206
+ }
207
+
208
+ if (config.services.length === 0) {
209
+ // Auto-discovery mode - check package.json exists
210
+ const pkgJson = join(projectPath, 'package.json');
211
+ if (!existsSync(pkgJson)) {
212
+ return {
213
+ name: 'entrypoints',
214
+ status: 'warn',
215
+ message: 'No package.json found for auto-discovery',
216
+ recommendation: 'Add package.json or configure services in config.yaml',
217
+ };
218
+ }
219
+ return {
220
+ name: 'entrypoints',
221
+ status: 'pass',
222
+ message: 'Using auto-discovery mode',
223
+ };
224
+ }
225
+
226
+ // Config-defined services - validate each
227
+ const issues: string[] = [];
228
+ const valid: string[] = [];
229
+
230
+ for (const svc of config.services) {
231
+ const svcPath = join(projectPath, svc.path);
232
+ let entrypoint: string;
233
+
234
+ if (svc.entryPoint) {
235
+ entrypoint = join(svcPath, svc.entryPoint);
236
+ } else {
237
+ // Auto-detect from package.json
238
+ const pkgPath = join(svcPath, 'package.json');
239
+ if (existsSync(pkgPath)) {
240
+ try {
241
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
242
+ entrypoint = join(svcPath, pkg.main || 'index.js');
243
+ } catch {
244
+ entrypoint = join(svcPath, 'index.js');
245
+ }
246
+ } else {
247
+ entrypoint = join(svcPath, 'index.js');
248
+ }
249
+ }
250
+
251
+ if (existsSync(entrypoint)) {
252
+ valid.push(svc.name);
253
+ } else {
254
+ issues.push(`${svc.name}: ${entrypoint} not found`);
255
+ }
256
+ }
257
+
258
+ if (issues.length > 0) {
259
+ return {
260
+ name: 'entrypoints',
261
+ status: 'warn',
262
+ message: `${issues.length} service(s) with missing entrypoints`,
263
+ recommendation: 'Check service paths in config.yaml',
264
+ details: { issues, valid },
265
+ };
266
+ }
267
+
268
+ return {
269
+ name: 'entrypoints',
270
+ status: 'pass',
271
+ message: `Entrypoints: ${valid.length} service(s) found`,
272
+ details: { services: valid },
273
+ };
274
+ }
275
+
276
+ // =============================================================================
277
+ // Level 3: Graph Health
278
+ // =============================================================================
279
+
280
+ /**
281
+ * Check if database file exists and has data.
282
+ */
283
+ export async function checkDatabaseExists(
284
+ projectPath: string
285
+ ): Promise<DoctorCheckResult> {
286
+ const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
287
+
288
+ if (!existsSync(dbPath)) {
289
+ return {
290
+ name: 'database',
291
+ status: 'fail',
292
+ message: 'Database not found',
293
+ recommendation: 'Run: grafema analyze',
294
+ };
295
+ }
296
+
297
+ // Check file size (empty DB is typically < 100 bytes)
298
+ const stats = statSync(dbPath);
299
+ if (stats.size < 100) {
300
+ return {
301
+ name: 'database',
302
+ status: 'warn',
303
+ message: 'Database appears empty',
304
+ recommendation: 'Run: grafema analyze',
305
+ };
306
+ }
307
+
308
+ return {
309
+ name: 'database',
310
+ status: 'pass',
311
+ message: `Database: ${dbPath}`,
312
+ details: { size: stats.size },
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Get graph statistics (requires server running).
318
+ */
319
+ export async function checkGraphStats(
320
+ projectPath: string
321
+ ): Promise<DoctorCheckResult> {
322
+ const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
323
+ const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
324
+
325
+ if (!existsSync(socketPath)) {
326
+ return {
327
+ name: 'graph_stats',
328
+ status: 'skip',
329
+ message: 'Server not running (skipped stats check)',
330
+ };
331
+ }
332
+
333
+ const backend = new RFDBServerBackend({ dbPath });
334
+ try {
335
+ await backend.connect();
336
+ const stats = await backend.getStats();
337
+ await backend.close();
338
+
339
+ if (stats.nodeCount === 0) {
340
+ return {
341
+ name: 'graph_stats',
342
+ status: 'fail',
343
+ message: 'Database is empty (0 nodes)',
344
+ recommendation: 'Run: grafema analyze',
345
+ };
346
+ }
347
+
348
+ return {
349
+ name: 'graph_stats',
350
+ status: 'pass',
351
+ message: `Graph: ${stats.nodeCount.toLocaleString()} nodes, ${stats.edgeCount.toLocaleString()} edges`,
352
+ details: {
353
+ nodeCount: stats.nodeCount,
354
+ edgeCount: stats.edgeCount,
355
+ nodesByType: stats.nodesByType,
356
+ edgesByType: stats.edgesByType,
357
+ },
358
+ };
359
+ } catch (err) {
360
+ return {
361
+ name: 'graph_stats',
362
+ status: 'warn',
363
+ message: `Could not read graph stats: ${(err as Error).message}`,
364
+ };
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Check graph connectivity - find disconnected nodes.
370
+ * Thresholds:
371
+ * 0-5%: pass (normal for external modules)
372
+ * 5-20%: warn
373
+ * >20%: fail (critical issue)
374
+ */
375
+ export async function checkConnectivity(
376
+ projectPath: string
377
+ ): Promise<DoctorCheckResult> {
378
+ const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
379
+ const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
380
+
381
+ if (!existsSync(socketPath)) {
382
+ return {
383
+ name: 'connectivity',
384
+ status: 'skip',
385
+ message: 'Server not running (skipped connectivity check)',
386
+ };
387
+ }
388
+
389
+ const backend = new RFDBServerBackend({ dbPath });
390
+ try {
391
+ await backend.connect();
392
+
393
+ // Get all nodes
394
+ const allNodes: Array<{ id: string; type: string }> = [];
395
+ for await (const node of backend.queryNodes({})) {
396
+ allNodes.push({ id: node.id, type: node.type as string });
397
+ }
398
+ const totalCount = allNodes.length;
399
+
400
+ if (totalCount === 0) {
401
+ await backend.close();
402
+ return {
403
+ name: 'connectivity',
404
+ status: 'skip',
405
+ message: 'No nodes to check',
406
+ };
407
+ }
408
+
409
+ // Find root nodes (SERVICE, MODULE, PROJECT)
410
+ const rootTypes = ['SERVICE', 'MODULE', 'PROJECT'];
411
+ const rootNodes = allNodes.filter(n => rootTypes.includes(n.type));
412
+
413
+ if (rootNodes.length === 0) {
414
+ await backend.close();
415
+ return {
416
+ name: 'connectivity',
417
+ status: 'warn',
418
+ message: 'No root nodes found (SERVICE/MODULE/PROJECT)',
419
+ recommendation: 'Run: grafema analyze',
420
+ };
421
+ }
422
+
423
+ // Get all edges and build adjacency
424
+ const allEdges = await backend.getAllEdges();
425
+
426
+ const adjacencyOut = new Map<string, string[]>();
427
+ const adjacencyIn = new Map<string, string[]>();
428
+
429
+ for (const edge of allEdges) {
430
+ if (!adjacencyOut.has(edge.src)) adjacencyOut.set(edge.src, []);
431
+ adjacencyOut.get(edge.src)!.push(edge.dst);
432
+ if (!adjacencyIn.has(edge.dst)) adjacencyIn.set(edge.dst, []);
433
+ adjacencyIn.get(edge.dst)!.push(edge.src);
434
+ }
435
+
436
+ // BFS from roots
437
+ const reachable = new Set<string>();
438
+ const queue = [...rootNodes.map(n => n.id)];
439
+
440
+ while (queue.length > 0) {
441
+ const nodeId = queue.shift()!;
442
+ if (reachable.has(nodeId)) continue;
443
+ reachable.add(nodeId);
444
+ const outgoing = adjacencyOut.get(nodeId) || [];
445
+ const incoming = adjacencyIn.get(nodeId) || [];
446
+ for (const targetId of [...outgoing, ...incoming]) {
447
+ if (!reachable.has(targetId)) queue.push(targetId);
448
+ }
449
+ }
450
+
451
+ await backend.close();
452
+
453
+ const unreachableCount = totalCount - reachable.size;
454
+ const percentage = (unreachableCount / totalCount) * 100;
455
+
456
+ if (unreachableCount === 0) {
457
+ return {
458
+ name: 'connectivity',
459
+ status: 'pass',
460
+ message: 'All nodes connected',
461
+ details: { totalNodes: totalCount },
462
+ };
463
+ }
464
+
465
+ // Group unreachable by type
466
+ const unreachableNodes = allNodes.filter(n => !reachable.has(n.id));
467
+ const byType: Record<string, number> = {};
468
+ for (const node of unreachableNodes) {
469
+ byType[node.type] = (byType[node.type] || 0) + 1;
470
+ }
471
+
472
+ if (percentage > 20) {
473
+ return {
474
+ name: 'connectivity',
475
+ status: 'fail',
476
+ message: `Critical: ${unreachableCount} disconnected nodes (${percentage.toFixed(1)}%)`,
477
+ recommendation: 'Run: grafema analyze --clear (rebuild graph)',
478
+ details: { unreachableCount, percentage, byType },
479
+ };
480
+ }
481
+
482
+ if (percentage > 5) {
483
+ return {
484
+ name: 'connectivity',
485
+ status: 'warn',
486
+ message: `${unreachableCount} disconnected nodes (${percentage.toFixed(1)}%)`,
487
+ recommendation: 'Run: grafema analyze --clear (may fix)',
488
+ details: { unreachableCount, percentage, byType },
489
+ };
490
+ }
491
+
492
+ return {
493
+ name: 'connectivity',
494
+ status: 'pass',
495
+ message: `${unreachableCount} disconnected nodes (${percentage.toFixed(1)}% - normal)`,
496
+ details: { unreachableCount, percentage, byType },
497
+ };
498
+ } catch (err) {
499
+ return {
500
+ name: 'connectivity',
501
+ status: 'warn',
502
+ message: `Could not check connectivity: ${(err as Error).message}`,
503
+ };
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Check if graph is fresh (no stale modules).
509
+ */
510
+ export async function checkFreshness(
511
+ projectPath: string
512
+ ): Promise<DoctorCheckResult> {
513
+ const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
514
+ const dbPath = join(projectPath, '.grafema', 'graph.rfdb');
515
+
516
+ if (!existsSync(socketPath)) {
517
+ return {
518
+ name: 'freshness',
519
+ status: 'skip',
520
+ message: 'Server not running (skipped freshness check)',
521
+ };
522
+ }
523
+
524
+ const backend = new RFDBServerBackend({ dbPath });
525
+ try {
526
+ await backend.connect();
527
+ const freshnessChecker = new GraphFreshnessChecker();
528
+ const result = await freshnessChecker.checkFreshness(backend);
529
+ await backend.close();
530
+
531
+ if (result.isFresh) {
532
+ return {
533
+ name: 'freshness',
534
+ status: 'pass',
535
+ message: 'Graph is up to date',
536
+ };
537
+ }
538
+
539
+ return {
540
+ name: 'freshness',
541
+ status: 'warn',
542
+ message: `${result.staleCount} stale module(s) detected`,
543
+ recommendation: 'Run: grafema analyze (or grafema check for auto-reanalysis)',
544
+ details: {
545
+ staleCount: result.staleCount,
546
+ staleModules: result.staleModules.slice(0, 5).map(m => m.file),
547
+ },
548
+ };
549
+ } catch (err) {
550
+ return {
551
+ name: 'freshness',
552
+ status: 'warn',
553
+ message: `Could not check freshness: ${(err as Error).message}`,
554
+ };
555
+ }
556
+ }
557
+
558
+ // =============================================================================
559
+ // Level 4: Informational
560
+ // =============================================================================
561
+
562
+ /**
563
+ * Collect version information (always passes).
564
+ */
565
+ export async function checkVersions(
566
+ projectPath: string
567
+ ): Promise<DoctorCheckResult> {
568
+ let cliVersion = 'unknown';
569
+ let coreVersion = 'unknown';
570
+ let rfdbVersion: string | undefined;
571
+
572
+ // Read CLI version - from dist/commands/doctor/ go up 3 levels to cli/
573
+ try {
574
+ const cliPkgPath = join(__dirname, '../../../package.json');
575
+ const cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'));
576
+ cliVersion = cliPkg.version;
577
+ } catch {
578
+ // Ignore errors
579
+ }
580
+
581
+ // Read core version
582
+ try {
583
+ const require = createRequire(import.meta.url);
584
+ const corePkgPath = require.resolve('@grafema/core/package.json');
585
+ const corePkg = JSON.parse(readFileSync(corePkgPath, 'utf-8'));
586
+ coreVersion = corePkg.version;
587
+ } catch {
588
+ // Ignore errors
589
+ }
590
+
591
+ // Get RFDB version from server if running
592
+ const socketPath = join(projectPath, '.grafema', 'rfdb.sock');
593
+ if (existsSync(socketPath)) {
594
+ const client = new RFDBClient(socketPath);
595
+ client.on('error', () => {});
596
+ try {
597
+ await client.connect();
598
+ const version = await client.ping();
599
+ rfdbVersion = version || undefined;
600
+ await client.close();
601
+ } catch {
602
+ // Ignore errors
603
+ }
604
+ }
605
+
606
+ return {
607
+ name: 'versions',
608
+ status: 'pass',
609
+ message: `CLI ${cliVersion}, Core ${coreVersion}${rfdbVersion ? `, RFDB ${rfdbVersion}` : ''}`,
610
+ details: { cli: cliVersion, core: coreVersion, rfdb: rfdbVersion },
611
+ };
612
+ }