[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

feat: Refactor the API for stopping spinners & progress bars (#405)

authored by

Chris Swithinbank and committed by
GitHub
(Oct 22, 2025, 3:38 PM +0100) 38019c78 69508f48

+224 -19
+27
.changeset/odd-bikes-nail.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + --- 4 + 5 + Updates the API for stopping spinners and progress bars to be clearer 6 + 7 + Previously, both the spinner and progress bar components used a single `stop` method that accepted a code to indicate success, cancellation, or error. This update separates these into distinct methods: `stop()`, `cancel()`, and `error()`: 8 + 9 + ```diff 10 + const spinner = prompts.spinner(); 11 + spinner.start(); 12 + 13 + // Cancelling a spinner 14 + - spinner.stop(undefined, 1); 15 + + spinner.cancel(); 16 + 17 + // Stopping with an error 18 + - spinner.stop(undefined, 2); 19 + + spinner.error(); 20 + ``` 21 + 22 + As before, you can pass a message to each method to customize the output displayed: 23 + 24 + ```js 25 + spinner.cancel("Operation cancelled by user"); 26 + progressBar.error("An error occurred during processing"); 27 + ```
+2
packages/prompts/src/progress-bar.ts
··· 63 63 return { 64 64 start, 65 65 stop: spin.stop, 66 + cancel: spin.cancel, 67 + error: spin.error, 66 68 advance, 67 69 isCancelled: spin.isCancelled, 68 70 message: (msg: string) => advance(0, msg),
+11 -3
packages/prompts/src/spinner.ts
··· 24 24 25 25 export interface SpinnerResult { 26 26 start(msg?: string): void; 27 - stop(msg?: string, code?: number): void; 27 + stop(msg?: string): void; 28 + cancel(msg?: string): void; 29 + error(msg?: string): void; 28 30 message(msg?: string): void; 29 31 readonly isCancelled: boolean; 30 32 } ··· 61 63 : (cancelMessage ?? settings.messages.cancel); 62 64 isCancelled = code === 1; 63 65 if (isSpinnerActive) { 64 - stop(msg, code); 66 + _stop(msg, code); 65 67 if (isCancelled && typeof onCancel === 'function') { 66 68 onCancel(); 67 69 } ··· 163 165 }, delay); 164 166 }; 165 167 166 - const stop = (msg = '', code = 0): void => { 168 + const _stop = (msg = '', code = 0): void => { 167 169 if (!isSpinnerActive) return; 168 170 isSpinnerActive = false; 169 171 clearInterval(loop); ··· 184 186 unblock(); 185 187 }; 186 188 189 + const stop = (msg = ''): void => _stop(msg, 0); 190 + const cancel = (msg = ''): void => _stop(msg, 1); 191 + const error = (msg = ''): void => _stop(msg, 2); 192 + 187 193 const message = (msg = ''): void => { 188 194 _message = removeTrailingDots(msg ?? _message); 189 195 }; ··· 192 198 start, 193 199 stop, 194 200 message, 201 + cancel, 202 + error, 195 203 get isCancelled() { 196 204 return isCancelled; 197 205 },
+64 -4
packages/prompts/test/__snapshots__/progress-bar.test.ts.snap
··· 125 125 ] 126 126 `; 127 127 128 - exports[`prompts - progress (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` 128 + exports[`prompts - progress (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` 129 129 [ 130 130 "<cursor.hide>", 131 131 "│ ··· 139 139 ] 140 140 `; 141 141 142 - exports[`prompts - progress (isCI = false) > stop > renders error symbol if code > 1 1`] = ` 142 + exports[`prompts - progress (isCI = false) > stop > renders error symbol when calling error() 1`] = ` 143 143 [ 144 144 "<cursor.hide>", 145 145 "│ ··· 162 162 "<cursor.left count=1>", 163 163 "<erase.down>", 164 164 "◇ foo 165 + ", 166 + "<cursor.show>", 167 + ] 168 + `; 169 + 170 + exports[`prompts - progress (isCI = false) > stop > renders message when cancelling 1`] = ` 171 + [ 172 + "<cursor.hide>", 173 + "│ 174 + ", 175 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", 176 + "<cursor.left count=1>", 177 + "<erase.down>", 178 + "■ cancelled :-( 179 + ", 180 + "<cursor.show>", 181 + ] 182 + `; 183 + 184 + exports[`prompts - progress (isCI = false) > stop > renders message when erroring 1`] = ` 185 + [ 186 + "<cursor.hide>", 187 + "│ 188 + ", 189 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", 190 + "<cursor.left count=1>", 191 + "<erase.down>", 192 + "▲ FATAL ERROR! 165 193 ", 166 194 "<cursor.show>", 167 195 ] ··· 382 410 ] 383 411 `; 384 412 385 - exports[`prompts - progress (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` 413 + exports[`prompts - progress (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` 386 414 [ 387 415 "<cursor.hide>", 388 416 "│ ··· 398 426 ] 399 427 `; 400 428 401 - exports[`prompts - progress (isCI = true) > stop > renders error symbol if code > 1 1`] = ` 429 + exports[`prompts - progress (isCI = true) > stop > renders error symbol when calling error() 1`] = ` 402 430 [ 403 431 "<cursor.hide>", 404 432 "│ ··· 425 453 "<cursor.left count=1>", 426 454 "<erase.down>", 427 455 "◇ foo 456 + ", 457 + "<cursor.show>", 458 + ] 459 + `; 460 + 461 + exports[`prompts - progress (isCI = true) > stop > renders message when cancelling 1`] = ` 462 + [ 463 + "<cursor.hide>", 464 + "│ 465 + ", 466 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", 467 + " 468 + ", 469 + "<cursor.left count=1>", 470 + "<erase.down>", 471 + "■ cancelled :-( 472 + ", 473 + "<cursor.show>", 474 + ] 475 + `; 476 + 477 + exports[`prompts - progress (isCI = true) > stop > renders message when erroring 1`] = ` 478 + [ 479 + "<cursor.hide>", 480 + "│ 481 + ", 482 + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", 483 + " 484 + ", 485 + "<cursor.left count=1>", 486 + "<erase.down>", 487 + "▲ FATAL ERROR! 428 488 ", 429 489 "<cursor.show>", 430 490 ]
+64 -4
packages/prompts/test/__snapshots__/spinner.test.ts.snap
··· 461 461 ] 462 462 `; 463 463 464 - exports[`spinner (isCI = false) > stop > renders cancel symbol if code = 1 1`] = ` 464 + exports[`spinner (isCI = false) > stop > renders cancel symbol when calling cancel() 1`] = ` 465 465 [ 466 466 "<cursor.hide>", 467 467 "│ ··· 475 475 ] 476 476 `; 477 477 478 - exports[`spinner (isCI = false) > stop > renders error symbol if code > 1 1`] = ` 478 + exports[`spinner (isCI = false) > stop > renders error symbol when calling error() 1`] = ` 479 479 [ 480 480 "<cursor.hide>", 481 481 "│ ··· 498 498 "<cursor.left count=1>", 499 499 "<erase.down>", 500 500 "◇ foo 501 + ", 502 + "<cursor.show>", 503 + ] 504 + `; 505 + 506 + exports[`spinner (isCI = false) > stop > renders message when cancelling 1`] = ` 507 + [ 508 + "<cursor.hide>", 509 + "│ 510 + ", 511 + "◒ ", 512 + "<cursor.left count=1>", 513 + "<erase.down>", 514 + "■ too dizzy — spinning cancelled 515 + ", 516 + "<cursor.show>", 517 + ] 518 + `; 519 + 520 + exports[`spinner (isCI = false) > stop > renders message when erroring 1`] = ` 521 + [ 522 + "<cursor.hide>", 523 + "│ 524 + ", 525 + "◒ ", 526 + "<cursor.left count=1>", 527 + "<erase.down>", 528 + "▲ error: spun too fast! 501 529 ", 502 530 "<cursor.show>", 503 531 ] ··· 789 817 ] 790 818 `; 791 819 792 - exports[`spinner (isCI = true) > stop > renders cancel symbol if code = 1 1`] = ` 820 + exports[`spinner (isCI = true) > stop > renders cancel symbol when calling cancel() 1`] = ` 793 821 [ 794 822 "<cursor.hide>", 795 823 "│ ··· 805 833 ] 806 834 `; 807 835 808 - exports[`spinner (isCI = true) > stop > renders error symbol if code > 1 1`] = ` 836 + exports[`spinner (isCI = true) > stop > renders error symbol when calling error() 1`] = ` 809 837 [ 810 838 "<cursor.hide>", 811 839 "│ ··· 832 860 "<cursor.left count=1>", 833 861 "<erase.down>", 834 862 "◇ foo 863 + ", 864 + "<cursor.show>", 865 + ] 866 + `; 867 + 868 + exports[`spinner (isCI = true) > stop > renders message when cancelling 1`] = ` 869 + [ 870 + "<cursor.hide>", 871 + "│ 872 + ", 873 + "◒ ...", 874 + " 875 + ", 876 + "<cursor.left count=1>", 877 + "<erase.down>", 878 + "■ too dizzy — spinning cancelled 879 + ", 880 + "<cursor.show>", 881 + ] 882 + `; 883 + 884 + exports[`spinner (isCI = true) > stop > renders message when erroring 1`] = ` 885 + [ 886 + "<cursor.hide>", 887 + "│ 888 + ", 889 + "◒ ...", 890 + " 891 + ", 892 + "<cursor.left count=1>", 893 + "<erase.down>", 894 + "▲ error: spun too fast! 835 895 ", 836 896 "<cursor.show>", 837 897 ]
+28 -4
packages/prompts/test/progress-bar.test.ts
··· 87 87 expect(output.buffer).toMatchSnapshot(); 88 88 }); 89 89 90 - test('renders cancel symbol if code = 1', () => { 90 + test('renders cancel symbol when calling cancel()', () => { 91 91 const result = prompts.progress({ output }); 92 92 93 93 result.start(); 94 94 95 95 vi.advanceTimersByTime(80); 96 96 97 - result.stop('', 1); 97 + result.cancel(); 98 98 99 99 expect(output.buffer).toMatchSnapshot(); 100 100 }); 101 101 102 - test('renders error symbol if code > 1', () => { 102 + test('renders error symbol when calling error()', () => { 103 103 const result = prompts.progress({ output }); 104 104 105 105 result.start(); 106 106 107 107 vi.advanceTimersByTime(80); 108 108 109 - result.stop('', 2); 109 + result.error(); 110 110 111 111 expect(output.buffer).toMatchSnapshot(); 112 112 }); ··· 131 131 vi.advanceTimersByTime(80); 132 132 133 133 result.stop('foo.'); 134 + 135 + expect(output.buffer).toMatchSnapshot(); 136 + }); 137 + 138 + test('renders message when cancelling', () => { 139 + const result = prompts.progress({ output }); 140 + 141 + result.start(); 142 + 143 + vi.advanceTimersByTime(80); 144 + 145 + result.cancel('cancelled :-('); 146 + 147 + expect(output.buffer).toMatchSnapshot(); 148 + }); 149 + 150 + test('renders message when erroring', () => { 151 + const result = prompts.progress({ output }); 152 + 153 + result.start(); 154 + 155 + vi.advanceTimersByTime(80); 156 + 157 + result.error('FATAL ERROR!'); 134 158 135 159 expect(output.buffer).toMatchSnapshot(); 136 160 });
+28 -4
packages/prompts/test/spinner.test.ts
··· 117 117 expect(output.buffer).toMatchSnapshot(); 118 118 }); 119 119 120 - test('renders cancel symbol if code = 1', () => { 120 + test('renders cancel symbol when calling cancel()', () => { 121 121 const result = prompts.spinner({ output }); 122 122 123 123 result.start(); 124 124 125 125 vi.advanceTimersByTime(80); 126 126 127 - result.stop('', 1); 127 + result.cancel(); 128 128 129 129 expect(output.buffer).toMatchSnapshot(); 130 130 }); 131 131 132 - test('renders error symbol if code > 1', () => { 132 + test('renders error symbol when calling error()', () => { 133 133 const result = prompts.spinner({ output }); 134 134 135 135 result.start(); 136 136 137 137 vi.advanceTimersByTime(80); 138 138 139 - result.stop('', 2); 139 + result.error(); 140 140 141 141 expect(output.buffer).toMatchSnapshot(); 142 142 }); ··· 161 161 vi.advanceTimersByTime(80); 162 162 163 163 result.stop('foo.'); 164 + 165 + expect(output.buffer).toMatchSnapshot(); 166 + }); 167 + 168 + test('renders message when cancelling', () => { 169 + const result = prompts.spinner({ output }); 170 + 171 + result.start(); 172 + 173 + vi.advanceTimersByTime(80); 174 + 175 + result.cancel('too dizzy — spinning cancelled'); 176 + 177 + expect(output.buffer).toMatchSnapshot(); 178 + }); 179 + 180 + test('renders message when erroring', () => { 181 + const result = prompts.spinner({ output }); 182 + 183 + result.start(); 184 + 185 + vi.advanceTimersByTime(80); 186 + 187 + result.error('error: spun too fast!'); 164 188 165 189 expect(output.buffer).toMatchSnapshot(); 166 190 });