@chainlink/external-adapter-framework 2.12.0 → 2.13.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.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/config/index.d.ts +9 -0
  3. package/config/index.js +12 -1
  4. package/config/index.js.map +1 -1
  5. package/generator-adapter/generators/app/templates/src/config/index.ts.ejs +3 -0
  6. package/generator-adapter/node_modules/.yarn-integrity +31 -35
  7. package/generator-adapter/node_modules/@inquirer/ansi/package.json +28 -26
  8. package/generator-adapter/node_modules/@inquirer/checkbox/dist/index.d.ts +6 -1
  9. package/generator-adapter/node_modules/@inquirer/checkbox/dist/index.js +30 -10
  10. package/generator-adapter/node_modules/@inquirer/checkbox/package.json +23 -21
  11. package/generator-adapter/node_modules/@inquirer/confirm/package.json +21 -19
  12. package/generator-adapter/node_modules/@inquirer/core/dist/index.d.ts +1 -1
  13. package/generator-adapter/node_modules/@inquirer/core/dist/index.js +1 -1
  14. package/generator-adapter/node_modules/@inquirer/core/dist/lib/create-prompt.js +51 -25
  15. package/generator-adapter/node_modules/@inquirer/core/dist/lib/key.d.ts +2 -0
  16. package/generator-adapter/node_modules/@inquirer/core/dist/lib/key.js +1 -0
  17. package/generator-adapter/node_modules/@inquirer/core/dist/lib/utils.js +1 -1
  18. package/generator-adapter/node_modules/@inquirer/core/package.json +22 -20
  19. package/generator-adapter/node_modules/@inquirer/editor/dist/index.d.ts +3 -0
  20. package/generator-adapter/node_modules/@inquirer/editor/dist/index.js +5 -5
  21. package/generator-adapter/node_modules/@inquirer/editor/package.json +22 -20
  22. package/generator-adapter/node_modules/@inquirer/expand/package.json +21 -19
  23. package/generator-adapter/node_modules/@inquirer/external-editor/package.json +33 -31
  24. package/generator-adapter/node_modules/@inquirer/figures/package.json +18 -16
  25. package/generator-adapter/node_modules/@inquirer/input/dist/index.js +5 -3
  26. package/generator-adapter/node_modules/@inquirer/input/package.json +21 -19
  27. package/generator-adapter/node_modules/@inquirer/number/package.json +21 -19
  28. package/generator-adapter/node_modules/@inquirer/password/dist/index.d.ts +6 -1
  29. package/generator-adapter/node_modules/@inquirer/password/dist/index.js +7 -2
  30. package/generator-adapter/node_modules/@inquirer/password/package.json +22 -20
  31. package/generator-adapter/node_modules/@inquirer/prompts/README.md +15 -0
  32. package/generator-adapter/node_modules/@inquirer/prompts/package.json +31 -29
  33. package/generator-adapter/node_modules/@inquirer/rawlist/README.md +4 -0
  34. package/generator-adapter/node_modules/@inquirer/rawlist/dist/index.d.ts +9 -2
  35. package/generator-adapter/node_modules/@inquirer/rawlist/dist/index.js +24 -7
  36. package/generator-adapter/node_modules/@inquirer/rawlist/package.json +21 -19
  37. package/generator-adapter/node_modules/@inquirer/search/README.md +2 -1
  38. package/generator-adapter/node_modules/@inquirer/search/dist/index.d.ts +1 -0
  39. package/generator-adapter/node_modules/@inquirer/search/dist/index.js +11 -4
  40. package/generator-adapter/node_modules/@inquirer/search/package.json +22 -20
  41. package/generator-adapter/node_modules/@inquirer/select/dist/index.d.ts +5 -2
  42. package/generator-adapter/node_modules/@inquirer/select/dist/index.js +28 -11
  43. package/generator-adapter/node_modules/@inquirer/select/package.json +23 -21
  44. package/generator-adapter/node_modules/@inquirer/type/package.json +18 -16
  45. package/generator-adapter/node_modules/@types/node/README.md +1 -1
  46. package/generator-adapter/node_modules/@types/node/http.d.ts +1 -1
  47. package/generator-adapter/node_modules/@types/node/package.json +2 -2
  48. package/generator-adapter/node_modules/@types/node/stream/web.d.ts +1 -1
  49. package/generator-adapter/node_modules/@types/node/test.d.ts +2 -1
  50. package/generator-adapter/node_modules/@types/node/web-globals/fetch.d.ts +12 -0
  51. package/generator-adapter/node_modules/@yeoman/adapter/dist/adapter.d.ts +0 -3
  52. package/generator-adapter/node_modules/@yeoman/adapter/dist/adapter.js +1 -0
  53. package/generator-adapter/node_modules/@yeoman/adapter/dist/adapter.js.map +1 -1
  54. package/generator-adapter/node_modules/@yeoman/adapter/dist/log.d.ts +0 -2
  55. package/generator-adapter/node_modules/@yeoman/adapter/dist/log.js.map +1 -1
  56. package/generator-adapter/node_modules/@yeoman/adapter/dist/queued-adapter.d.ts +0 -1
  57. package/generator-adapter/node_modules/@yeoman/adapter/dist/queued-adapter.js.map +1 -1
  58. package/generator-adapter/node_modules/@yeoman/adapter/dist/testing/test-adapter.d.ts +0 -1
  59. package/generator-adapter/node_modules/@yeoman/adapter/dist/testing/test-adapter.js.map +1 -1
  60. package/generator-adapter/node_modules/@yeoman/adapter/package.json +11 -11
  61. package/generator-adapter/node_modules/ansi-styles/index.d.ts +341 -232
  62. package/generator-adapter/node_modules/ansi-styles/index.js +130 -190
  63. package/generator-adapter/node_modules/ansi-styles/license +1 -1
  64. package/generator-adapter/node_modules/ansi-styles/package.json +10 -8
  65. package/generator-adapter/node_modules/ansi-styles/readme.md +32 -53
  66. package/generator-adapter/node_modules/chalk/package.json +3 -3
  67. package/generator-adapter/node_modules/chalk/readme.md +25 -53
  68. package/generator-adapter/node_modules/chalk/source/index.d.ts +6 -1
  69. package/generator-adapter/node_modules/chalk/source/vendor/supports-color/browser.js +6 -2
  70. package/generator-adapter/node_modules/chalk/source/vendor/supports-color/index.js +10 -2
  71. package/generator-adapter/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
  72. package/generator-adapter/node_modules/fast-string-truncated-width/dist/index.js +111 -0
  73. package/generator-adapter/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
  74. package/generator-adapter/node_modules/{p-queue/dist/options copy.js → fast-string-truncated-width/dist/types.js} +1 -0
  75. package/generator-adapter/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
  76. package/generator-adapter/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
  77. package/generator-adapter/node_modules/fast-string-truncated-width/license +21 -0
  78. package/generator-adapter/node_modules/fast-string-truncated-width/package.json +35 -0
  79. package/generator-adapter/node_modules/fast-string-truncated-width/readme.md +59 -0
  80. package/generator-adapter/node_modules/fast-string-width/dist/index.d.ts +4 -0
  81. package/generator-adapter/node_modules/fast-string-width/dist/index.js +14 -0
  82. package/generator-adapter/node_modules/fast-string-width/license +21 -0
  83. package/generator-adapter/node_modules/fast-string-width/package.json +34 -0
  84. package/generator-adapter/node_modules/fast-string-width/readme.md +42 -0
  85. package/generator-adapter/node_modules/fast-wrap-ansi/LICENSE +23 -0
  86. package/generator-adapter/node_modules/fast-wrap-ansi/README.md +26 -0
  87. package/generator-adapter/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
  88. package/generator-adapter/node_modules/fast-wrap-ansi/lib/main.js +219 -0
  89. package/generator-adapter/node_modules/fast-wrap-ansi/lib/main.js.map +1 -0
  90. package/generator-adapter/node_modules/fast-wrap-ansi/package.json +51 -0
  91. package/generator-adapter/node_modules/iconv-lite/lib/index.d.ts +114 -26
  92. package/generator-adapter/node_modules/iconv-lite/lib/index.js +39 -40
  93. package/generator-adapter/node_modules/iconv-lite/package.json +13 -2
  94. package/generator-adapter/node_modules/iconv-lite/types/encodings.d.ts +423 -0
  95. package/generator-adapter/node_modules/inquirer/dist/types.d.ts +12 -11
  96. package/generator-adapter/node_modules/inquirer/dist/ui/prompt.js +9 -1
  97. package/generator-adapter/node_modules/inquirer/dist/ui/skipped-renderer.d.ts +13 -0
  98. package/generator-adapter/node_modules/inquirer/dist/ui/skipped-renderer.js +57 -0
  99. package/generator-adapter/node_modules/inquirer/package.json +21 -19
  100. package/generator-adapter/node_modules/log-symbols/package.json +1 -1
  101. package/generator-adapter/node_modules/log-symbols/symbols.js +1 -1
  102. package/generator-adapter/node_modules/mem-fs/dist/index.d.ts +0 -2
  103. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/LICENSE +21 -0
  104. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/README.md +447 -0
  105. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/index.js +346 -0
  106. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/lib/inspect-stream.js +13 -0
  107. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/lib/is-stream.js +15 -0
  108. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/lib/normalize.js +9 -0
  109. package/generator-adapter/node_modules/mem-fs/node_modules/vinyl/package.json +59 -0
  110. package/generator-adapter/node_modules/mem-fs/package.json +9 -9
  111. package/generator-adapter/node_modules/ora/index.d.ts +3 -1
  112. package/generator-adapter/node_modules/ora/index.js +299 -109
  113. package/generator-adapter/node_modules/ora/package.json +3 -4
  114. package/generator-adapter/node_modules/ora/readme.md +24 -0
  115. package/generator-adapter/node_modules/p-queue/dist/index.js +145 -22
  116. package/generator-adapter/node_modules/p-queue/dist/options.d.ts +17 -1
  117. package/generator-adapter/node_modules/p-queue/package.json +1 -1
  118. package/generator-adapter/node_modules/p-queue/readme.md +32 -1
  119. package/generator-adapter/node_modules/stdin-discarder/index.js +50 -22
  120. package/generator-adapter/node_modules/stdin-discarder/package.json +1 -1
  121. package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/package.json +3 -3
  122. package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/readme.md +53 -25
  123. package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/index.d.ts +1 -6
  124. package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/supports-color/browser.js +2 -6
  125. package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/supports-color/index.js +2 -10
  126. package/generator-adapter/package.json +3 -3
  127. package/package.json +12 -12
  128. package/transports/websocket.d.ts +5 -0
  129. package/transports/websocket.js +25 -13
  130. package/transports/websocket.js.map +1 -1
  131. package/generator-adapter/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  132. package/generator-adapter/node_modules/emoji-regex/README.md +0 -107
  133. package/generator-adapter/node_modules/emoji-regex/index.d.ts +0 -3
  134. package/generator-adapter/node_modules/emoji-regex/index.js +0 -4
  135. package/generator-adapter/node_modules/emoji-regex/index.mjs +0 -4
  136. package/generator-adapter/node_modules/emoji-regex/package.json +0 -45
  137. package/generator-adapter/node_modules/iconv-lite/Changelog.md +0 -236
  138. package/generator-adapter/node_modules/jake/node_modules/ansi-styles/index.d.ts +0 -345
  139. package/generator-adapter/node_modules/jake/node_modules/ansi-styles/index.js +0 -163
  140. package/generator-adapter/node_modules/jake/node_modules/ansi-styles/license +0 -9
  141. package/generator-adapter/node_modules/jake/node_modules/ansi-styles/package.json +0 -56
  142. package/generator-adapter/node_modules/jake/node_modules/ansi-styles/readme.md +0 -152
  143. package/generator-adapter/node_modules/ora/node_modules/log-symbols/browser-symbols.js +0 -4
  144. package/generator-adapter/node_modules/ora/node_modules/log-symbols/browser.js +0 -1
  145. package/generator-adapter/node_modules/ora/node_modules/log-symbols/index.d.ts +0 -22
  146. package/generator-adapter/node_modules/ora/node_modules/log-symbols/index.js +0 -1
  147. package/generator-adapter/node_modules/ora/node_modules/log-symbols/license +0 -9
  148. package/generator-adapter/node_modules/ora/node_modules/log-symbols/package.json +0 -60
  149. package/generator-adapter/node_modules/ora/node_modules/log-symbols/readme.md +0 -39
  150. package/generator-adapter/node_modules/ora/node_modules/log-symbols/symbols.js +0 -14
  151. package/generator-adapter/node_modules/ora/node_modules/strip-ansi/index.d.ts +0 -15
  152. package/generator-adapter/node_modules/ora/node_modules/strip-ansi/index.js +0 -14
  153. package/generator-adapter/node_modules/ora/node_modules/strip-ansi/license +0 -9
  154. package/generator-adapter/node_modules/ora/node_modules/strip-ansi/package.json +0 -59
  155. package/generator-adapter/node_modules/ora/node_modules/strip-ansi/readme.md +0 -37
  156. package/generator-adapter/node_modules/p-queue/dist/options copy.d.ts +0 -121
  157. package/generator-adapter/node_modules/wrap-ansi/index.d.ts +0 -41
  158. package/generator-adapter/node_modules/wrap-ansi/index.js +0 -222
  159. package/generator-adapter/node_modules/wrap-ansi/license +0 -9
  160. package/generator-adapter/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +0 -39
  161. package/generator-adapter/node_modules/wrap-ansi/node_modules/string-width/index.js +0 -82
  162. package/generator-adapter/node_modules/wrap-ansi/node_modules/string-width/license +0 -9
  163. package/generator-adapter/node_modules/wrap-ansi/node_modules/string-width/package.json +0 -64
  164. package/generator-adapter/node_modules/wrap-ansi/node_modules/string-width/readme.md +0 -66
  165. package/generator-adapter/node_modules/wrap-ansi/package.json +0 -69
  166. package/generator-adapter/node_modules/wrap-ansi/readme.md +0 -75
  167. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/license +0 -0
  168. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/index.js +0 -0
  169. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/utilities.js +0 -0
  170. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +0 -0
  171. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/ansi-styles/index.js +0 -0
  172. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/supports-color/browser.d.ts +0 -0
  173. /package/generator-adapter/node_modules/{ora → yeoman-generator}/node_modules/chalk/source/vendor/supports-color/index.d.ts +0 -0
@@ -1,34 +1,117 @@
1
1
  import process from 'node:process';
2
+ import {stripVTControlCharacters} from 'node:util';
2
3
  import chalk from 'chalk';
3
4
  import cliCursor from 'cli-cursor';
4
5
  import cliSpinners from 'cli-spinners';
5
6
  import logSymbols from 'log-symbols';
6
- import stripAnsi from 'strip-ansi';
7
7
  import stringWidth from 'string-width';
8
8
  import isInteractive from 'is-interactive';
9
9
  import isUnicodeSupported from 'is-unicode-supported';
10
10
  import stdinDiscarder from 'stdin-discarder';
11
11
 
12
+ // Constants
13
+ const RENDER_DEFERRAL_TIMEOUT = 200; // Milliseconds to wait before re-rendering after partial chunk write
14
+ const SYNCHRONIZED_OUTPUT_ENABLE = '\u001B[?2026h';
15
+ const SYNCHRONIZED_OUTPUT_DISABLE = '\u001B[?2026l';
16
+
17
+ // Global state for concurrent spinner detection
18
+ const activeHooksPerStream = new Map(); // Stream → ora instance
19
+
12
20
  class Ora {
13
21
  #linesToClear = 0;
14
- #isDiscardingStdin = false;
15
- #lineCount = 0;
16
22
  #frameIndex = -1;
17
- #lastSpinnerFrameTime = 0;
18
- #lastIndent = 0;
23
+ #lastFrameTime = 0;
19
24
  #options;
20
25
  #spinner;
21
26
  #stream;
22
27
  #id;
23
- #initialInterval;
24
- #isEnabled;
25
- #isSilent;
26
- #indent;
27
- #text;
28
- #prefixText;
29
- #suffixText;
28
+ #hookedStreams = new Map();
29
+ #isInternalWrite = false;
30
+ #drainHandler;
31
+ #deferRenderTimer;
32
+ #isDiscardingStdin = false;
30
33
  color;
31
34
 
35
+ // Helper to execute writes while preventing hook recursion
36
+ #internalWrite(fn) {
37
+ this.#isInternalWrite = true;
38
+ try {
39
+ return fn();
40
+ } finally {
41
+ this.#isInternalWrite = false;
42
+ }
43
+ }
44
+
45
+ // Helper to render if still spinning
46
+ #tryRender() {
47
+ if (this.isSpinning) {
48
+ this.render();
49
+ }
50
+ }
51
+
52
+ #stringifyChunk(chunk, encoding) {
53
+ if (chunk === undefined || chunk === null) {
54
+ return '';
55
+ }
56
+
57
+ if (typeof chunk === 'string') {
58
+ return chunk;
59
+ }
60
+
61
+ /* eslint-disable n/prefer-global/buffer */
62
+ if (Buffer.isBuffer(chunk) || ArrayBuffer.isView(chunk)) {
63
+ const normalizedEncoding = (typeof encoding === 'string' && encoding && encoding !== 'buffer') ? encoding : 'utf8';
64
+ return Buffer.from(chunk).toString(normalizedEncoding);
65
+ }
66
+ /* eslint-enable n/prefer-global/buffer */
67
+
68
+ return String(chunk);
69
+ }
70
+
71
+ #chunkTerminatesLine(chunkString) {
72
+ if (!chunkString) {
73
+ return false;
74
+ }
75
+
76
+ const lastCharacter = chunkString.at(-1);
77
+ return lastCharacter === '\n' || lastCharacter === '\r';
78
+ }
79
+
80
+ #scheduleRenderDeferral() {
81
+ // If already deferred, don't reset timer - let it complete
82
+ if (this.#deferRenderTimer) {
83
+ return;
84
+ }
85
+
86
+ this.#deferRenderTimer = setTimeout(() => {
87
+ this.#deferRenderTimer = undefined;
88
+
89
+ if (this.isSpinning) {
90
+ this.#tryRender();
91
+ }
92
+ }, RENDER_DEFERRAL_TIMEOUT);
93
+
94
+ if (typeof this.#deferRenderTimer?.unref === 'function') {
95
+ this.#deferRenderTimer.unref();
96
+ }
97
+ }
98
+
99
+ #clearRenderDeferral() {
100
+ if (this.#deferRenderTimer) {
101
+ clearTimeout(this.#deferRenderTimer);
102
+ this.#deferRenderTimer = undefined;
103
+ }
104
+ }
105
+
106
+ // Helper to build complete line with symbol, text, prefix, and suffix
107
+ #buildOutputLine(symbol, text, prefixText, suffixText) {
108
+ const fullPrefixText = this.#getFullPrefixText(prefixText, ' ');
109
+ const separatorText = symbol ? ' ' : '';
110
+ const fullText = (typeof text === 'string') ? separatorText + text : '';
111
+ const fullSuffixText = this.#getFullSuffixText(suffixText, ' ');
112
+ return fullPrefixText + symbol + fullText + fullSuffixText;
113
+ }
114
+
32
115
  constructor(options) {
33
116
  if (typeof options === 'string') {
34
117
  options = {
@@ -47,16 +130,23 @@ class Ora {
47
130
  // Public
48
131
  this.color = this.#options.color;
49
132
 
50
- // It's important that these use the public setters.
51
- this.spinner = this.#options.spinner;
52
-
53
- this.#initialInterval = this.#options.interval;
54
133
  this.#stream = this.#options.stream;
55
- this.#isEnabled = typeof this.#options.isEnabled === 'boolean' ? this.#options.isEnabled : isInteractive({stream: this.#stream});
56
- this.#isSilent = typeof this.#options.isSilent === 'boolean' ? this.#options.isSilent : false;
134
+
135
+ // Normalize isEnabled and isSilent into options
136
+ if (typeof this.#options.isEnabled !== 'boolean') {
137
+ this.#options.isEnabled = isInteractive({stream: this.#stream});
138
+ }
139
+
140
+ if (typeof this.#options.isSilent !== 'boolean') {
141
+ this.#options.isSilent = false;
142
+ }
57
143
 
58
144
  // Set *after* `this.#stream`.
145
+ // Store original interval before spinner setter clears it
146
+ const userInterval = this.#options.interval;
59
147
  // It's important that these use the public setters.
148
+ this.spinner = this.#options.spinner;
149
+ this.#options.interval = userInterval;
60
150
  this.text = this.#options.text;
61
151
  this.prefixText = this.#options.prefixText;
62
152
  this.suffixText = this.#options.suffixText;
@@ -64,7 +154,7 @@ class Ora {
64
154
 
65
155
  if (process.env.NODE_ENV === 'test') {
66
156
  this._stream = this.#stream;
67
- this._isEnabled = this.#isEnabled;
157
+ this._isEnabled = this.#options.isEnabled;
68
158
 
69
159
  Object.defineProperty(this, '_linesToClear', {
70
160
  get() {
@@ -83,14 +173,21 @@ class Ora {
83
173
 
84
174
  Object.defineProperty(this, '_lineCount', {
85
175
  get() {
86
- return this.#lineCount;
176
+ const columns = this.#stream.columns ?? 80;
177
+ const prefixText = typeof this.#options.prefixText === 'function' ? '' : this.#options.prefixText;
178
+ const suffixText = typeof this.#options.suffixText === 'function' ? '' : this.#options.suffixText;
179
+ const fullPrefixText = (typeof prefixText === 'string' && prefixText !== '') ? prefixText + ' ' : '';
180
+ const fullSuffixText = (typeof suffixText === 'string' && suffixText !== '') ? ' ' + suffixText : '';
181
+ const spinnerChar = '-';
182
+ const fullText = ' '.repeat(this.#options.indent) + fullPrefixText + spinnerChar + (typeof this.#options.text === 'string' ? ' ' + this.#options.text : '') + fullSuffixText;
183
+ return this.#computeLineCountFrom(fullText, columns);
87
184
  },
88
185
  });
89
186
  }
90
187
  }
91
188
 
92
189
  get indent() {
93
- return this.#indent;
190
+ return this.#options.indent;
94
191
  }
95
192
 
96
193
  set indent(indent = 0) {
@@ -98,12 +195,11 @@ class Ora {
98
195
  throw new Error('The `indent` option must be an integer from 0 and up');
99
196
  }
100
197
 
101
- this.#indent = indent;
102
- this.#updateLineCount();
198
+ this.#options.indent = indent;
103
199
  }
104
200
 
105
201
  get interval() {
106
- return this.#initialInterval ?? this.#spinner.interval ?? 100;
202
+ return this.#options.interval ?? this.#spinner.interval ?? 100;
107
203
  }
108
204
 
109
205
  get spinner() {
@@ -112,7 +208,7 @@ class Ora {
112
208
 
113
209
  set spinner(spinner) {
114
210
  this.#frameIndex = -1;
115
- this.#initialInterval = undefined;
211
+ this.#options.interval = undefined;
116
212
 
117
213
  if (typeof spinner === 'object') {
118
214
  if (!Array.isArray(spinner.frames) || spinner.frames.length === 0 || spinner.frames.some(frame => typeof frame !== 'string')) {
@@ -137,30 +233,27 @@ class Ora {
137
233
  }
138
234
 
139
235
  get text() {
140
- return this.#text;
236
+ return this.#options.text;
141
237
  }
142
238
 
143
239
  set text(value = '') {
144
- this.#text = value;
145
- this.#updateLineCount();
240
+ this.#options.text = value;
146
241
  }
147
242
 
148
243
  get prefixText() {
149
- return this.#prefixText;
244
+ return this.#options.prefixText;
150
245
  }
151
246
 
152
247
  set prefixText(value = '') {
153
- this.#prefixText = value;
154
- this.#updateLineCount();
248
+ this.#options.prefixText = value;
155
249
  }
156
250
 
157
251
  get suffixText() {
158
- return this.#suffixText;
252
+ return this.#options.suffixText;
159
253
  }
160
254
 
161
255
  set suffixText(value = '') {
162
- this.#suffixText = value;
163
- this.#updateLineCount();
256
+ this.#options.suffixText = value;
164
257
  }
165
258
 
166
259
  get isSpinning() {
@@ -176,39 +269,25 @@ class Ora {
176
269
  return '';
177
270
  }
178
271
 
179
- #getFullPrefixText(prefixText = this.#prefixText, postfix = ' ') {
272
+ #getFullPrefixText(prefixText = this.#options.prefixText, postfix = ' ') {
180
273
  return this.#formatAffix(prefixText, postfix, false);
181
274
  }
182
275
 
183
- #getFullSuffixText(suffixText = this.#suffixText, prefix = ' ') {
276
+ #getFullSuffixText(suffixText = this.#options.suffixText, prefix = ' ') {
184
277
  return this.#formatAffix(suffixText, prefix, true);
185
278
  }
186
279
 
187
280
  #computeLineCountFrom(text, columns) {
188
281
  let count = 0;
189
- for (const line of stripAnsi(text).split('\n')) {
282
+ for (const line of stripVTControlCharacters(text).split('\n')) {
190
283
  count += Math.max(1, Math.ceil(stringWidth(line) / columns));
191
284
  }
192
285
 
193
286
  return count;
194
287
  }
195
288
 
196
- #updateLineCount() {
197
- const columns = this.#stream.columns ?? 80;
198
-
199
- // Simple side-effect free approximation (do not call functions)
200
- const prefixText = typeof this.#prefixText === 'function' ? '' : this.#prefixText;
201
- const suffixText = typeof this.#suffixText === 'function' ? '' : this.#suffixText;
202
- const fullPrefixText = (typeof prefixText === 'string' && prefixText !== '') ? prefixText + ' ' : '';
203
- const fullSuffixText = (typeof suffixText === 'string' && suffixText !== '') ? ' ' + suffixText : '';
204
- const spinnerChar = '-';
205
- const fullText = ' '.repeat(this.#indent) + fullPrefixText + spinnerChar + (typeof this.#text === 'string' ? ' ' + this.#text : '') + fullSuffixText;
206
-
207
- this.#lineCount = this.#computeLineCountFrom(fullText, columns);
208
- }
209
-
210
289
  get isEnabled() {
211
- return this.#isEnabled && !this.#isSilent;
290
+ return this.#options.isEnabled && !this.#options.isSilent;
212
291
  }
213
292
 
214
293
  set isEnabled(value) {
@@ -216,11 +295,11 @@ class Ora {
216
295
  throw new TypeError('The `isEnabled` option must be a boolean');
217
296
  }
218
297
 
219
- this.#isEnabled = value;
298
+ this.#options.isEnabled = value;
220
299
  }
221
300
 
222
301
  get isSilent() {
223
- return this.#isSilent;
302
+ return this.#options.isSilent;
224
303
  }
225
304
 
226
305
  set isSilent(value) {
@@ -228,16 +307,15 @@ class Ora {
228
307
  throw new TypeError('The `isSilent` option must be a boolean');
229
308
  }
230
309
 
231
- this.#isSilent = value;
310
+ this.#options.isSilent = value;
232
311
  }
233
312
 
234
313
  frame() {
235
- // Ensure we only update the spinner frame at the wanted interval,
236
- // even if the render method is called more often.
314
+ // Only advance frame if enough time has passed (throttle to interval)
237
315
  const now = Date.now();
238
- if (this.#frameIndex === -1 || now - this.#lastSpinnerFrameTime >= this.interval) {
239
- this.#frameIndex = ++this.#frameIndex % this.#spinner.frames.length;
240
- this.#lastSpinnerFrameTime = now;
316
+ if (this.#frameIndex === -1 || now - this.#lastFrameTime >= this.interval) {
317
+ this.#frameIndex = (this.#frameIndex + 1) % this.#spinner.frames.length;
318
+ this.#lastFrameTime = now;
241
319
  }
242
320
 
243
321
  const {frames} = this.#spinner;
@@ -247,59 +325,166 @@ class Ora {
247
325
  frame = chalk[this.color](frame);
248
326
  }
249
327
 
250
- const fullPrefixText = this.#getFullPrefixText(this.#prefixText, ' ');
328
+ const fullPrefixText = this.#getFullPrefixText(this.#options.prefixText, ' ');
251
329
  const fullText = typeof this.text === 'string' ? ' ' + this.text : '';
252
- const fullSuffixText = this.#getFullSuffixText(this.#suffixText, ' ');
330
+ const fullSuffixText = this.#getFullSuffixText(this.#options.suffixText, ' ');
253
331
 
254
332
  return fullPrefixText + frame + fullText + fullSuffixText;
255
333
  }
256
334
 
257
335
  clear() {
258
- if (!this.#isEnabled || !this.#stream.isTTY) {
336
+ if (!this.isEnabled || !this.#stream.isTTY) {
259
337
  return this;
260
338
  }
261
339
 
262
- this.#stream.cursorTo(0);
340
+ // Protect cursor control methods (cursorTo, moveCursor, clearLine) which internally call stream.write
341
+ this.#internalWrite(() => {
342
+ this.#stream.cursorTo(0);
343
+
344
+ for (let index = 0; index < this.#linesToClear; index++) {
345
+ if (index > 0) {
346
+ this.#stream.moveCursor(0, -1);
347
+ }
348
+
349
+ this.#stream.clearLine(1);
350
+ }
351
+
352
+ if (this.#options.indent) {
353
+ this.#stream.cursorTo(this.#options.indent);
354
+ }
355
+ });
356
+
357
+ this.#linesToClear = 0;
358
+
359
+ return this;
360
+ }
361
+
362
+ // Helper to hook a single stream
363
+ #hookStream(stream) {
364
+ if (!stream || this.#hookedStreams.has(stream) || !stream.isTTY || typeof stream.write !== 'function') {
365
+ return;
366
+ }
367
+
368
+ // Detect concurrent spinners
369
+ if (activeHooksPerStream.has(stream)) {
370
+ console.warn('[ora] Multiple concurrent spinners detected. This may cause visual corruption. Use one spinner at a time.');
371
+ }
372
+
373
+ const originalWrite = stream.write;
374
+ this.#hookedStreams.set(stream, originalWrite);
375
+ activeHooksPerStream.set(stream, this);
376
+ stream.write = (chunk, encoding, callback) => this.#hookedWrite(stream, originalWrite, chunk, encoding, callback);
377
+ }
378
+
379
+ /**
380
+ Intercept stream writes while spinner is active to handle external writes cleanly without visual corruption.
381
+ Hooks process stdio streams and the active spinner stream so console.log(), console.error(), and direct writes stay tidy.
382
+ */
383
+ #installHook() {
384
+ if (!this.isEnabled || this.#hookedStreams.size > 0) {
385
+ return;
386
+ }
387
+
388
+ const streamsToHook = new Set([this.#stream, process.stdout, process.stderr]);
263
389
 
264
- for (let index = 0; index < this.#linesToClear; index++) {
265
- if (index > 0) {
266
- this.#stream.moveCursor(0, -1);
390
+ for (const stream of streamsToHook) {
391
+ this.#hookStream(stream);
392
+ }
393
+ }
394
+
395
+ #uninstallHook() {
396
+ for (const [stream, originalWrite] of this.#hookedStreams) {
397
+ stream.write = originalWrite;
398
+ if (activeHooksPerStream.get(stream) === this) {
399
+ activeHooksPerStream.delete(stream);
267
400
  }
401
+ }
268
402
 
269
- this.#stream.clearLine(1);
403
+ this.#hookedStreams.clear();
404
+ }
405
+
406
+ // eslint-disable-next-line max-params -- Need stream and originalWrite for multi-stream support
407
+ #hookedWrite(stream, originalWrite, chunk, encoding, callback) {
408
+ // Handle both write(chunk, encoding, callback) and write(chunk, callback) signatures
409
+ if (typeof encoding === 'function') {
410
+ callback = encoding;
411
+ encoding = undefined;
270
412
  }
271
413
 
272
- if (this.#indent || this.#lastIndent !== this.#indent) {
273
- this.#stream.cursorTo(this.#indent);
414
+ // Pass through our own internal writes (spinner rendering, cursor control)
415
+ if (this.#isInternalWrite) {
416
+ return originalWrite.call(stream, chunk, encoding, callback);
274
417
  }
275
418
 
276
- this.#lastIndent = this.#indent;
277
- this.#linesToClear = 0;
419
+ // External write detected - clear spinner, write content, re-render if appropriate
420
+ this.clear();
278
421
 
279
- return this;
422
+ const chunkString = this.#stringifyChunk(chunk, encoding);
423
+ const chunkTerminatesLine = this.#chunkTerminatesLine(chunkString);
424
+
425
+ const writeResult = originalWrite.call(stream, chunk, encoding, callback);
426
+
427
+ // Schedule or clear render deferral based on chunk content
428
+ if (chunkTerminatesLine) {
429
+ this.#clearRenderDeferral();
430
+ } else if (chunkString.length > 0) {
431
+ this.#scheduleRenderDeferral();
432
+ }
433
+
434
+ // Re-render spinner below the new output if still spinning and not deferred
435
+ if (this.isSpinning && !this.#deferRenderTimer) {
436
+ this.render();
437
+ }
438
+
439
+ return writeResult;
280
440
  }
281
441
 
282
442
  render() {
283
- if (!this.#isEnabled || this.#isSilent) {
443
+ if (!this.isEnabled || this.#drainHandler || this.#deferRenderTimer) {
284
444
  return this;
285
445
  }
286
446
 
287
- this.clear();
447
+ const useSynchronizedOutput = this.#stream.isTTY;
448
+ let shouldDisableSynchronizedOutput = false;
288
449
 
289
- let frameContent = this.frame();
290
- const columns = this.#stream.columns ?? 80;
291
- const actualLineCount = this.#computeLineCountFrom(frameContent, columns);
450
+ try {
451
+ if (useSynchronizedOutput) {
452
+ this.#internalWrite(() => this.#stream.write(SYNCHRONIZED_OUTPUT_ENABLE));
453
+ shouldDisableSynchronizedOutput = true;
454
+ }
292
455
 
293
- // If content would exceed viewport height, truncate it to prevent garbage
294
- const consoleHeight = this.#stream.rows;
295
- if (consoleHeight && consoleHeight > 1 && actualLineCount > consoleHeight) {
296
- const lines = frameContent.split('\n');
297
- const maxLines = consoleHeight - 1; // Reserve one line for truncation message
298
- frameContent = [...lines.slice(0, maxLines), '... (content truncated to fit terminal)'].join('\n');
299
- }
456
+ this.clear();
457
+
458
+ let frameContent = this.frame();
459
+ const columns = this.#stream.columns ?? 80;
460
+ const actualLineCount = this.#computeLineCountFrom(frameContent, columns);
461
+
462
+ // If content would exceed viewport height, truncate it to prevent garbage
463
+ const consoleHeight = this.#stream.rows;
464
+ if (consoleHeight && consoleHeight > 1 && actualLineCount > consoleHeight) {
465
+ const lines = frameContent.split('\n');
466
+ const maxLines = consoleHeight - 1; // Reserve one line for truncation message
467
+ frameContent = [...lines.slice(0, maxLines), '... (content truncated to fit terminal)'].join('\n');
468
+ }
469
+
470
+ const canContinue = this.#internalWrite(() => this.#stream.write(frameContent));
300
471
 
301
- this.#stream.write(frameContent);
302
- this.#linesToClear = this.#computeLineCountFrom(frameContent, columns);
472
+ // Handle backpressure - pause rendering if stream buffer is full
473
+ if (canContinue === false && this.#stream.isTTY) {
474
+ this.#drainHandler = () => {
475
+ this.#drainHandler = undefined;
476
+ this.#tryRender();
477
+ };
478
+
479
+ this.#stream.once('drain', this.#drainHandler);
480
+ }
481
+
482
+ this.#linesToClear = this.#computeLineCountFrom(frameContent, columns);
483
+ } finally {
484
+ if (shouldDisableSynchronizedOutput) {
485
+ this.#internalWrite(() => this.#stream.write(SYNCHRONIZED_OUTPUT_DISABLE));
486
+ }
487
+ }
303
488
 
304
489
  return this;
305
490
  }
@@ -309,15 +494,16 @@ class Ora {
309
494
  this.text = text;
310
495
  }
311
496
 
312
- if (this.#isSilent) {
497
+ if (this.isSilent) {
313
498
  return this;
314
499
  }
315
500
 
316
- if (!this.#isEnabled) {
317
- const line = ' '.repeat(this.#indent) + this.#getFullPrefixText(this.#prefixText, ' ') + (this.text ? `- ${this.text}` : '') + this.#getFullSuffixText(this.#suffixText, ' ');
501
+ if (!this.isEnabled) {
502
+ const symbol = this.text ? '-' : '';
503
+ const line = ' '.repeat(this.#options.indent) + this.#buildOutputLine(symbol, this.text, this.#options.prefixText, this.#options.suffixText);
318
504
 
319
505
  if (line.trim() !== '') {
320
- this.#stream.write(line + '\n');
506
+ this.#internalWrite(() => this.#stream.write(line + '\n'));
321
507
  }
322
508
 
323
509
  return this;
@@ -332,10 +518,11 @@ class Ora {
332
518
  }
333
519
 
334
520
  if (this.#options.discardStdin && process.stdin.isTTY) {
335
- this.#isDiscardingStdin = true;
336
521
  stdinDiscarder.start();
522
+ this.#isDiscardingStdin = true;
337
523
  }
338
524
 
525
+ this.#installHook();
339
526
  this.render();
340
527
  this.#id = setInterval(this.render.bind(this), this.interval);
341
528
 
@@ -345,18 +532,28 @@ class Ora {
345
532
  stop() {
346
533
  clearInterval(this.#id);
347
534
  this.#id = undefined;
348
- this.#frameIndex = 0;
535
+ this.#frameIndex = -1;
536
+ this.#lastFrameTime = 0;
537
+
538
+ this.#clearRenderDeferral();
539
+ this.#uninstallHook();
349
540
 
350
- if (this.#isEnabled) {
541
+ // Clean up drain handler if it exists
542
+ if (this.#drainHandler) {
543
+ this.#stream.removeListener('drain', this.#drainHandler);
544
+ this.#drainHandler = undefined;
545
+ }
546
+
547
+ if (this.isEnabled) {
351
548
  this.clear();
352
549
  if (this.#options.hideCursor) {
353
550
  cliCursor.show(this.#stream);
354
551
  }
355
552
  }
356
553
 
357
- if (this.#options.discardStdin && process.stdin.isTTY && this.#isDiscardingStdin) {
358
- stdinDiscarder.stop();
554
+ if (this.#isDiscardingStdin) {
359
555
  this.#isDiscardingStdin = false;
556
+ stdinDiscarder.stop();
360
557
  }
361
558
 
362
559
  return this;
@@ -379,26 +576,19 @@ class Ora {
379
576
  }
380
577
 
381
578
  stopAndPersist(options = {}) {
382
- if (this.#isSilent) {
579
+ if (this.isSilent) {
383
580
  return this;
384
581
  }
385
582
 
386
- const prefixText = options.prefixText ?? this.#prefixText;
387
- const fullPrefixText = this.#getFullPrefixText(prefixText, ' ');
388
-
389
- const symbolText = options.symbol ?? ' ';
390
-
583
+ const symbol = options.symbol ?? ' ';
391
584
  const text = options.text ?? this.text;
392
- const separatorText = symbolText ? ' ' : '';
393
- const fullText = (typeof text === 'string') ? separatorText + text : '';
394
-
395
- const suffixText = options.suffixText ?? this.#suffixText;
396
- const fullSuffixText = this.#getFullSuffixText(suffixText, ' ');
585
+ const prefixText = options.prefixText ?? this.#options.prefixText;
586
+ const suffixText = options.suffixText ?? this.#options.suffixText;
397
587
 
398
- const textToWrite = fullPrefixText + symbolText + fullText + fullSuffixText + '\n';
588
+ const textToWrite = this.#buildOutputLine(symbol, text, prefixText, suffixText) + '\n';
399
589
 
400
590
  this.stop();
401
- this.#stream.write(textToWrite);
591
+ this.#internalWrite(() => this.#stream.write(textToWrite));
402
592
 
403
593
  return this;
404
594
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ora",
3
- "version": "9.0.0",
3
+ "version": "9.3.0",
4
4
  "description": "Elegant terminal spinner",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/ora",
@@ -49,9 +49,8 @@
49
49
  "is-interactive": "^2.0.0",
50
50
  "is-unicode-supported": "^2.1.0",
51
51
  "log-symbols": "^7.0.1",
52
- "stdin-discarder": "^0.2.2",
53
- "string-width": "^8.1.0",
54
- "strip-ansi": "^7.1.2"
52
+ "stdin-discarder": "^0.3.1",
53
+ "string-width": "^8.1.0"
55
54
  },
56
55
  "devDependencies": {
57
56
  "@types/node": "^24.5.0",
@@ -138,6 +138,8 @@ Discard stdin input (except Ctrl+C) while running if it's TTY. This prevents the
138
138
 
139
139
  This has no effect on Windows as there is no good way to implement discarding stdin properly there.
140
140
 
141
+ Note: `discardStdin` puts stdin into raw mode. In raw mode, `Ctrl+C` no longer generates `SIGINT` from the terminal. Ora re-emits `Ctrl+C` from stdin input, but if your code blocks the event loop with synchronous work, `Ctrl+C` handling is delayed until the blocking work ends. Use async APIs, a worker thread, or a child process to keep `Ctrl+C` responsive, or set `discardStdin` to `false`.
142
+
141
143
  ### Instance
142
144
 
143
145
  #### .text <sup>get/set</sup>
@@ -312,6 +314,28 @@ const spinner = ora(`Loading ${chalk.red('unicorns')}`).start();
312
314
 
313
315
  JavaScript is single-threaded, so any synchronous operations will block the spinner's animation. To avoid this, prefer using asynchronous operations.
314
316
 
317
+ ### Why do I get the line spinner on Windows Terminal or WSL?
318
+
319
+ Windows Terminal does [not expose](https://github.com/microsoft/terminal/issues/1040) a reliable, stable way to detect itself or Unicode support from Node, and `WT_SESSION` is explicitly informative, not a detection API and can be inherited by other terminals. That makes environment-based detection best-effort. If you are in a Unicode-capable terminal, set `spinner` explicitly, for example `ora({spinner: 'dots'})`.
320
+
321
+ ### Can I log messages while the spinner is running?
322
+
323
+ Yes! Ora automatically handles writes to the same stream. The spinner will temporarily clear itself, output your message, and re-render below:
324
+
325
+ ```js
326
+ const spinner = ora('Processing...').start();
327
+
328
+ console.log('Step 1 complete');
329
+ console.log('Step 2 complete');
330
+
331
+ spinner.succeed('Done!');
332
+ ```
333
+
334
+ The output will be clean with each log appearing above the spinner. This works seamlessly without requiring any special logging methods. Both `console.log()` (stdout) and `console.error()`/`console.warn()` (stderr) are supported.
335
+
336
+ > [!NOTE]
337
+ > Don't run multiple spinners concurrently. Use one spinner at a time.
338
+
315
339
  ### Can I display multiple spinners simultaneously?
316
340
 
317
341
  No. Ora is designed to display a single spinner at a time. For multiple concurrent progress indicators, consider alternatives like [listr2](https://github.com/listr2/listr2) or [spinnies](https://github.com/jcarpanelli/spinnies).