@grafema/cli 0.2.11 → 0.3.0-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 (133) hide show
  1. package/dist/cli.js +13 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/analyze.d.ts.map +1 -1
  4. package/dist/commands/analyze.js +2 -4
  5. package/dist/commands/analyze.js.map +1 -1
  6. package/dist/commands/analyzeAction.d.ts +5 -3
  7. package/dist/commands/analyzeAction.d.ts.map +1 -1
  8. package/dist/commands/analyzeAction.js +109 -151
  9. package/dist/commands/analyzeAction.js.map +1 -1
  10. package/dist/commands/check.d.ts +1 -1
  11. package/dist/commands/check.js +4 -4
  12. package/dist/commands/check.js.map +1 -1
  13. package/dist/commands/context.js +2 -2
  14. package/dist/commands/context.js.map +1 -1
  15. package/dist/commands/coverage.js +2 -2
  16. package/dist/commands/coverage.js.map +1 -1
  17. package/dist/commands/describe.d.ts +13 -0
  18. package/dist/commands/describe.d.ts.map +1 -0
  19. package/dist/commands/describe.js +131 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/doctor/checks.d.ts +6 -1
  22. package/dist/commands/doctor/checks.d.ts.map +1 -1
  23. package/dist/commands/doctor/checks.js +128 -13
  24. package/dist/commands/doctor/checks.js.map +1 -1
  25. package/dist/commands/doctor.d.ts +10 -9
  26. package/dist/commands/doctor.d.ts.map +1 -1
  27. package/dist/commands/doctor.js +12 -10
  28. package/dist/commands/doctor.js.map +1 -1
  29. package/dist/commands/explain.js +2 -2
  30. package/dist/commands/explain.js.map +1 -1
  31. package/dist/commands/file.js +2 -2
  32. package/dist/commands/file.js.map +1 -1
  33. package/dist/commands/get.js +2 -2
  34. package/dist/commands/get.js.map +1 -1
  35. package/dist/commands/git-ingest.d.ts +6 -0
  36. package/dist/commands/git-ingest.d.ts.map +1 -0
  37. package/dist/commands/git-ingest.js +46 -0
  38. package/dist/commands/git-ingest.js.map +1 -0
  39. package/dist/commands/impact.d.ts.map +1 -1
  40. package/dist/commands/impact.js +276 -50
  41. package/dist/commands/impact.js.map +1 -1
  42. package/dist/commands/init.d.ts.map +1 -1
  43. package/dist/commands/init.js +20 -22
  44. package/dist/commands/init.js.map +1 -1
  45. package/dist/commands/ls.js +2 -2
  46. package/dist/commands/ls.js.map +1 -1
  47. package/dist/commands/overview.js +2 -2
  48. package/dist/commands/overview.js.map +1 -1
  49. package/dist/commands/query.d.ts +1 -1
  50. package/dist/commands/query.d.ts.map +1 -1
  51. package/dist/commands/query.js +169 -7
  52. package/dist/commands/query.js.map +1 -1
  53. package/dist/commands/schema.js +2 -2
  54. package/dist/commands/schema.js.map +1 -1
  55. package/dist/commands/server.d.ts.map +1 -1
  56. package/dist/commands/server.js +122 -76
  57. package/dist/commands/server.js.map +1 -1
  58. package/dist/commands/stats.js +2 -2
  59. package/dist/commands/stats.js.map +1 -1
  60. package/dist/commands/tldr.d.ts +12 -0
  61. package/dist/commands/tldr.d.ts.map +1 -0
  62. package/dist/commands/tldr.js +81 -0
  63. package/dist/commands/tldr.js.map +1 -0
  64. package/dist/commands/trace.d.ts +1 -1
  65. package/dist/commands/trace.d.ts.map +1 -1
  66. package/dist/commands/trace.js +17 -133
  67. package/dist/commands/trace.js.map +1 -1
  68. package/dist/commands/types.js +2 -2
  69. package/dist/commands/types.js.map +1 -1
  70. package/dist/commands/who.d.ts +12 -0
  71. package/dist/commands/who.d.ts.map +1 -0
  72. package/dist/commands/who.js +184 -0
  73. package/dist/commands/who.js.map +1 -0
  74. package/dist/commands/why.d.ts +12 -0
  75. package/dist/commands/why.d.ts.map +1 -0
  76. package/dist/commands/why.js +118 -0
  77. package/dist/commands/why.js.map +1 -0
  78. package/dist/commands/wtf.d.ts +12 -0
  79. package/dist/commands/wtf.d.ts.map +1 -0
  80. package/dist/commands/wtf.js +117 -0
  81. package/dist/commands/wtf.js.map +1 -0
  82. package/dist/plugins/builtinPlugins.d.ts +1 -9
  83. package/dist/plugins/builtinPlugins.d.ts.map +1 -1
  84. package/dist/plugins/builtinPlugins.js +2 -67
  85. package/dist/plugins/builtinPlugins.js.map +1 -1
  86. package/dist/plugins/pluginLoader.d.ts +1 -15
  87. package/dist/plugins/pluginLoader.d.ts.map +1 -1
  88. package/dist/plugins/pluginLoader.js +2 -100
  89. package/dist/plugins/pluginLoader.js.map +1 -1
  90. package/dist/plugins/pluginResolver.js +3 -3
  91. package/dist/utils/progressRenderer.d.ts +15 -1
  92. package/dist/utils/progressRenderer.d.ts.map +1 -1
  93. package/dist/utils/progressRenderer.js +19 -3
  94. package/dist/utils/progressRenderer.js.map +1 -1
  95. package/dist/utils/queryHints.d.ts +6 -0
  96. package/dist/utils/queryHints.d.ts.map +1 -0
  97. package/dist/utils/queryHints.js +36 -0
  98. package/dist/utils/queryHints.js.map +1 -0
  99. package/package.json +4 -4
  100. package/skills/grafema-codebase-analysis/SKILL.md +1 -1
  101. package/src/cli.ts +14 -0
  102. package/src/commands/analyze.ts +2 -4
  103. package/src/commands/analyzeAction.ts +122 -168
  104. package/src/commands/check.ts +5 -5
  105. package/src/commands/context.ts +3 -3
  106. package/src/commands/coverage.ts +2 -2
  107. package/src/commands/describe.ts +160 -0
  108. package/src/commands/doctor/checks.ts +153 -10
  109. package/src/commands/doctor.ts +13 -9
  110. package/src/commands/explain.ts +2 -2
  111. package/src/commands/explore.tsx +2 -2
  112. package/src/commands/file.ts +3 -3
  113. package/src/commands/get.ts +2 -2
  114. package/src/commands/git-ingest.ts +49 -0
  115. package/src/commands/impact.ts +318 -55
  116. package/src/commands/init.ts +20 -22
  117. package/src/commands/ls.ts +2 -2
  118. package/src/commands/overview.ts +2 -2
  119. package/src/commands/query.ts +197 -7
  120. package/src/commands/schema.ts +2 -2
  121. package/src/commands/server.ts +136 -84
  122. package/src/commands/stats.ts +2 -2
  123. package/src/commands/tldr.ts +103 -0
  124. package/src/commands/trace.ts +19 -161
  125. package/src/commands/types.ts +2 -2
  126. package/src/commands/who.ts +215 -0
  127. package/src/commands/why.ts +134 -0
  128. package/src/commands/wtf.ts +140 -0
  129. package/src/plugins/builtinPlugins.ts +1 -108
  130. package/src/plugins/pluginLoader.ts +1 -123
  131. package/src/plugins/pluginResolver.js +3 -3
  132. package/src/utils/progressRenderer.ts +34 -4
  133. package/src/utils/queryHints.ts +46 -0
@@ -13,10 +13,12 @@ import { Command } from 'commander';
13
13
  import { resolve, join, basename } from 'path';
14
14
  import { toRelativeDisplay } from '../utils/pathUtils.js';
15
15
  import { existsSync } from 'fs';
16
- import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
16
+ import { RFDBServerBackend, parseSemanticId, parseSemanticIdV2, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/util';
17
17
  import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
18
18
  import { exitWithError } from '../utils/errorFormatter.js';
19
19
  import { Spinner } from '../utils/spinner.js';
20
+ import { extractQueriedTypes, findSimilarTypes } from '../utils/queryHints.js';
21
+ import type { DatalogExplainResult, CypherResult } from '@grafema/types';
20
22
 
21
23
  // Node type constants to avoid magic string duplication
22
24
  const HTTP_ROUTE_TYPE = 'http:route';
@@ -32,6 +34,8 @@ interface QueryOptions {
32
34
  json?: boolean;
33
35
  limit: string;
34
36
  raw?: boolean;
37
+ cypher?: boolean;
38
+ explain?: boolean;
35
39
  type?: string; // Explicit node type (bypasses type aliases)
36
40
  }
37
41
 
@@ -106,6 +110,26 @@ Rules (must define violation/1):
106
110
  grafema query --raw 'violation(X) :- node(X, "FUNCTION").'
107
111
  grafema query --raw 'violation(X) :- node(X, "http:route"), attr(X, "method", "POST").'`
108
112
  )
113
+ .option(
114
+ '--cypher',
115
+ `Execute a Cypher query instead of Datalog
116
+
117
+ Cypher is a graph query language with pattern-matching syntax.
118
+
119
+ Examples:
120
+ grafema query --cypher 'MATCH (n:FUNCTION) RETURN n.name LIMIT 10'
121
+ grafema query --cypher 'MATCH (a)-[:CALLS]->(b) RETURN a.name, b.name'`
122
+ )
123
+ .option(
124
+ '--explain',
125
+ `Show step-by-step query execution (use with --raw)
126
+
127
+ Displays each predicate evaluation, result counts, and timing.
128
+ Useful when a query returns no results — shows where the funnel drops to zero.
129
+
130
+ Example:
131
+ grafema query --raw 'type(X, "FUNCTION"), attr(X, "name", "main")' --explain`
132
+ )
109
133
  .option(
110
134
  '-t, --type <nodeType>',
111
135
  `Filter by exact node type (bypasses type aliases)
@@ -135,6 +159,7 @@ Examples:
135
159
  grafema query --type FUNCTION "auth" Explicit type (no alias resolution)
136
160
  grafema query -t http:request "/api" Search custom node types
137
161
  grafema query --raw 'type(X, "FUNCTION")' Raw Datalog query
162
+ grafema query --cypher 'MATCH (n:FUNCTION) RETURN n.name' Cypher query
138
163
  `)
139
164
  .action(async (pattern: string, options: QueryOptions) => {
140
165
  const projectPath = resolve(options.project);
@@ -145,7 +170,7 @@ Examples:
145
170
  exitWithError('No graph database found', ['Run: grafema analyze']);
146
171
  }
147
172
 
148
- const backend = new RFDBServerBackend({ dbPath });
173
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
149
174
  await backend.connect();
150
175
 
151
176
  const spinner = new Spinner('Querying graph...');
@@ -158,10 +183,23 @@ Examples:
158
183
  exitWithError('Invalid limit', ['Use a positive number, e.g.: --limit 10']);
159
184
  }
160
185
 
186
+ // --explain only works with --raw
187
+ if (options.explain && !options.raw) {
188
+ spinner.stop();
189
+ console.error('Note: --explain requires --raw. Ignoring --explain.');
190
+ }
191
+
192
+ // Cypher mode
193
+ if (options.cypher) {
194
+ spinner.stop();
195
+ await executeCypherQuery(backend, pattern, limit, options.json);
196
+ return;
197
+ }
198
+
161
199
  // Raw Datalog mode
162
200
  if (options.raw) {
163
201
  spinner.stop();
164
- await executeRawQuery(backend, pattern, limit, options.json);
202
+ await executeRawQuery(backend, pattern, limit, options.json, options.explain);
165
203
  return;
166
204
  }
167
205
 
@@ -373,7 +411,7 @@ export function isFileScope(scope: string): boolean {
373
411
  /**
374
412
  * Check if a semantic ID matches the given scope constraints.
375
413
  *
376
- * Uses parseSemanticId from @grafema/core for robust ID parsing.
414
+ * Uses parseSemanticId from @grafema/util for robust ID parsing.
377
415
  *
378
416
  * Scope matching rules:
379
417
  * - File scope: semantic ID must match the file path (full or basename)
@@ -737,7 +775,7 @@ async function getCallers(
737
775
  const callNode = await backend.getNode(edge.src);
738
776
  if (!callNode) continue;
739
777
 
740
- // Find the FUNCTION that contains this CALL (use shared utility from @grafema/core)
778
+ // Find the FUNCTION that contains this CALL (use shared utility from @grafema/util)
741
779
  const containingFunc = await findContainingFunctionCore(backend, callNode.id);
742
780
 
743
781
  if (containingFunc && !seen.has(containingFunc.id)) {
@@ -763,7 +801,7 @@ async function getCallers(
763
801
  /**
764
802
  * Get functions that this node calls
765
803
  *
766
- * Uses shared utility from @grafema/core which:
804
+ * Uses shared utility from @grafema/util which:
767
805
  * - Follows HAS_SCOPE -> SCOPE -> CONTAINS pattern correctly
768
806
  * - Finds both CALL and METHOD_CALL nodes
769
807
  * - Only returns resolved calls (those with CALLS edges to targets)
@@ -1070,6 +1108,58 @@ export function getUnknownPredicates(query: string): string[] {
1070
1108
  return predicates.filter(p => !BUILTIN_PREDICATES.has(p) && !ruleHeads.has(p));
1071
1109
  }
1072
1110
 
1111
+ /**
1112
+ * Execute Cypher query and display results in tabular format.
1113
+ */
1114
+ async function executeCypherQuery(
1115
+ backend: RFDBServerBackend,
1116
+ query: string,
1117
+ limit: number,
1118
+ json?: boolean,
1119
+ ): Promise<void> {
1120
+ const result: CypherResult = await backend.cypherQuery(query);
1121
+
1122
+ if (json) {
1123
+ console.log(JSON.stringify(result, null, 2));
1124
+ return;
1125
+ }
1126
+
1127
+ if (result.rowCount === 0) {
1128
+ console.log('No results.');
1129
+ return;
1130
+ }
1131
+
1132
+ const limited = result.rows.slice(0, limit);
1133
+
1134
+ // Calculate column widths for tabular display
1135
+ const colWidths = result.columns.map((col, i) => {
1136
+ let maxWidth = col.length;
1137
+ for (const row of limited) {
1138
+ const cellLen = String(row[i] ?? '').length;
1139
+ if (cellLen > maxWidth) maxWidth = cellLen;
1140
+ }
1141
+ return Math.min(maxWidth, 60); // cap at 60 chars
1142
+ });
1143
+
1144
+ // Header
1145
+ const header = result.columns.map((col, i) => col.padEnd(colWidths[i])).join(' ');
1146
+ const separator = colWidths.map(w => '-'.repeat(w)).join(' ');
1147
+ console.log(header);
1148
+ console.log(separator);
1149
+
1150
+ // Rows
1151
+ for (const row of limited) {
1152
+ const line = row.map((cell, i) => {
1153
+ const s = String(cell ?? '');
1154
+ return s.length > colWidths[i] ? s.slice(0, colWidths[i] - 1) + '\u2026' : s.padEnd(colWidths[i]);
1155
+ }).join(' ');
1156
+ console.log(line);
1157
+ }
1158
+
1159
+ console.log('');
1160
+ console.log(`${limited.length}${result.rowCount > limit ? ` of ${result.rowCount}` : ''} row(s)`);
1161
+ }
1162
+
1073
1163
  /**
1074
1164
  * Execute raw Datalog query.
1075
1165
  * Uses unified executeDatalog endpoint which auto-detects rules vs direct queries.
@@ -1078,8 +1168,19 @@ async function executeRawQuery(
1078
1168
  backend: RFDBServerBackend,
1079
1169
  query: string,
1080
1170
  limit: number,
1081
- json?: boolean
1171
+ json?: boolean,
1172
+ explain?: boolean,
1082
1173
  ): Promise<void> {
1174
+ if (explain) {
1175
+ const result = await backend.executeDatalog(query, true);
1176
+ if (json) {
1177
+ console.log(JSON.stringify(result, null, 2));
1178
+ return;
1179
+ }
1180
+ renderExplainOutput(result, limit);
1181
+ return;
1182
+ }
1183
+
1083
1184
  const results = await backend.executeDatalog(query);
1084
1185
  const limited = results.slice(0, limit);
1085
1186
 
@@ -1106,5 +1207,94 @@ async function executeRawQuery(
1106
1207
  const builtinList = [...BUILTIN_PREDICATES].join(', ');
1107
1208
  console.error(`Note: unknown predicate ${unknownList}. Built-in predicates: ${builtinList}`);
1108
1209
  }
1210
+
1211
+ // Type suggestions: only if there are type literals in the query
1212
+ const { nodeTypes, edgeTypes } = extractQueriedTypes(query);
1213
+ if (nodeTypes.length > 0 || edgeTypes.length > 0) {
1214
+ const nodeCounts = nodeTypes.length > 0 ? await backend.countNodesByType() : {};
1215
+ const edgeCounts = edgeTypes.length > 0 ? await backend.countEdgesByType() : {};
1216
+ const availableNodeTypes = Object.keys(nodeCounts);
1217
+ const availableEdgeTypes = Object.keys(edgeCounts);
1218
+
1219
+ if (nodeTypes.length > 0 && availableNodeTypes.length === 0) {
1220
+ console.error('Note: graph has no nodes');
1221
+ } else {
1222
+ for (const queriedType of nodeTypes) {
1223
+ if (!nodeCounts[queriedType]) {
1224
+ const similar = findSimilarTypes(queriedType, availableNodeTypes);
1225
+ if (similar.length > 0) {
1226
+ console.error(`Note: unknown node type "${queriedType}". Did you mean: ${similar.join(', ')}?`);
1227
+ } else {
1228
+ const typeList = availableNodeTypes.slice(0, 10).join(', ');
1229
+ const more = availableNodeTypes.length > 10 ? '...' : '';
1230
+ console.error(`Note: unknown node type "${queriedType}". Available: ${typeList}${more}`);
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ if (edgeTypes.length > 0 && availableEdgeTypes.length === 0) {
1237
+ console.error('Note: graph has no edges');
1238
+ } else {
1239
+ for (const queriedType of edgeTypes) {
1240
+ if (!edgeCounts[queriedType]) {
1241
+ const similar = findSimilarTypes(queriedType, availableEdgeTypes);
1242
+ if (similar.length > 0) {
1243
+ console.error(`Note: unknown edge type "${queriedType}". Did you mean: ${similar.join(', ')}?`);
1244
+ } else {
1245
+ const typeList = availableEdgeTypes.slice(0, 10).join(', ');
1246
+ const more = availableEdgeTypes.length > 10 ? '...' : '';
1247
+ console.error(`Note: unknown edge type "${queriedType}". Available: ${typeList}${more}`);
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ function renderExplainOutput(result: DatalogExplainResult, limit: number): void {
1257
+ // Print warnings to stderr first so they're immediately visible
1258
+ if (result.warnings && result.warnings.length > 0) {
1259
+ console.error('Warnings:');
1260
+ for (const warning of result.warnings) {
1261
+ console.error(` ${warning}`);
1262
+ }
1263
+ console.error('');
1264
+ }
1265
+
1266
+ console.log('Explain mode \u2014 step-by-step execution:\n');
1267
+
1268
+ for (const step of result.explainSteps) {
1269
+ const args = step.args.join(', ');
1270
+ console.log(` Step ${step.step}: [${step.operation}] ${step.predicate}(${args})`);
1271
+ console.log(` \u2192 ${step.resultCount} result(s) in ${step.durationUs} \u00b5s`);
1272
+ if (step.details) {
1273
+ console.log(` ${step.details}`);
1274
+ }
1275
+ console.log('');
1276
+ }
1277
+
1278
+ console.log('Query statistics:');
1279
+ console.log(` Nodes visited: ${result.stats.nodesVisited}`);
1280
+ console.log(` Edges traversed: ${result.stats.edgesTraversed}`);
1281
+ console.log(` Rule evaluations: ${result.stats.ruleEvaluations}`);
1282
+ console.log(` Total results: ${result.stats.totalResults}`);
1283
+ console.log(` Total duration: ${result.profile.totalDurationUs} \u00b5s`);
1284
+ if (result.profile.ruleEvalTimeUs === 0 && result.profile.projectionTimeUs === 0) {
1285
+ console.log(' (rule_eval_time and projection_time: not yet tracked)');
1286
+ }
1287
+ console.log('');
1288
+
1289
+ const bindingsToShow = result.bindings.slice(0, limit);
1290
+ if (bindingsToShow.length === 0) {
1291
+ console.log('No results.');
1292
+ } else {
1293
+ console.log(`Results (${bindingsToShow.length}${result.bindings.length > limit ? ` of ${result.bindings.length}` : ''}):`);
1294
+ console.log('');
1295
+ for (const row of bindingsToShow) {
1296
+ const pairs = Object.entries(row).map(([k, v]) => `${k}=${v}`).join(', ');
1297
+ console.log(` { ${pairs} }`);
1298
+ }
1109
1299
  }
1110
1300
  }
@@ -21,7 +21,7 @@ import {
21
21
  type GraphSchema,
22
22
  type NodeTypeSchema,
23
23
  type EdgeTypeSchema,
24
- } from '@grafema/core';
24
+ } from '@grafema/util';
25
25
  import { exitWithError } from '../utils/errorFormatter.js';
26
26
 
27
27
  interface ExportOptions {
@@ -262,7 +262,7 @@ const exportSubcommand = new Command('export')
262
262
  exitWithError('No graph database found', ['Run: grafema analyze']);
263
263
  }
264
264
 
265
- const backend = new RFDBServerBackend({ dbPath });
265
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
266
266
  await backend.connect();
267
267
 
268
268
  try {
@@ -10,10 +10,9 @@
10
10
 
11
11
  import { Command } from 'commander';
12
12
  import { resolve, join } from 'path';
13
- import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
14
- import { spawn } from 'child_process';
13
+ import { existsSync, unlinkSync, readFileSync } from 'fs';
15
14
  import { setTimeout as sleep } from 'timers/promises';
16
- import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary } from '@grafema/core';
15
+ import { RFDBClient, loadConfig, RFDBServerBackend, findRfdbBinary, startRfdbServer } from '@grafema/util';
17
16
  import { exitWithError } from '../utils/errorFormatter.js';
18
17
 
19
18
  // Extend config type for server settings
@@ -41,7 +40,7 @@ async function isServerRunning(socketPath: string): Promise<{ running: boolean;
41
40
  return { running: false };
42
41
  }
43
42
 
44
- const client = new RFDBClient(socketPath);
43
+ const client = new RFDBClient(socketPath, 'cli');
45
44
  // Suppress error events (we handle via try/catch)
46
45
  client.on('error', () => {});
47
46
 
@@ -67,6 +66,55 @@ function getProjectPaths(projectPath: string) {
67
66
  return { grafemaDir, socketPath, dbPath, pidPath };
68
67
  }
69
68
 
69
+ /**
70
+ * Resolve RFDB binary path: CLI flag > config > auto-detect
71
+ */
72
+ function resolveBinaryPath(projectPath: string, explicitBinary?: string): string | null {
73
+ if (explicitBinary) {
74
+ return findServerBinary(explicitBinary);
75
+ }
76
+
77
+ // Try config
78
+ try {
79
+ const config = loadConfig(projectPath);
80
+ const serverConfig = (config as unknown as { server?: ServerConfig }).server;
81
+ if (serverConfig?.binaryPath) {
82
+ return findServerBinary(serverConfig.binaryPath);
83
+ }
84
+ } catch {
85
+ // Config not found or invalid - continue with auto-detect
86
+ }
87
+
88
+ return findServerBinary();
89
+ }
90
+
91
+ /**
92
+ * Stop a running RFDB server: send shutdown, wait for socket removal, clean PID
93
+ */
94
+ async function stopRunningServer(socketPath: string, pidPath: string): Promise<void> {
95
+ const client = new RFDBClient(socketPath, 'cli');
96
+ client.on('error', () => {});
97
+
98
+ try {
99
+ await client.connect();
100
+ await client.shutdown();
101
+ } catch {
102
+ // Expected - server closes connection
103
+ }
104
+
105
+ // Wait for socket to disappear
106
+ let attempts = 0;
107
+ while (existsSync(socketPath) && attempts < 30) {
108
+ await sleep(100);
109
+ attempts++;
110
+ }
111
+
112
+ // Clean up PID file
113
+ if (existsSync(pidPath)) {
114
+ unlinkSync(pidPath);
115
+ }
116
+ }
117
+
70
118
  // Create main server command with subcommands
71
119
  export const serverCommand = new Command('server')
72
120
  .description('Manage RFDB (Rega Flow Database) server lifecycle')
@@ -111,34 +159,7 @@ serverCommand
111
159
  return;
112
160
  }
113
161
 
114
- // Remove stale socket if exists
115
- if (existsSync(socketPath)) {
116
- unlinkSync(socketPath);
117
- }
118
-
119
- // Determine binary path: CLI flag > config > auto-detect
120
- let binaryPath: string | null = null;
121
-
122
- if (options.binary) {
123
- // Explicit --binary flag
124
- binaryPath = findServerBinary(options.binary);
125
- } else {
126
- // Try to read from config
127
- try {
128
- const config = loadConfig(projectPath);
129
- const serverConfig = (config as unknown as { server?: ServerConfig }).server;
130
- if (serverConfig?.binaryPath) {
131
- binaryPath = findServerBinary(serverConfig.binaryPath);
132
- }
133
- } catch {
134
- // Config not found or invalid - continue with auto-detect
135
- }
136
-
137
- // Auto-detect if not specified
138
- if (!binaryPath) {
139
- binaryPath = findServerBinary();
140
- }
141
- }
162
+ const binaryPath = resolveBinaryPath(projectPath, options.binary);
142
163
 
143
164
  if (!binaryPath) {
144
165
  exitWithError('RFDB server binary not found', [
@@ -156,33 +177,19 @@ serverCommand
156
177
  console.log(` Database: ${dbPath}`);
157
178
  console.log(` Socket: ${socketPath}`);
158
179
 
159
- // Start server
160
- const serverProcess = spawn(binaryPath, [dbPath, '--socket', socketPath], {
161
- stdio: ['ignore', 'pipe', 'pipe'],
162
- detached: true,
180
+ // Start server using shared utility
181
+ const serverProcess = await startRfdbServer({
182
+ dbPath,
183
+ socketPath,
184
+ binaryPath,
185
+ pidPath,
186
+ waitTimeoutMs: 10000,
163
187
  });
164
188
 
165
- // Don't let server process prevent parent from exiting
166
- serverProcess.unref();
167
-
168
- // Write PID file
169
- if (serverProcess.pid) {
170
- writeFileSync(pidPath, String(serverProcess.pid));
171
- }
172
-
173
- // Wait for socket to appear (increased timeout for slow systems)
174
- let attempts = 0;
175
- while (!existsSync(socketPath) && attempts < 100) {
176
- await sleep(100);
177
- attempts++;
178
- }
179
-
180
- if (!existsSync(socketPath)) {
181
- exitWithError('Server failed to start', [
182
- 'Check if database path is valid',
183
- 'Check server logs for errors',
184
- `Binary used: ${binaryPath}`
185
- ]);
189
+ if (serverProcess === null) {
190
+ // Existing server detected via PID file
191
+ console.log('Server already running (detected via PID file)');
192
+ return;
186
193
  }
187
194
 
188
195
  // Verify server is responsive
@@ -227,31 +234,7 @@ serverCommand
227
234
  }
228
235
 
229
236
  console.log('Stopping RFDB server...');
230
-
231
- // Send shutdown command
232
- const client = new RFDBClient(socketPath);
233
- // Suppress error events (server closes connection on shutdown)
234
- client.on('error', () => {});
235
-
236
- try {
237
- await client.connect();
238
- await client.shutdown();
239
- } catch {
240
- // Expected - server closes connection
241
- }
242
-
243
- // Wait for socket to disappear
244
- let attempts = 0;
245
- while (existsSync(socketPath) && attempts < 30) {
246
- await sleep(100);
247
- attempts++;
248
- }
249
-
250
- // Clean up PID file
251
- if (existsSync(pidPath)) {
252
- unlinkSync(pidPath);
253
- }
254
-
237
+ await stopRunningServer(socketPath, pidPath);
255
238
  console.log('Server stopped');
256
239
  });
257
240
 
@@ -285,7 +268,7 @@ serverCommand
285
268
  let nodeCount: number | undefined;
286
269
  let edgeCount: number | undefined;
287
270
  if (status.running) {
288
- const client = new RFDBClient(socketPath);
271
+ const client = new RFDBClient(socketPath, 'cli');
289
272
  client.on('error', () => {}); // Suppress error events
290
273
 
291
274
  try {
@@ -341,6 +324,75 @@ serverCommand
341
324
  }
342
325
  });
343
326
 
327
+ // grafema server restart
328
+ serverCommand
329
+ .command('restart')
330
+ .description('Restart the RFDB server (stop if running, then start)')
331
+ .option('-p, --project <path>', 'Project path', '.')
332
+ .option('-b, --binary <path>', 'Path to rfdb-server binary')
333
+ .action(async (options: { project: string; binary?: string }) => {
334
+ const projectPath = resolve(options.project);
335
+ const { grafemaDir, socketPath, dbPath, pidPath } = getProjectPaths(projectPath);
336
+
337
+ // Check if grafema is initialized
338
+ if (!existsSync(grafemaDir)) {
339
+ exitWithError('Grafema not initialized', [
340
+ 'Run: grafema init',
341
+ 'Or: grafema analyze (initializes automatically)'
342
+ ]);
343
+ }
344
+
345
+ // Stop server if running
346
+ const status = await isServerRunning(socketPath);
347
+ if (status.running) {
348
+ console.log('Stopping RFDB server...');
349
+ await stopRunningServer(socketPath, pidPath);
350
+ console.log('Server stopped');
351
+ }
352
+
353
+ const binaryPath = resolveBinaryPath(projectPath, options.binary);
354
+
355
+ if (!binaryPath) {
356
+ exitWithError('RFDB server binary not found', [
357
+ 'Specify path: grafema server restart --binary /path/to/rfdb-server',
358
+ 'Or add to config.yaml:',
359
+ ' server:',
360
+ ' binaryPath: /path/to/rfdb-server',
361
+ 'Or install: npm install @grafema/rfdb',
362
+ 'Or build: cargo build --release && cp target/release/rfdb-server ~/.local/bin/'
363
+ ]);
364
+ }
365
+
366
+ console.log('Starting RFDB server...');
367
+ console.log(` Binary: ${binaryPath}`);
368
+ console.log(` Database: ${dbPath}`);
369
+ console.log(` Socket: ${socketPath}`);
370
+
371
+ const serverProcess = await startRfdbServer({
372
+ dbPath,
373
+ socketPath,
374
+ binaryPath,
375
+ pidPath,
376
+ waitTimeoutMs: 10000,
377
+ });
378
+
379
+ const verifyStatus = await isServerRunning(socketPath);
380
+ if (!verifyStatus.running) {
381
+ exitWithError('Server started but not responding', [
382
+ 'Check server logs for errors'
383
+ ]);
384
+ }
385
+
386
+ console.log('');
387
+ console.log(`Server restarted successfully`);
388
+ if (verifyStatus.version) {
389
+ console.log(` Version: ${verifyStatus.version}`);
390
+ }
391
+ if (serverProcess?.pid) {
392
+ console.log(` PID: ${serverProcess.pid}`);
393
+ }
394
+ });
395
+
344
396
  // grafema server graphql
345
397
  serverCommand
346
398
  .command('graphql')
@@ -362,7 +414,7 @@ serverCommand
362
414
  }
363
415
 
364
416
  // Create backend connection
365
- const backend = new RFDBServerBackend({ socketPath });
417
+ const backend = new RFDBServerBackend({ socketPath, clientName: 'cli' });
366
418
  await backend.connect();
367
419
 
368
420
  // Import and start GraphQL server
@@ -5,7 +5,7 @@
5
5
  import { Command } from 'commander';
6
6
  import { resolve, join } from 'path';
7
7
  import { existsSync } from 'fs';
8
- import { RFDBServerBackend } from '@grafema/core';
8
+ import { RFDBServerBackend } from '@grafema/util';
9
9
  import { exitWithError } from '../utils/errorFormatter.js';
10
10
 
11
11
  export const statsCommand = new Command('stats')
@@ -29,7 +29,7 @@ Examples:
29
29
  exitWithError('No graph database found', ['Run: grafema analyze']);
30
30
  }
31
31
 
32
- const backend = new RFDBServerBackend({ dbPath });
32
+ const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
33
33
  await backend.connect();
34
34
 
35
35
  try {