@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.
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +2 -4
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts +5 -3
- package/dist/commands/analyzeAction.d.ts.map +1 -1
- package/dist/commands/analyzeAction.js +109 -151
- package/dist/commands/analyzeAction.js.map +1 -1
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +4 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.js +2 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/coverage.js +2 -2
- package/dist/commands/coverage.js.map +1 -1
- package/dist/commands/describe.d.ts +13 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/describe.js +131 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts +6 -1
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +128 -13
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/doctor.d.ts +10 -9
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/file.js +2 -2
- package/dist/commands/file.js.map +1 -1
- package/dist/commands/get.js +2 -2
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/git-ingest.d.ts +6 -0
- package/dist/commands/git-ingest.d.ts.map +1 -0
- package/dist/commands/git-ingest.js +46 -0
- package/dist/commands/git-ingest.js.map +1 -0
- package/dist/commands/impact.d.ts.map +1 -1
- package/dist/commands/impact.js +276 -50
- package/dist/commands/impact.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +20 -22
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ls.js +2 -2
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/overview.js +2 -2
- package/dist/commands/overview.js.map +1 -1
- package/dist/commands/query.d.ts +1 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +169 -7
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/schema.js +2 -2
- package/dist/commands/schema.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +122 -76
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/stats.js +2 -2
- package/dist/commands/stats.js.map +1 -1
- package/dist/commands/tldr.d.ts +12 -0
- package/dist/commands/tldr.d.ts.map +1 -0
- package/dist/commands/tldr.js +81 -0
- package/dist/commands/tldr.js.map +1 -0
- package/dist/commands/trace.d.ts +1 -1
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +17 -133
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/types.js +2 -2
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/who.d.ts +12 -0
- package/dist/commands/who.d.ts.map +1 -0
- package/dist/commands/who.js +184 -0
- package/dist/commands/who.js.map +1 -0
- package/dist/commands/why.d.ts +12 -0
- package/dist/commands/why.d.ts.map +1 -0
- package/dist/commands/why.js +118 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/commands/wtf.d.ts +12 -0
- package/dist/commands/wtf.d.ts.map +1 -0
- package/dist/commands/wtf.js +117 -0
- package/dist/commands/wtf.js.map +1 -0
- package/dist/plugins/builtinPlugins.d.ts +1 -9
- package/dist/plugins/builtinPlugins.d.ts.map +1 -1
- package/dist/plugins/builtinPlugins.js +2 -67
- package/dist/plugins/builtinPlugins.js.map +1 -1
- package/dist/plugins/pluginLoader.d.ts +1 -15
- package/dist/plugins/pluginLoader.d.ts.map +1 -1
- package/dist/plugins/pluginLoader.js +2 -100
- package/dist/plugins/pluginLoader.js.map +1 -1
- package/dist/plugins/pluginResolver.js +3 -3
- package/dist/utils/progressRenderer.d.ts +15 -1
- package/dist/utils/progressRenderer.d.ts.map +1 -1
- package/dist/utils/progressRenderer.js +19 -3
- package/dist/utils/progressRenderer.js.map +1 -1
- package/dist/utils/queryHints.d.ts +6 -0
- package/dist/utils/queryHints.d.ts.map +1 -0
- package/dist/utils/queryHints.js +36 -0
- package/dist/utils/queryHints.js.map +1 -0
- package/package.json +4 -4
- package/skills/grafema-codebase-analysis/SKILL.md +1 -1
- package/src/cli.ts +14 -0
- package/src/commands/analyze.ts +2 -4
- package/src/commands/analyzeAction.ts +122 -168
- package/src/commands/check.ts +5 -5
- package/src/commands/context.ts +3 -3
- package/src/commands/coverage.ts +2 -2
- package/src/commands/describe.ts +160 -0
- package/src/commands/doctor/checks.ts +153 -10
- package/src/commands/doctor.ts +13 -9
- package/src/commands/explain.ts +2 -2
- package/src/commands/explore.tsx +2 -2
- package/src/commands/file.ts +3 -3
- package/src/commands/get.ts +2 -2
- package/src/commands/git-ingest.ts +49 -0
- package/src/commands/impact.ts +318 -55
- package/src/commands/init.ts +20 -22
- package/src/commands/ls.ts +2 -2
- package/src/commands/overview.ts +2 -2
- package/src/commands/query.ts +197 -7
- package/src/commands/schema.ts +2 -2
- package/src/commands/server.ts +136 -84
- package/src/commands/stats.ts +2 -2
- package/src/commands/tldr.ts +103 -0
- package/src/commands/trace.ts +19 -161
- package/src/commands/types.ts +2 -2
- package/src/commands/who.ts +215 -0
- package/src/commands/why.ts +134 -0
- package/src/commands/wtf.ts +140 -0
- package/src/plugins/builtinPlugins.ts +1 -108
- package/src/plugins/pluginLoader.ts +1 -123
- package/src/plugins/pluginResolver.js +3 -3
- package/src/utils/progressRenderer.ts +34 -4
- package/src/utils/queryHints.ts +46 -0
package/src/commands/query.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
}
|
package/src/commands/schema.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type GraphSchema,
|
|
22
22
|
type NodeTypeSchema,
|
|
23
23
|
type EdgeTypeSchema,
|
|
24
|
-
} from '@grafema/
|
|
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 {
|
package/src/commands/server.ts
CHANGED
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import { Command } from 'commander';
|
|
12
12
|
import { resolve, join } from 'path';
|
|
13
|
-
import { existsSync, unlinkSync,
|
|
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/
|
|
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
|
-
|
|
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 =
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
package/src/commands/stats.ts
CHANGED
|
@@ -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/
|
|
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 {
|