@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.
Files changed (84) hide show
  1. package/README.md +73 -0
  2. package/dist/cli.js +1 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/analyze.d.ts +9 -0
  5. package/dist/commands/analyze.d.ts.map +1 -1
  6. package/dist/commands/analyze.js +136 -52
  7. package/dist/commands/analyze.js.map +1 -0
  8. package/dist/commands/check.d.ts +2 -6
  9. package/dist/commands/check.d.ts.map +1 -1
  10. package/dist/commands/check.js +32 -46
  11. package/dist/commands/check.js.map +1 -0
  12. package/dist/commands/coverage.js +1 -0
  13. package/dist/commands/coverage.js.map +1 -0
  14. package/dist/commands/doctor/checks.d.ts.map +1 -1
  15. package/dist/commands/doctor/checks.js +9 -5
  16. package/dist/commands/doctor/checks.js.map +1 -0
  17. package/dist/commands/doctor/output.js +1 -0
  18. package/dist/commands/doctor/output.js.map +1 -0
  19. package/dist/commands/doctor/types.js +1 -0
  20. package/dist/commands/doctor/types.js.map +1 -0
  21. package/dist/commands/doctor.js +1 -0
  22. package/dist/commands/doctor.js.map +1 -0
  23. package/dist/commands/explain.js +1 -0
  24. package/dist/commands/explain.js.map +1 -0
  25. package/dist/commands/explore.d.ts.map +1 -1
  26. package/dist/commands/explore.js +9 -4
  27. package/dist/commands/explore.js.map +1 -0
  28. package/dist/commands/get.d.ts.map +1 -1
  29. package/dist/commands/get.js +7 -0
  30. package/dist/commands/get.js.map +1 -0
  31. package/dist/commands/impact.js +1 -0
  32. package/dist/commands/impact.js.map +1 -0
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +7 -1
  35. package/dist/commands/init.js.map +1 -0
  36. package/dist/commands/ls.d.ts.map +1 -1
  37. package/dist/commands/ls.js +7 -0
  38. package/dist/commands/ls.js.map +1 -0
  39. package/dist/commands/overview.d.ts.map +1 -1
  40. package/dist/commands/overview.js +1 -0
  41. package/dist/commands/overview.js.map +1 -0
  42. package/dist/commands/query.d.ts.map +1 -1
  43. package/dist/commands/query.js +68 -1
  44. package/dist/commands/query.js.map +1 -0
  45. package/dist/commands/schema.js +1 -0
  46. package/dist/commands/schema.js.map +1 -0
  47. package/dist/commands/server.d.ts +2 -1
  48. package/dist/commands/server.d.ts.map +1 -1
  49. package/dist/commands/server.js +128 -15
  50. package/dist/commands/server.js.map +1 -0
  51. package/dist/commands/stats.js +1 -0
  52. package/dist/commands/stats.js.map +1 -0
  53. package/dist/commands/trace.js +1 -0
  54. package/dist/commands/trace.js.map +1 -0
  55. package/dist/commands/types.js +1 -0
  56. package/dist/commands/types.js.map +1 -0
  57. package/dist/utils/codePreview.js +1 -0
  58. package/dist/utils/codePreview.js.map +1 -0
  59. package/dist/utils/errorFormatter.js +1 -0
  60. package/dist/utils/errorFormatter.js.map +1 -0
  61. package/dist/utils/formatNode.js +1 -0
  62. package/dist/utils/formatNode.js.map +1 -0
  63. package/dist/utils/progressRenderer.d.ts +119 -0
  64. package/dist/utils/progressRenderer.d.ts.map +1 -0
  65. package/dist/utils/progressRenderer.js +245 -0
  66. package/dist/utils/progressRenderer.js.map +1 -0
  67. package/dist/utils/spinner.d.ts +39 -0
  68. package/dist/utils/spinner.d.ts.map +1 -0
  69. package/dist/utils/spinner.js +84 -0
  70. package/dist/utils/spinner.js.map +1 -0
  71. package/package.json +5 -4
  72. package/src/commands/analyze.ts +150 -55
  73. package/src/commands/check.ts +36 -68
  74. package/src/commands/doctor/checks.ts +8 -5
  75. package/src/commands/explore.tsx +8 -4
  76. package/src/commands/get.ts +8 -0
  77. package/src/commands/impact.ts +1 -1
  78. package/src/commands/init.ts +6 -2
  79. package/src/commands/ls.ts +8 -0
  80. package/src/commands/overview.ts +0 -4
  81. package/src/commands/query.ts +77 -1
  82. package/src/commands/server.ts +142 -16
  83. package/src/utils/progressRenderer.ts +288 -0
  84. 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
+ }