@microsoft/m365-copilot-eval 1.0.1-preview.1 → 1.1.0-preview.1

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.
@@ -0,0 +1,677 @@
1
+ /**
2
+ * Progress display module for CLI initialization
3
+ * Provides real-time progress indication with support for:
4
+ * - Interactive TTY terminals (in-place updates, spinners, progress bars)
5
+ * - CI/CD environments (timestamped line-based output)
6
+ * - Verbose mode (detailed sub-step information)
7
+ */
8
+
9
+ import * as readline from 'readline';
10
+ import { setInterval, clearInterval } from 'timers';
11
+
12
+ /**
13
+ * Initialization phases configuration
14
+ */
15
+ export const PHASES = [
16
+ {
17
+ id: 'download',
18
+ name: 'Downloading Python runtime',
19
+ order: 1,
20
+ progressType: 'determinate',
21
+ },
22
+ {
23
+ id: 'extract',
24
+ name: 'Extracting Python runtime',
25
+ order: 2,
26
+ progressType: 'indeterminate',
27
+ },
28
+ {
29
+ id: 'venv',
30
+ name: 'Creating virtual environment',
31
+ order: 3,
32
+ progressType: 'indeterminate',
33
+ },
34
+ {
35
+ id: 'deps',
36
+ name: 'Installing dependencies',
37
+ order: 4,
38
+ progressType: 'determinate',
39
+ },
40
+ ];
41
+
42
+ /**
43
+ * Spinner frames for indeterminate progress (Unicode Braille)
44
+ */
45
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
46
+ const SPINNER_INTERVAL = 80; // ms
47
+
48
+ /**
49
+ * Default options for ProgressReporter
50
+ */
51
+ const DEFAULT_OPTIONS = {
52
+ verbose: false,
53
+ quiet: false,
54
+ forceInteractive: false,
55
+ forceNonInteractive: false,
56
+ ciOutputInterval: 30000, // 30 seconds
57
+ };
58
+
59
+ /**
60
+ * Detect if running in an interactive terminal
61
+ * @returns {boolean} True if interactive TTY, false for CI/CD or piped output
62
+ */
63
+ export function isInteractiveTerminal() {
64
+ // Not a TTY (pipes, redirects)
65
+ if (!process.stdout.isTTY) return false;
66
+
67
+ // Common CI environment variables
68
+ if (process.env.CI) return false;
69
+ if (process.env.GITHUB_ACTIONS) return false;
70
+ if (process.env.JENKINS_URL) return false;
71
+ if (process.env.GITLAB_CI) return false;
72
+ if (process.env.TF_BUILD) return false; // Azure Pipelines
73
+ if (process.env.CIRCLECI) return false;
74
+ if (process.env.TRAVIS) return false;
75
+ if (process.env.BUILDKITE) return false;
76
+
77
+ return true;
78
+ }
79
+
80
+ /**
81
+ * Format bytes to human-readable string
82
+ * @param {number} bytes - Number of bytes
83
+ * @returns {string} Formatted string (e.g., "45.2 MB")
84
+ */
85
+ export function formatBytes(bytes) {
86
+ if (bytes === 0) return '0 B';
87
+ if (bytes < 0) return '0 B';
88
+
89
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
90
+ const k = 1024;
91
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
92
+ const index = Math.min(i, units.length - 1);
93
+
94
+ return `${(bytes / Math.pow(k, index)).toFixed(index > 0 ? 1 : 0)} ${units[index]}`;
95
+ }
96
+
97
+ /**
98
+ * Format duration in milliseconds to human-readable string
99
+ * @param {number} ms - Duration in milliseconds
100
+ * @returns {string} Formatted string (e.g., "2m 30s")
101
+ */
102
+ export function formatDuration(ms) {
103
+ if (ms < 0) ms = 0;
104
+
105
+ const seconds = Math.floor(ms / 1000);
106
+ const minutes = Math.floor(seconds / 60);
107
+ const remainingSeconds = seconds % 60;
108
+
109
+ if (minutes > 0) {
110
+ return `${minutes}m ${remainingSeconds}s`;
111
+ }
112
+ return `${seconds}s`;
113
+ }
114
+
115
+ /**
116
+ * Calculate estimated time remaining
117
+ * @param {number} current - Current progress value
118
+ * @param {number} total - Total progress value
119
+ * @param {number} elapsedMs - Elapsed time in milliseconds
120
+ * @returns {number|null} Estimated remaining time in ms, or null if not calculable
121
+ */
122
+ export function calculateETA(current, total, elapsedMs) {
123
+ if (current <= 0 || total <= 0 || elapsedMs <= 0) return null;
124
+
125
+ const percentage = (current / total) * 100;
126
+ // Only calculate when we have enough progress data (> 5%)
127
+ if (percentage < 5) return null;
128
+
129
+ const rate = current / elapsedMs; // units per ms
130
+ const remaining = total - current;
131
+ return Math.round(remaining / rate);
132
+ }
133
+
134
+ /**
135
+ * Render ASCII progress bar
136
+ * @param {number} percentage - Progress percentage (0-100)
137
+ * @param {number} width - Bar width in characters
138
+ * @returns {string} Progress bar string (e.g., "[████████░░░░]")
139
+ */
140
+ export function renderProgressBar(percentage, width = 12) {
141
+ const clamped = Math.max(0, Math.min(100, percentage));
142
+ const filled = Math.round((clamped / 100) * width);
143
+ const empty = width - filled;
144
+ return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
145
+ }
146
+
147
+ /**
148
+ * Spinner class for indeterminate progress
149
+ */
150
+ export class Spinner {
151
+ constructor(label = '') {
152
+ this.label = label;
153
+ this.frameIndex = 0;
154
+ this.interval = null;
155
+ this.isRunning = false;
156
+ }
157
+
158
+ /**
159
+ * Start the spinner animation
160
+ * @param {Function} renderFn - Function to call with each frame
161
+ */
162
+ start(renderFn) {
163
+ if (this.isRunning) return;
164
+ this.isRunning = true;
165
+
166
+ this.interval = setInterval(() => {
167
+ const frame = SPINNER_FRAMES[this.frameIndex++ % SPINNER_FRAMES.length];
168
+ if (renderFn) {
169
+ renderFn(frame, this.label);
170
+ }
171
+ }, SPINNER_INTERVAL);
172
+ }
173
+
174
+ /**
175
+ * Stop the spinner animation
176
+ */
177
+ stop() {
178
+ if (this.interval) {
179
+ clearInterval(this.interval);
180
+ this.interval = null;
181
+ }
182
+ this.isRunning = false;
183
+ }
184
+
185
+ /**
186
+ * Update the spinner label
187
+ * @param {string} label - New label text
188
+ */
189
+ setLabel(label) {
190
+ this.label = label;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Progress Reporter class for managing initialization progress display
196
+ */
197
+ export class ProgressReporter {
198
+ /**
199
+ * @param {Object} options - Configuration options
200
+ * @param {boolean} options.verbose - Show detailed sub-step information
201
+ * @param {boolean} options.quiet - Suppress all progress output
202
+ * @param {boolean} options.forceInteractive - Force interactive mode (for testing)
203
+ * @param {boolean} options.forceNonInteractive - Force CI mode (for testing)
204
+ * @param {number} options.ciOutputInterval - Interval (ms) between CI output lines
205
+ */
206
+ constructor(options = {}) {
207
+ this.options = { ...DEFAULT_OPTIONS, ...options };
208
+ this.isInteractive = this._determineInteractiveMode();
209
+ this.startTime = Date.now();
210
+ this.phaseStartTime = null;
211
+ this.currentPhase = null;
212
+ this.phaseStatuses = new Map();
213
+ this.spinner = null;
214
+ this.lastCIOutput = 0;
215
+
216
+ // Initialize phase statuses
217
+ for (const phase of PHASES) {
218
+ this.phaseStatuses.set(phase.id, 'pending');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Determine if we should use interactive mode
224
+ * @private
225
+ */
226
+ _determineInteractiveMode() {
227
+ if (this.options.forceInteractive) return true;
228
+ if (this.options.forceNonInteractive) return false;
229
+ return isInteractiveTerminal();
230
+ }
231
+
232
+ /**
233
+ * Get phase info by ID
234
+ * @param {string} phaseId - Phase identifier
235
+ * @returns {Object|undefined} Phase configuration
236
+ */
237
+ _getPhase(phaseId) {
238
+ return PHASES.find((p) => p.id === phaseId);
239
+ }
240
+
241
+ /**
242
+ * Format phase prefix for display
243
+ * @param {string} phaseId - Phase identifier
244
+ * @returns {string} Formatted prefix (e.g., "[Step 1/4]")
245
+ */
246
+ formatPhasePrefix(phaseId) {
247
+ const phase = this._getPhase(phaseId);
248
+ if (!phase) return '';
249
+ return `[Step ${phase.order}/${PHASES.length}]`;
250
+ }
251
+
252
+ /**
253
+ * Update the current line in place (TTY) or print new line (CI)
254
+ * @param {string} text - Text to display
255
+ */
256
+ updateLine(text) {
257
+ if (this.options.quiet) return;
258
+
259
+ if (this.isInteractive) {
260
+ // Truncate text to terminal width
261
+ const maxWidth = (process.stdout.columns || 80) - 1;
262
+ let displayText = text;
263
+ if (text.length > maxWidth) {
264
+ displayText = text.substring(0, maxWidth - 3) + '...';
265
+ }
266
+
267
+ // Simple in-place update: carriage return, clear, write
268
+ // Note: Dynamic terminal resize may cause brief artifacts
269
+ process.stdout.write('\r');
270
+ readline.clearLine(process.stdout, 0);
271
+ process.stdout.write(displayText);
272
+ } else {
273
+ // CI mode: print with timestamp
274
+ const elapsed = formatDuration(Date.now() - this.startTime);
275
+ console.log(`[${elapsed}] ${text}`);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Print a new line (both TTY and CI)
281
+ * @param {string} text - Text to display
282
+ */
283
+ printLine(text) {
284
+ if (this.options.quiet) return;
285
+
286
+ if (this.isInteractive) {
287
+ // Truncate text to terminal width to prevent wrapping
288
+ const maxWidth = (process.stdout.columns || 80) - 1;
289
+ let displayText = text;
290
+ if (text.length > maxWidth) {
291
+ displayText = text.substring(0, maxWidth - 3) + '...';
292
+ }
293
+
294
+ // Clear current line, write text, and move to next line
295
+ readline.cursorTo(process.stdout, 0);
296
+ readline.clearLine(process.stdout, 0);
297
+ process.stdout.write(displayText);
298
+ process.stdout.write('\n');
299
+ } else {
300
+ const elapsed = formatDuration(Date.now() - this.startTime);
301
+ console.log(`[${elapsed}] ${text}`);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Start a new phase
307
+ * @param {string} phaseId - Phase identifier
308
+ */
309
+ startPhase(phaseId) {
310
+ const phase = this._getPhase(phaseId);
311
+ if (!phase) return;
312
+
313
+ // Stop any existing spinner
314
+ if (this.spinner) {
315
+ this.spinner.stop();
316
+ this.spinner = null;
317
+ }
318
+
319
+ this.currentPhase = phaseId;
320
+ this.phaseStartTime = Date.now();
321
+ this.phaseStatuses.set(phaseId, 'active');
322
+
323
+ const prefix = this.formatPhasePrefix(phaseId);
324
+
325
+ if (phase.progressType === 'indeterminate' && this.isInteractive) {
326
+ // Start spinner for indeterminate phases
327
+ this.spinner = new Spinner(phase.name);
328
+ this.spinner.start((frame, label) => {
329
+ this.updateLine(`${prefix} ${label} ${frame}`);
330
+ });
331
+ } else if (!this.isInteractive) {
332
+ // CI mode: print phase start
333
+ this.printLine(`${prefix} ${phase.name}...`);
334
+ this.lastCIOutput = Date.now();
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Update progress for the current phase
340
+ * @param {Object} update - Progress update
341
+ * @param {string} update.phaseId - Phase identifier
342
+ * @param {number} [update.current] - Current progress value
343
+ * @param {number} [update.total] - Total progress value
344
+ * @param {string} [update.message] - Optional status message
345
+ */
346
+ updateProgress(update) {
347
+ if (this.options.quiet) return;
348
+
349
+ const { phaseId, current, total, message } = update;
350
+ const phase = this._getPhase(phaseId);
351
+ if (!phase) return;
352
+
353
+ const prefix = this.formatPhasePrefix(phaseId);
354
+ const elapsed = Date.now() - (this.phaseStartTime || this.startTime);
355
+
356
+ // Status-specific handling for deps phase (must come before generic checks)
357
+ if (
358
+ phase.progressType === 'determinate' &&
359
+ update.status === 'downloading' &&
360
+ current !== undefined &&
361
+ total !== undefined
362
+ ) {
363
+ // Downloading phase with known total (from collecting count)
364
+ // Guard against division by zero
365
+ if (total === 0) {
366
+ // Fall back to indeterminate spinner if total is invalid
367
+ if (this.isInteractive) {
368
+ if (!this.spinner || !this.spinner.isRunning) {
369
+ this.spinner = new Spinner('Downloading packages');
370
+ this.spinner.start((frame, label) => {
371
+ this.updateLine(`${prefix} ${frame} ${label}`);
372
+ });
373
+ }
374
+ }
375
+ return;
376
+ }
377
+
378
+ const percentage = Math.round((current / total) * 100);
379
+ const progressBar = renderProgressBar(percentage);
380
+ const countStr = ` (${current}/${total})`;
381
+
382
+ const displayText = `${prefix} Downloading packages ${progressBar} ${percentage}%${countStr}`;
383
+
384
+ if (this.isInteractive) {
385
+ // Stop spinner if running, switch to progress bar
386
+ if (this.spinner && this.spinner.isRunning) {
387
+ this.spinner.stop();
388
+ this.spinner = null;
389
+ }
390
+ this.updateLine(displayText);
391
+ } else {
392
+ const now = Date.now();
393
+ if (now - this.lastCIOutput >= this.options.ciOutputInterval) {
394
+ this.printLine(
395
+ `${prefix} Downloading packages... ${percentage}%${countStr}`
396
+ );
397
+ this.lastCIOutput = now;
398
+ }
399
+ }
400
+ } else if (
401
+ phase.progressType === 'determinate' &&
402
+ update.status === 'installing'
403
+ ) {
404
+ // Installing phase - show spinner with "Installing packages..." and elapsed time
405
+ const statusLabel = 'Installing packages';
406
+
407
+ if (this.isInteractive) {
408
+ if (!this.spinner || !this.spinner.isRunning) {
409
+ this.spinner = new Spinner(statusLabel);
410
+ this.spinner.start((frame, label) => {
411
+ const currentElapsed = formatDuration(
412
+ Date.now() - (this.phaseStartTime || this.startTime)
413
+ );
414
+ this.updateLine(`${prefix} ${label} [${currentElapsed}] ${frame}`);
415
+ });
416
+ } else {
417
+ this.spinner.setLabel(statusLabel);
418
+ }
419
+ } else {
420
+ const now = Date.now();
421
+ if (now - this.lastCIOutput >= this.options.ciOutputInterval) {
422
+ const elapsedStr = formatDuration(elapsed);
423
+ this.printLine(`${prefix} ${statusLabel}... [${elapsedStr}]`);
424
+ this.lastCIOutput = now;
425
+ }
426
+ }
427
+ } else if (
428
+ phase.progressType === 'determinate' &&
429
+ current !== undefined &&
430
+ total !== undefined &&
431
+ total > 0
432
+ ) {
433
+ // Generic determinate progress with known total (e.g., Python runtime download)
434
+ const rawPercentage = (current / total) * 100;
435
+ const percentage = Math.max(0, Math.min(100, Math.round(rawPercentage)));
436
+ const progressBar = renderProgressBar(percentage);
437
+
438
+ // Format size display (for downloads) - compact format for 80-column terminals
439
+ let sizeStr = '';
440
+ if (total > 1000) {
441
+ // Likely bytes - use shared unit for compactness
442
+ const currentBytes = formatBytes(current);
443
+ const totalBytes = formatBytes(total);
444
+ // Extract unit from total (e.g., "50.0 MB" -> "MB")
445
+ const unit = totalBytes.split(' ')[1];
446
+ const currentVal = currentBytes.split(' ')[0];
447
+ const totalVal = totalBytes.split(' ')[0];
448
+ sizeStr = ` (${currentVal}/${totalVal} ${unit})`;
449
+ } else {
450
+ // Likely count (packages)
451
+ sizeStr = ` (${current}/${total})`;
452
+ }
453
+
454
+ const displayText = `${prefix} ${phase.name} ${progressBar} ${percentage}%${sizeStr}`;
455
+
456
+ if (this.isInteractive) {
457
+ // Stop spinner if running (e.g., when deps transitions from installing spinner to determinate progress)
458
+ if (this.spinner && this.spinner.isRunning) {
459
+ this.spinner.stop();
460
+ this.spinner = null;
461
+ }
462
+ this.updateLine(displayText);
463
+ } else {
464
+ // CI mode: output periodically
465
+ const now = Date.now();
466
+ if (now - this.lastCIOutput >= this.options.ciOutputInterval) {
467
+ this.printLine(`${prefix} ${phase.name}... ${percentage}%${sizeStr}`);
468
+ this.lastCIOutput = now;
469
+ }
470
+ }
471
+
472
+ // Verbose mode: show additional details
473
+ if (this.options.verbose && message) {
474
+ if (this.isInteractive) {
475
+ console.log(`\n → ${message}`);
476
+ } else {
477
+ console.log(` → ${message}`);
478
+ }
479
+ }
480
+ } else if (
481
+ phase.progressType === 'determinate' &&
482
+ current !== undefined &&
483
+ total === undefined
484
+ ) {
485
+ // Progress with count but unknown total (e.g., pip collecting dependencies)
486
+ // Show spinner with count, status, and elapsed time
487
+ const status = update.status || 'collecting';
488
+ let statusLabel = phase.name;
489
+ let countStr = ` (${current} packages)`;
490
+ const elapsedStr = ` [${formatDuration(elapsed)}]`;
491
+
492
+ // Customize display based on pip phase
493
+ if (status === 'collecting') {
494
+ statusLabel = 'Collecting dependencies';
495
+ countStr = ` (${current} packages)`;
496
+ } else if (status === 'cached') {
497
+ statusLabel = 'Using cached packages';
498
+ countStr = ` (${current} cached)`;
499
+ }
500
+
501
+ if (this.isInteractive) {
502
+ // Use spinner with elapsed time to show activity
503
+ if (this.spinner && this.spinner.isRunning) {
504
+ this.spinner.setLabel(`${statusLabel}${countStr}${elapsedStr}`);
505
+ } else {
506
+ // Start spinner for count-based progress
507
+ if (!this.spinner) {
508
+ this.spinner = new Spinner(
509
+ `${statusLabel}${countStr}${elapsedStr}`
510
+ );
511
+ this.spinner.start((frame, label) => {
512
+ // Update elapsed time on each frame
513
+ const currentElapsed = formatDuration(
514
+ Date.now() - (this.phaseStartTime || this.startTime)
515
+ );
516
+ const baseLabel = label.replace(/\s*\[\d+m?\s*\d*s\]$/, ''); // Remove old elapsed
517
+ this.updateLine(
518
+ `${prefix} ${baseLabel} [${currentElapsed}] ${frame}`
519
+ );
520
+ });
521
+ } else {
522
+ this.spinner.setLabel(`${statusLabel}${countStr}${elapsedStr}`);
523
+ }
524
+ }
525
+ } else {
526
+ // CI mode: output periodically
527
+ const now = Date.now();
528
+ if (now - this.lastCIOutput >= this.options.ciOutputInterval) {
529
+ this.printLine(`${prefix} ${statusLabel}...${countStr}${elapsedStr}`);
530
+ this.lastCIOutput = now;
531
+ }
532
+ }
533
+
534
+ // Verbose mode: show package name
535
+ if (this.options.verbose && message) {
536
+ if (this.isInteractive) {
537
+ readline.clearLine(process.stdout, 0);
538
+ readline.cursorTo(process.stdout, 0);
539
+ console.log(` → ${message}`);
540
+ } else {
541
+ console.log(` → ${message}`);
542
+ }
543
+ }
544
+ } else if (phase.progressType === 'indeterminate') {
545
+ // For indeterminate phases, spinner handles display in interactive mode
546
+ // In CI mode, output periodically
547
+ if (!this.isInteractive) {
548
+ const now = Date.now();
549
+ if (now - this.lastCIOutput >= this.options.ciOutputInterval) {
550
+ this.printLine(`${prefix} ${phase.name}...`);
551
+ this.lastCIOutput = now;
552
+ }
553
+ }
554
+
555
+ // Verbose mode: show message if provided
556
+ if (this.options.verbose && message) {
557
+ if (this.isInteractive) {
558
+ // Clear spinner line and print message
559
+ readline.clearLine(process.stdout, 0);
560
+ readline.cursorTo(process.stdout, 0);
561
+ console.log(` → ${message}`);
562
+ } else {
563
+ console.log(` → ${message}`);
564
+ }
565
+ }
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Mark a phase as complete
571
+ * @param {string} phaseId - Phase identifier
572
+ */
573
+ completePhase(phaseId) {
574
+ const phase = this._getPhase(phaseId);
575
+ if (!phase) return;
576
+
577
+ // Stop spinner if running
578
+ if (this.spinner) {
579
+ this.spinner.stop();
580
+ this.spinner = null;
581
+ }
582
+
583
+ this.phaseStatuses.set(phaseId, 'completed');
584
+ const prefix = this.formatPhasePrefix(phaseId);
585
+
586
+ this.printLine(`${prefix} ${phase.name} ✓`);
587
+ this.currentPhase = null;
588
+ this.phaseStartTime = null;
589
+ }
590
+
591
+ /**
592
+ * Mark a phase as failed
593
+ * @param {string} phaseId - Phase identifier
594
+ * @param {Error} error - The error that occurred
595
+ */
596
+ failPhase(phaseId, error) {
597
+ const phase = this._getPhase(phaseId);
598
+ if (!phase) return;
599
+
600
+ // Stop spinner if running
601
+ if (this.spinner) {
602
+ this.spinner.stop();
603
+ this.spinner = null;
604
+ }
605
+
606
+ this.phaseStatuses.set(phaseId, 'failed');
607
+
608
+ // Clear current line and display error
609
+ if (this.isInteractive) {
610
+ readline.clearLine(process.stdout, 0);
611
+ readline.cursorTo(process.stdout, 0);
612
+ }
613
+
614
+ console.log(`\n❌ Failed: ${phase.name}`);
615
+ console.log(`\nError: ${error.message}`);
616
+ console.log(`\nSuggested actions:`);
617
+ console.log(` • Check your internet connection`);
618
+ console.log(` • If behind a proxy, set HTTP_PROXY/HTTPS_PROXY`);
619
+ console.log(` • Run with --verbose for detailed output`);
620
+
621
+ this.currentPhase = null;
622
+ this.phaseStartTime = null;
623
+ }
624
+
625
+ /**
626
+ * Mark a phase as skipped (cache hit)
627
+ * @param {string} phaseId - Phase identifier
628
+ */
629
+ skipPhase(phaseId) {
630
+ this.phaseStatuses.set(phaseId, 'skipped');
631
+ // Silent skip per FR-007 - no output
632
+ }
633
+
634
+ /**
635
+ * Display final completion summary
636
+ */
637
+ complete() {
638
+ if (this.options.quiet) return;
639
+
640
+ // Stop any running spinner
641
+ if (this.spinner) {
642
+ this.spinner.stop();
643
+ this.spinner = null;
644
+ }
645
+
646
+ const totalTime = formatDuration(Date.now() - this.startTime);
647
+ this.printLine(`\n✓ Initialization complete (${totalTime})`);
648
+ }
649
+
650
+ /**
651
+ * Create a progress callback function for use with initialization functions
652
+ * @returns {Function} Callback function accepting progress updates
653
+ */
654
+ createCallback() {
655
+ return (update) => {
656
+ switch (update.type) {
657
+ case 'start':
658
+ this.startPhase(update.phaseId);
659
+ break;
660
+ case 'progress':
661
+ this.updateProgress(update);
662
+ break;
663
+ case 'complete':
664
+ this.completePhase(update.phaseId);
665
+ break;
666
+ case 'error':
667
+ this.failPhase(update.phaseId, update.error);
668
+ break;
669
+ case 'skip':
670
+ this.skipPhase(update.phaseId);
671
+ break;
672
+ }
673
+ };
674
+ }
675
+ }
676
+
677
+ export default ProgressReporter;