@grafema/cli 0.2.4-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 +128 -15
- 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 +142 -16
- package/src/utils/progressRenderer.ts +288 -0
- package/src/utils/spinner.ts +94 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple terminal spinner for slow operations.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - TTY detection: silent in non-TTY environments (CI, pipes)
|
|
6
|
+
* - Delayed display: spinner only appears after 100ms to avoid flicker
|
|
7
|
+
* - Elapsed time: shows seconds for long operations
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const spinner = new Spinner('Querying graph...');
|
|
11
|
+
* spinner.start();
|
|
12
|
+
* await slowOperation();
|
|
13
|
+
* spinner.stop();
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: Always call stop() BEFORE any console.log output.
|
|
16
|
+
*/
|
|
17
|
+
export class Spinner {
|
|
18
|
+
private message: string;
|
|
19
|
+
private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
20
|
+
private interval: ReturnType<typeof setInterval> | null = null;
|
|
21
|
+
private displayTimer: ReturnType<typeof setTimeout> | null = null;
|
|
22
|
+
private frameIndex = 0;
|
|
23
|
+
private startTime = 0;
|
|
24
|
+
private displayDelay: number;
|
|
25
|
+
private isSpinning = false;
|
|
26
|
+
|
|
27
|
+
constructor(message: string, displayDelay = 100) {
|
|
28
|
+
this.message = message;
|
|
29
|
+
this.displayDelay = displayDelay;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Start the spinner. Spinner appears only after displayDelay ms.
|
|
34
|
+
* In non-TTY environments (CI, pipes), this is a no-op.
|
|
35
|
+
*/
|
|
36
|
+
start(): void {
|
|
37
|
+
// TTY check - ora pattern
|
|
38
|
+
if (!process.stdout.isTTY) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.startTime = Date.now();
|
|
43
|
+
|
|
44
|
+
// Defer display to avoid flicker on fast queries
|
|
45
|
+
this.displayTimer = setTimeout(() => {
|
|
46
|
+
this.isSpinning = true;
|
|
47
|
+
this.render();
|
|
48
|
+
|
|
49
|
+
// Animate frames at 80ms interval
|
|
50
|
+
this.interval = setInterval(() => {
|
|
51
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
52
|
+
this.render();
|
|
53
|
+
}, 80);
|
|
54
|
+
}, this.displayDelay);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private render(): void {
|
|
58
|
+
if (!this.isSpinning) return;
|
|
59
|
+
|
|
60
|
+
const frame = this.frames[this.frameIndex];
|
|
61
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
62
|
+
const timeStr = elapsed > 0 ? ` (${elapsed}s)` : '';
|
|
63
|
+
|
|
64
|
+
process.stdout.clearLine(0);
|
|
65
|
+
process.stdout.cursorTo(0);
|
|
66
|
+
process.stdout.write(`${frame} ${this.message}${timeStr}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stop the spinner and clear the line.
|
|
71
|
+
* Safe to call multiple times or if spinner never started.
|
|
72
|
+
*/
|
|
73
|
+
stop(): void {
|
|
74
|
+
// Clear deferred display timer
|
|
75
|
+
if (this.displayTimer) {
|
|
76
|
+
clearTimeout(this.displayTimer);
|
|
77
|
+
this.displayTimer = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Stop animation
|
|
81
|
+
if (this.interval) {
|
|
82
|
+
clearInterval(this.interval);
|
|
83
|
+
this.interval = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clear line if we were displaying
|
|
87
|
+
if (this.isSpinning && process.stdout.isTTY) {
|
|
88
|
+
process.stdout.clearLine(0);
|
|
89
|
+
process.stdout.cursorTo(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.isSpinning = false;
|
|
93
|
+
}
|
|
94
|
+
}
|