@grafema/cli 0.2.3-beta → 0.2.5-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/README.md +73 -0
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +136 -52
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/check.d.ts +2 -6
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +32 -46
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/coverage.js +1 -0
- package/dist/commands/coverage.js.map +1 -0
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +9 -5
- package/dist/commands/doctor/checks.js.map +1 -0
- package/dist/commands/doctor/output.js +1 -0
- package/dist/commands/doctor/output.js.map +1 -0
- package/dist/commands/doctor/types.js +1 -0
- package/dist/commands/doctor/types.js.map +1 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.js +1 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/explore.d.ts.map +1 -1
- package/dist/commands/explore.js +9 -4
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +7 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/impact.js +1 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -1
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +7 -0
- package/dist/commands/ls.js.map +1 -0
- package/dist/commands/overview.d.ts.map +1 -1
- package/dist/commands/overview.js +1 -0
- package/dist/commands/overview.js.map +1 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +68 -1
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/schema.js +1 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/server.d.ts +2 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +50 -3
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/stats.js +1 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/trace.js +1 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/utils/codePreview.js +1 -0
- package/dist/utils/codePreview.js.map +1 -0
- package/dist/utils/errorFormatter.js +1 -0
- package/dist/utils/errorFormatter.js.map +1 -0
- package/dist/utils/formatNode.js +1 -0
- package/dist/utils/formatNode.js.map +1 -0
- package/dist/utils/progressRenderer.d.ts +119 -0
- package/dist/utils/progressRenderer.d.ts.map +1 -0
- package/dist/utils/progressRenderer.js +245 -0
- package/dist/utils/progressRenderer.js.map +1 -0
- package/dist/utils/spinner.d.ts +39 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +84 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +5 -4
- package/src/commands/analyze.ts +150 -55
- package/src/commands/check.ts +36 -68
- package/src/commands/doctor/checks.ts +8 -5
- package/src/commands/explore.tsx +8 -4
- package/src/commands/get.ts +8 -0
- package/src/commands/impact.ts +1 -1
- package/src/commands/init.ts +6 -2
- package/src/commands/ls.ts +8 -0
- package/src/commands/overview.ts +0 -4
- package/src/commands/query.ts +77 -1
- package/src/commands/server.ts +57 -3
- package/src/utils/progressRenderer.ts +288 -0
- package/src/utils/spinner.ts +94 -0
package/src/commands/explore.tsx
CHANGED
|
@@ -160,9 +160,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
160
160
|
}));
|
|
161
161
|
}
|
|
162
162
|
} catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
163
164
|
setState(s => ({
|
|
164
165
|
...s,
|
|
165
|
-
error:
|
|
166
|
+
error: message,
|
|
166
167
|
loading: false,
|
|
167
168
|
}));
|
|
168
169
|
}
|
|
@@ -387,9 +388,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
387
388
|
error: results.length === 0 ? `No results for "${query}"` : null,
|
|
388
389
|
}));
|
|
389
390
|
} catch (err) {
|
|
391
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
390
392
|
setState(s => ({
|
|
391
393
|
...s,
|
|
392
|
-
error:
|
|
394
|
+
error: message,
|
|
393
395
|
loading: false,
|
|
394
396
|
}));
|
|
395
397
|
}
|
|
@@ -408,9 +410,10 @@ function Explorer({ backend, startNode, projectPath }: ExplorerProps) {
|
|
|
408
410
|
loading: false,
|
|
409
411
|
}));
|
|
410
412
|
} catch (err) {
|
|
413
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
411
414
|
setState(s => ({
|
|
412
415
|
...s,
|
|
413
|
-
error:
|
|
416
|
+
error: message,
|
|
414
417
|
loading: false,
|
|
415
418
|
}));
|
|
416
419
|
}
|
|
@@ -1010,7 +1013,8 @@ async function runBatchExplore(
|
|
|
1010
1013
|
outputResults(callees, 'callees', useJson, projectPath, target);
|
|
1011
1014
|
}
|
|
1012
1015
|
} catch (err) {
|
|
1013
|
-
|
|
1016
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1017
|
+
exitWithError(`Explore failed: ${message}`);
|
|
1014
1018
|
}
|
|
1015
1019
|
}
|
|
1016
1020
|
|
package/src/commands/get.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync } from 'fs';
|
|
|
12
12
|
import { RFDBServerBackend } from '@grafema/core';
|
|
13
13
|
import { formatNodeDisplay } from '../utils/formatNode.js';
|
|
14
14
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
15
|
+
import { Spinner } from '../utils/spinner.js';
|
|
15
16
|
|
|
16
17
|
interface GetOptions {
|
|
17
18
|
project: string;
|
|
@@ -66,11 +67,15 @@ Examples:
|
|
|
66
67
|
const backend = new RFDBServerBackend({ dbPath });
|
|
67
68
|
await backend.connect();
|
|
68
69
|
|
|
70
|
+
const spinner = new Spinner('Querying graph...');
|
|
71
|
+
spinner.start();
|
|
72
|
+
|
|
69
73
|
try {
|
|
70
74
|
// Retrieve node by semantic ID
|
|
71
75
|
const node = await backend.getNode(semanticId);
|
|
72
76
|
|
|
73
77
|
if (!node) {
|
|
78
|
+
spinner.stop();
|
|
74
79
|
exitWithError('Node not found', [
|
|
75
80
|
`ID: ${semanticId}`,
|
|
76
81
|
'Try: grafema query "<name>" to search for nodes',
|
|
@@ -81,6 +86,8 @@ Examples:
|
|
|
81
86
|
const incomingEdges = await backend.getIncomingEdges(semanticId, null);
|
|
82
87
|
const outgoingEdges = await backend.getOutgoingEdges(semanticId, null);
|
|
83
88
|
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
84
91
|
if (options.json) {
|
|
85
92
|
await outputJSON(backend, node, incomingEdges, outgoingEdges);
|
|
86
93
|
} else {
|
|
@@ -88,6 +95,7 @@ Examples:
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
} finally {
|
|
98
|
+
spinner.stop();
|
|
91
99
|
await backend.close();
|
|
92
100
|
}
|
|
93
101
|
});
|
package/src/commands/impact.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Command } from 'commander';
|
|
|
10
10
|
import { resolve, join, dirname } from 'path';
|
|
11
11
|
import { relative } from 'path';
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
|
-
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore
|
|
13
|
+
import { RFDBServerBackend, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
14
14
|
import { formatNodeDisplay, formatNodeInline } from '../utils/formatNode.js';
|
|
15
15
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
16
16
|
|
package/src/commands/init.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { createInterface } from 'readline';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { exitWithError } from '../utils/errorFormatter.js';
|
|
12
11
|
import { stringify as stringifyYAML } from 'yaml';
|
|
13
12
|
import { DEFAULT_CONFIG } from '@grafema/core';
|
|
14
13
|
|
|
@@ -75,7 +74,9 @@ function askYesNo(question: string): Promise<boolean> {
|
|
|
75
74
|
function runAnalyze(projectPath: string): Promise<number> {
|
|
76
75
|
return new Promise((resolve) => {
|
|
77
76
|
const cliPath = join(__dirname, '..', 'cli.js');
|
|
78
|
-
|
|
77
|
+
// Use process.execPath (absolute path to current Node binary) instead of
|
|
78
|
+
// 'node' to avoid PATH lookup failures when nvm isn't loaded in the shell.
|
|
79
|
+
const child = spawn(process.execPath, [cliPath, 'analyze', projectPath], {
|
|
79
80
|
stdio: 'inherit', // Pass through all I/O for user to see progress
|
|
80
81
|
});
|
|
81
82
|
child.on('close', (code) => resolve(code ?? 1));
|
|
@@ -92,6 +93,9 @@ function printNextSteps(): void {
|
|
|
92
93
|
console.log(' 1. Review config: code .grafema/config.yaml');
|
|
93
94
|
console.log(' 2. Build graph: grafema analyze');
|
|
94
95
|
console.log(' 3. Explore: grafema overview');
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log('For AI-assisted setup, use the Grafema MCP server');
|
|
98
|
+
console.log('with the "onboard_project" prompt.');
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/**
|
package/src/commands/ls.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { resolve, join, relative } from 'path';
|
|
|
15
15
|
import { existsSync } from 'fs';
|
|
16
16
|
import { RFDBServerBackend } from '@grafema/core';
|
|
17
17
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
|
+
import { Spinner } from '../utils/spinner.js';
|
|
18
19
|
|
|
19
20
|
interface LsOptions {
|
|
20
21
|
project: string;
|
|
@@ -66,6 +67,9 @@ Discover available types:
|
|
|
66
67
|
const backend = new RFDBServerBackend({ dbPath });
|
|
67
68
|
await backend.connect();
|
|
68
69
|
|
|
70
|
+
const spinner = new Spinner('Querying graph...');
|
|
71
|
+
spinner.start();
|
|
72
|
+
|
|
69
73
|
try {
|
|
70
74
|
const limit = parseInt(options.limit, 10);
|
|
71
75
|
const nodeType = options.type;
|
|
@@ -73,6 +77,7 @@ Discover available types:
|
|
|
73
77
|
// Check if type exists in graph
|
|
74
78
|
const typeCounts = await backend.countNodesByType();
|
|
75
79
|
if (!typeCounts[nodeType]) {
|
|
80
|
+
spinner.stop();
|
|
76
81
|
const availableTypes = Object.keys(typeCounts).sort();
|
|
77
82
|
exitWithError(`No nodes of type "${nodeType}" found`, [
|
|
78
83
|
'Available types:',
|
|
@@ -103,6 +108,8 @@ Discover available types:
|
|
|
103
108
|
const totalCount = typeCounts[nodeType];
|
|
104
109
|
const showing = nodes.length;
|
|
105
110
|
|
|
111
|
+
spinner.stop();
|
|
112
|
+
|
|
106
113
|
if (options.json) {
|
|
107
114
|
console.log(JSON.stringify({
|
|
108
115
|
type: nodeType,
|
|
@@ -125,6 +132,7 @@ Discover available types:
|
|
|
125
132
|
}
|
|
126
133
|
}
|
|
127
134
|
} finally {
|
|
135
|
+
spinner.stop();
|
|
128
136
|
await backend.close();
|
|
129
137
|
}
|
|
130
138
|
});
|
package/src/commands/overview.ts
CHANGED
|
@@ -8,10 +8,6 @@ import { existsSync } from 'fs';
|
|
|
8
8
|
import { RFDBServerBackend } from '@grafema/core';
|
|
9
9
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
10
10
|
|
|
11
|
-
interface NodeStats {
|
|
12
|
-
type: string;
|
|
13
|
-
count: number;
|
|
14
|
-
}
|
|
15
11
|
|
|
16
12
|
export const overviewCommand = new Command('overview')
|
|
17
13
|
.description('Show project overview and statistics')
|
package/src/commands/query.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { existsSync } from 'fs';
|
|
|
15
15
|
import { RFDBServerBackend, parseSemanticId, findCallsInFunction as findCallsInFunctionCore, findContainingFunction as findContainingFunctionCore } from '@grafema/core';
|
|
16
16
|
import { formatNodeDisplay, formatNodeInline, formatLocation } from '../utils/formatNode.js';
|
|
17
17
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
|
+
import { Spinner } from '../utils/spinner.js';
|
|
18
19
|
|
|
19
20
|
interface QueryOptions {
|
|
20
21
|
project: string;
|
|
@@ -130,9 +131,13 @@ Examples:
|
|
|
130
131
|
const backend = new RFDBServerBackend({ dbPath });
|
|
131
132
|
await backend.connect();
|
|
132
133
|
|
|
134
|
+
const spinner = new Spinner('Querying graph...');
|
|
135
|
+
spinner.start();
|
|
136
|
+
|
|
133
137
|
try {
|
|
134
138
|
// Raw Datalog mode
|
|
135
139
|
if (options.raw) {
|
|
140
|
+
spinner.stop();
|
|
136
141
|
await executeRawQuery(backend, pattern, options);
|
|
137
142
|
return;
|
|
138
143
|
}
|
|
@@ -159,6 +164,8 @@ Examples:
|
|
|
159
164
|
// Find matching nodes
|
|
160
165
|
const nodes = await findNodes(backend, query, limit);
|
|
161
166
|
|
|
167
|
+
spinner.stop();
|
|
168
|
+
|
|
162
169
|
// Check if query has scope constraints for suggestion
|
|
163
170
|
const hasScope = query.file !== null || query.scopes.length > 0;
|
|
164
171
|
|
|
@@ -218,6 +225,7 @@ Examples:
|
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
} finally {
|
|
228
|
+
spinner.stop();
|
|
221
229
|
await backend.close();
|
|
222
230
|
}
|
|
223
231
|
});
|
|
@@ -254,6 +262,11 @@ function parsePattern(pattern: string): { type: string | null; name: string } {
|
|
|
254
262
|
emit: 'socketio:emit',
|
|
255
263
|
on: 'socketio:on',
|
|
256
264
|
listener: 'socketio:on',
|
|
265
|
+
// Grafema internal
|
|
266
|
+
plugin: 'grafema:plugin',
|
|
267
|
+
// Property access aliases (REG-395)
|
|
268
|
+
property: 'PROPERTY_ACCESS',
|
|
269
|
+
prop: 'PROPERTY_ACCESS',
|
|
257
270
|
};
|
|
258
271
|
|
|
259
272
|
if (typeMap[typeWord]) {
|
|
@@ -551,7 +564,8 @@ async function findNodes(
|
|
|
551
564
|
'http:request',
|
|
552
565
|
'socketio:event',
|
|
553
566
|
'socketio:emit',
|
|
554
|
-
'socketio:on'
|
|
567
|
+
'socketio:on',
|
|
568
|
+
'PROPERTY_ACCESS'
|
|
555
569
|
];
|
|
556
570
|
|
|
557
571
|
for (const nodeType of searchTypes) {
|
|
@@ -606,6 +620,21 @@ async function findNodes(
|
|
|
606
620
|
nodeInfo.handlerName = node.handlerName as string | undefined;
|
|
607
621
|
}
|
|
608
622
|
|
|
623
|
+
// Include plugin-specific fields
|
|
624
|
+
if (nodeType === 'grafema:plugin') {
|
|
625
|
+
nodeInfo.phase = node.phase as string | undefined;
|
|
626
|
+
nodeInfo.priority = node.priority as number | undefined;
|
|
627
|
+
nodeInfo.builtin = node.builtin as boolean | undefined;
|
|
628
|
+
nodeInfo.createsNodes = node.createsNodes as string[] | undefined;
|
|
629
|
+
nodeInfo.createsEdges = node.createsEdges as string[] | undefined;
|
|
630
|
+
nodeInfo.dependencies = node.dependencies as string[] | undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Include objectName for PROPERTY_ACCESS nodes (REG-395)
|
|
634
|
+
if (nodeType === 'PROPERTY_ACCESS') {
|
|
635
|
+
nodeInfo.objectName = node.objectName as string | undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
609
638
|
results.push(nodeInfo);
|
|
610
639
|
if (results.length >= limit) break;
|
|
611
640
|
}
|
|
@@ -735,6 +764,12 @@ async function displayNode(node: NodeInfo, projectPath: string, backend: RFDBSer
|
|
|
735
764
|
return;
|
|
736
765
|
}
|
|
737
766
|
|
|
767
|
+
// Special formatting for Grafema plugin nodes
|
|
768
|
+
if (node.type === 'grafema:plugin') {
|
|
769
|
+
console.log(formatPluginDisplay(node, projectPath));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
738
773
|
console.log(formatNodeDisplay(node, { projectPath }));
|
|
739
774
|
|
|
740
775
|
// Add scope context if present
|
|
@@ -890,6 +925,47 @@ function formatSocketIONodeDisplay(node: NodeInfo, projectPath: string): string
|
|
|
890
925
|
return lines.join('\n');
|
|
891
926
|
}
|
|
892
927
|
|
|
928
|
+
/**
|
|
929
|
+
* Format Grafema plugin node for display.
|
|
930
|
+
*
|
|
931
|
+
* Output:
|
|
932
|
+
* [grafema:plugin] HTTPConnectionEnricher
|
|
933
|
+
* Phase: ENRICHMENT (priority: 50)
|
|
934
|
+
* Creates: edges: INTERACTS_WITH, HTTP_RECEIVES
|
|
935
|
+
* Dependencies: ExpressRouteAnalyzer, FetchAnalyzer, ExpressResponseAnalyzer
|
|
936
|
+
* Source: packages/core/src/plugins/enrichment/HTTPConnectionEnricher.ts
|
|
937
|
+
*/
|
|
938
|
+
function formatPluginDisplay(node: NodeInfo, projectPath: string): string {
|
|
939
|
+
const lines: string[] = [];
|
|
940
|
+
|
|
941
|
+
lines.push(`[${node.type}] ${node.name}`);
|
|
942
|
+
|
|
943
|
+
const phase = (node.phase as string) || 'unknown';
|
|
944
|
+
const priority = (node.priority as number) ?? 0;
|
|
945
|
+
lines.push(` Phase: ${phase} (priority: ${priority})`);
|
|
946
|
+
|
|
947
|
+
const createsNodes = (node.createsNodes as string[]) || [];
|
|
948
|
+
const createsEdges = (node.createsEdges as string[]) || [];
|
|
949
|
+
const createsParts: string[] = [];
|
|
950
|
+
if (createsNodes.length > 0) createsParts.push(`nodes: ${createsNodes.join(', ')}`);
|
|
951
|
+
if (createsEdges.length > 0) createsParts.push(`edges: ${createsEdges.join(', ')}`);
|
|
952
|
+
if (createsParts.length > 0) {
|
|
953
|
+
lines.push(` Creates: ${createsParts.join('; ')}`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const deps = (node.dependencies as string[]) || [];
|
|
957
|
+
if (deps.length > 0) {
|
|
958
|
+
lines.push(` Dependencies: ${deps.join(', ')}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (node.file) {
|
|
962
|
+
const relPath = relative(projectPath, node.file);
|
|
963
|
+
lines.push(` Source: ${relPath}`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return lines.join('\n');
|
|
967
|
+
}
|
|
968
|
+
|
|
893
969
|
/**
|
|
894
970
|
* Execute raw Datalog query (backwards compat)
|
|
895
971
|
*/
|
package/src/commands/server.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server command - Manage RFDB server lifecycle
|
|
2
|
+
* Server command - Manage RFDB (Rega Flow Database) server lifecycle
|
|
3
3
|
*
|
|
4
4
|
* Provides explicit control over the RFDB server process:
|
|
5
5
|
* grafema server start - Start detached server
|
|
6
6
|
* grafema server stop - Stop server gracefully
|
|
7
7
|
* grafema server status - Check if server is running
|
|
8
|
+
* grafema server graphql - Start GraphQL API server
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { Command } from 'commander';
|
|
@@ -13,7 +14,7 @@ import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
|
|
|
13
14
|
import { spawn } from 'child_process';
|
|
14
15
|
import { fileURLToPath } from 'url';
|
|
15
16
|
import { setTimeout as sleep } from 'timers/promises';
|
|
16
|
-
import { RFDBClient, loadConfig } from '@grafema/core';
|
|
17
|
+
import { RFDBClient, loadConfig, RFDBServerBackend } from '@grafema/core';
|
|
17
18
|
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -124,7 +125,7 @@ function getProjectPaths(projectPath: string) {
|
|
|
124
125
|
|
|
125
126
|
// Create main server command with subcommands
|
|
126
127
|
export const serverCommand = new Command('server')
|
|
127
|
-
.description('Manage RFDB server lifecycle')
|
|
128
|
+
.description('Manage RFDB (Rega Flow Database) server lifecycle')
|
|
128
129
|
.addHelpText('after', `
|
|
129
130
|
Examples:
|
|
130
131
|
grafema server start Start the RFDB server
|
|
@@ -395,3 +396,56 @@ serverCommand
|
|
|
395
396
|
}
|
|
396
397
|
}
|
|
397
398
|
});
|
|
399
|
+
|
|
400
|
+
// grafema server graphql
|
|
401
|
+
serverCommand
|
|
402
|
+
.command('graphql')
|
|
403
|
+
.description('Start GraphQL API server (requires RFDB server running)')
|
|
404
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
405
|
+
.option('--port <number>', 'Port to listen on', '4000')
|
|
406
|
+
.option('--host <string>', 'Hostname to bind to', 'localhost')
|
|
407
|
+
.action(async (options: { project: string; port: string; host: string }) => {
|
|
408
|
+
const projectPath = resolve(options.project);
|
|
409
|
+
const { socketPath } = getProjectPaths(projectPath);
|
|
410
|
+
|
|
411
|
+
// Check if RFDB server is running
|
|
412
|
+
const status = await isServerRunning(socketPath);
|
|
413
|
+
if (!status.running) {
|
|
414
|
+
exitWithError('RFDB server not running', [
|
|
415
|
+
'Start the server first: grafema server start',
|
|
416
|
+
'Or run: grafema analyze (starts server automatically)'
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Create backend connection
|
|
421
|
+
const backend = new RFDBServerBackend({ socketPath });
|
|
422
|
+
await backend.connect();
|
|
423
|
+
|
|
424
|
+
// Import and start GraphQL server
|
|
425
|
+
const { startServer } = await import('@grafema/api');
|
|
426
|
+
const port = parseInt(options.port, 10);
|
|
427
|
+
|
|
428
|
+
console.log('Starting Grafema GraphQL API...');
|
|
429
|
+
console.log(` RFDB Socket: ${socketPath}`);
|
|
430
|
+
if (status.version) {
|
|
431
|
+
console.log(` RFDB Version: ${status.version}`);
|
|
432
|
+
}
|
|
433
|
+
console.log('');
|
|
434
|
+
|
|
435
|
+
const server = startServer({
|
|
436
|
+
backend,
|
|
437
|
+
port,
|
|
438
|
+
hostname: options.host,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Handle shutdown
|
|
442
|
+
const shutdown = async () => {
|
|
443
|
+
console.log('\nShutting down GraphQL server...');
|
|
444
|
+
server.close();
|
|
445
|
+
await backend.close();
|
|
446
|
+
process.exit(0);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
process.on('SIGINT', shutdown);
|
|
450
|
+
process.on('SIGTERM', shutdown);
|
|
451
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressRenderer - Formats and displays analysis progress for CLI.
|
|
3
|
+
*
|
|
4
|
+
* Consumes ProgressInfo events from Orchestrator and renders them as
|
|
5
|
+
* user-friendly progress output with phase tracking, elapsed time,
|
|
6
|
+
* and spinner animation.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const renderer = new ProgressRenderer({ isInteractive: true });
|
|
11
|
+
* orchestrator.run({
|
|
12
|
+
* onProgress: (info) => renderer.update(info),
|
|
13
|
+
* });
|
|
14
|
+
* console.log(renderer.finish(elapsed));
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ProgressInfo } from '@grafema/core';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for creating a ProgressRenderer instance.
|
|
22
|
+
*/
|
|
23
|
+
export interface ProgressRendererOptions {
|
|
24
|
+
/** Whether output is to a TTY (enables spinner and line overwriting) */
|
|
25
|
+
isInteractive?: boolean;
|
|
26
|
+
/** Minimum milliseconds between display updates (default: 100) */
|
|
27
|
+
throttle?: number;
|
|
28
|
+
/** Custom write function for output (default: process.stdout.write) */
|
|
29
|
+
write?: (text: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ProgressRenderer - Formats and displays analysis progress for CLI.
|
|
34
|
+
*
|
|
35
|
+
* Consumes ProgressInfo events from Orchestrator and renders them as
|
|
36
|
+
* user-friendly progress output with phase tracking, elapsed time,
|
|
37
|
+
* and spinner animation.
|
|
38
|
+
*/
|
|
39
|
+
export class ProgressRenderer {
|
|
40
|
+
private phases: string[] = ['discovery', 'indexing', 'analysis', 'enrichment', 'validation'];
|
|
41
|
+
private currentPhaseIndex: number = -1;
|
|
42
|
+
private currentPhase: string = '';
|
|
43
|
+
private currentPlugin: string = '';
|
|
44
|
+
private message: string = '';
|
|
45
|
+
private totalFiles: number = 0;
|
|
46
|
+
private processedFiles: number = 0;
|
|
47
|
+
private servicesAnalyzed: number = 0;
|
|
48
|
+
private spinnerIndex: number = 0;
|
|
49
|
+
private isInteractive: boolean;
|
|
50
|
+
private startTime: number;
|
|
51
|
+
private lastDisplayTime: number = 0;
|
|
52
|
+
private displayThrottle: number;
|
|
53
|
+
private write: (text: string) => void;
|
|
54
|
+
private spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
55
|
+
private activePlugins: string[] = [];
|
|
56
|
+
private nodeCount: number = 0;
|
|
57
|
+
private edgeCount: number = 0;
|
|
58
|
+
|
|
59
|
+
constructor(options?: ProgressRendererOptions) {
|
|
60
|
+
this.isInteractive = options?.isInteractive ?? process.stdout.isTTY ?? false;
|
|
61
|
+
this.displayThrottle = options?.throttle ?? 100;
|
|
62
|
+
this.startTime = Date.now();
|
|
63
|
+
this.write = options?.write ?? ((text: string) => process.stdout.write(text));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Process a progress event from Orchestrator.
|
|
68
|
+
* Updates internal state and displays formatted output if throttle allows.
|
|
69
|
+
*/
|
|
70
|
+
update(info: ProgressInfo): void {
|
|
71
|
+
// Update phase tracking
|
|
72
|
+
if (info.phase && info.phase !== this.currentPhase) {
|
|
73
|
+
this.currentPhase = info.phase;
|
|
74
|
+
const idx = this.phases.indexOf(info.phase);
|
|
75
|
+
if (idx !== -1) {
|
|
76
|
+
this.currentPhaseIndex = idx;
|
|
77
|
+
}
|
|
78
|
+
// Reset phase-specific state
|
|
79
|
+
this.activePlugins = [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Update state from progress info
|
|
83
|
+
if (info.currentPlugin !== undefined) {
|
|
84
|
+
this.currentPlugin = info.currentPlugin;
|
|
85
|
+
// Track active plugins for enrichment/validation display
|
|
86
|
+
if ((this.currentPhase === 'enrichment' || this.currentPhase === 'validation') &&
|
|
87
|
+
info.currentPlugin && !this.activePlugins.includes(info.currentPlugin)) {
|
|
88
|
+
this.activePlugins.push(info.currentPlugin);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (info.message !== undefined) {
|
|
92
|
+
this.message = info.message;
|
|
93
|
+
}
|
|
94
|
+
if (info.totalFiles !== undefined) {
|
|
95
|
+
this.totalFiles = info.totalFiles;
|
|
96
|
+
}
|
|
97
|
+
if (info.processedFiles !== undefined) {
|
|
98
|
+
this.processedFiles = info.processedFiles;
|
|
99
|
+
}
|
|
100
|
+
if (info.servicesAnalyzed !== undefined) {
|
|
101
|
+
this.servicesAnalyzed = info.servicesAnalyzed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update spinner
|
|
105
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
106
|
+
|
|
107
|
+
// Check throttling
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
if (now - this.lastDisplayTime < this.displayThrottle) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.lastDisplayTime = now;
|
|
113
|
+
|
|
114
|
+
this.display();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update graph statistics (called separately from progress events).
|
|
119
|
+
* This allows real-time node/edge count updates.
|
|
120
|
+
*/
|
|
121
|
+
setStats(nodeCount: number, edgeCount: number): void {
|
|
122
|
+
this.nodeCount = nodeCount;
|
|
123
|
+
this.edgeCount = edgeCount;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format and display current state to console.
|
|
128
|
+
*/
|
|
129
|
+
private display(): void {
|
|
130
|
+
const output = this.formatOutput();
|
|
131
|
+
|
|
132
|
+
if (this.isInteractive) {
|
|
133
|
+
// TTY mode: overwrite previous line, pad with spaces to clear old content
|
|
134
|
+
const padded = output.padEnd(80, ' ');
|
|
135
|
+
this.write(`\r${padded}`);
|
|
136
|
+
} else {
|
|
137
|
+
// Non-TTY mode: append newline
|
|
138
|
+
this.write(`${output}\n`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private formatOutput(): string {
|
|
143
|
+
if (this.isInteractive) {
|
|
144
|
+
return this.formatInteractive();
|
|
145
|
+
} else {
|
|
146
|
+
return this.formatNonInteractive();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format elapsed time as human-readable string.
|
|
152
|
+
*/
|
|
153
|
+
private formatElapsed(): string {
|
|
154
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
|
155
|
+
if (elapsed < 60) {
|
|
156
|
+
return `${elapsed.toFixed(1)}s`;
|
|
157
|
+
}
|
|
158
|
+
const minutes = Math.floor(elapsed / 60);
|
|
159
|
+
const seconds = Math.floor(elapsed % 60);
|
|
160
|
+
return `${minutes}m${seconds}s`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private formatInteractive(): string {
|
|
164
|
+
const spinner = this.spinnerFrames[this.spinnerIndex];
|
|
165
|
+
const elapsed = this.formatElapsed();
|
|
166
|
+
const phaseLabel = this.getPhaseLabel();
|
|
167
|
+
const progress = this.formatPhaseProgress();
|
|
168
|
+
const stats = this.formatStats();
|
|
169
|
+
|
|
170
|
+
// Format: ⠋ [3/5] Analysis... 150/4047 modules | 12.5s | 1.2M nodes
|
|
171
|
+
return `${spinner} ${phaseLabel}${progress} | ${elapsed}${stats}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private formatNonInteractive(): string {
|
|
175
|
+
const elapsed = this.formatElapsed();
|
|
176
|
+
return `[${this.currentPhase}] ${this.message || this.formatPhaseProgress()} (${elapsed})`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Format node/edge counts if available.
|
|
181
|
+
*/
|
|
182
|
+
private formatStats(): string {
|
|
183
|
+
if (this.nodeCount === 0 && this.edgeCount === 0) {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
const nodes = this.formatNumber(this.nodeCount);
|
|
187
|
+
const edges = this.formatNumber(this.edgeCount);
|
|
188
|
+
return ` | ${nodes} nodes, ${edges} edges`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Format large numbers with K/M suffix.
|
|
193
|
+
*/
|
|
194
|
+
private formatNumber(n: number): string {
|
|
195
|
+
if (n >= 1_000_000) {
|
|
196
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
197
|
+
}
|
|
198
|
+
if (n >= 1_000) {
|
|
199
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
200
|
+
}
|
|
201
|
+
return String(n);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get formatted phase label with number, e.g., "[3/5] Analysis..."
|
|
206
|
+
*/
|
|
207
|
+
private getPhaseLabel(): string {
|
|
208
|
+
const phaseNum = this.currentPhaseIndex + 1;
|
|
209
|
+
const totalPhases = this.phases.length;
|
|
210
|
+
const phaseName = this.currentPhase.charAt(0).toUpperCase() + this.currentPhase.slice(1);
|
|
211
|
+
return `[${phaseNum}/${totalPhases}] ${phaseName}...`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Format progress details based on current phase.
|
|
216
|
+
*/
|
|
217
|
+
private formatPhaseProgress(): string {
|
|
218
|
+
switch (this.currentPhase) {
|
|
219
|
+
case 'discovery':
|
|
220
|
+
if (this.servicesAnalyzed > 0) {
|
|
221
|
+
return ` ${this.servicesAnalyzed} services found`;
|
|
222
|
+
}
|
|
223
|
+
return '';
|
|
224
|
+
case 'indexing':
|
|
225
|
+
case 'analysis':
|
|
226
|
+
if (this.totalFiles > 0) {
|
|
227
|
+
return ` ${this.processedFiles}/${this.totalFiles} modules`;
|
|
228
|
+
}
|
|
229
|
+
return '';
|
|
230
|
+
case 'enrichment':
|
|
231
|
+
case 'validation':
|
|
232
|
+
if (this.activePlugins.length > 0) {
|
|
233
|
+
return ` (${this.formatPluginList(this.activePlugins)})`;
|
|
234
|
+
}
|
|
235
|
+
return '';
|
|
236
|
+
default:
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format plugin list, truncating if more than 3 plugins.
|
|
243
|
+
*/
|
|
244
|
+
private formatPluginList(plugins: string[]): string {
|
|
245
|
+
if (plugins.length <= 3) {
|
|
246
|
+
return plugins.join(', ');
|
|
247
|
+
}
|
|
248
|
+
// Truncate to 3 plugins + "..."
|
|
249
|
+
return plugins.slice(0, 3).join(', ') + ', ...';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get final summary message after analysis complete.
|
|
254
|
+
* @param durationSeconds - Total duration of analysis
|
|
255
|
+
* @returns Formatted completion message
|
|
256
|
+
*/
|
|
257
|
+
finish(durationSeconds: number): string {
|
|
258
|
+
return `Analysis complete in ${durationSeconds.toFixed(2)}s`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Expose internal state for testing.
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
getState(): {
|
|
266
|
+
phaseIndex: number;
|
|
267
|
+
phase: string;
|
|
268
|
+
processedFiles: number;
|
|
269
|
+
totalFiles: number;
|
|
270
|
+
servicesAnalyzed: number;
|
|
271
|
+
spinnerIndex: number;
|
|
272
|
+
activePlugins: string[];
|
|
273
|
+
nodeCount: number;
|
|
274
|
+
edgeCount: number;
|
|
275
|
+
} {
|
|
276
|
+
return {
|
|
277
|
+
phaseIndex: this.currentPhaseIndex,
|
|
278
|
+
phase: this.currentPhase,
|
|
279
|
+
processedFiles: this.processedFiles,
|
|
280
|
+
totalFiles: this.totalFiles,
|
|
281
|
+
servicesAnalyzed: this.servicesAnalyzed,
|
|
282
|
+
spinnerIndex: this.spinnerIndex,
|
|
283
|
+
activePlugins: [...this.activePlugins],
|
|
284
|
+
nodeCount: this.nodeCount,
|
|
285
|
+
edgeCount: this.edgeCount,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|