@kikkimo/claude-launcher 2.0.0 → 2.2.0

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,715 @@
1
+ /**
2
+ * StdinManager - Unified stdin state management
3
+ *
4
+ * This module provides centralized management of stdin operations,
5
+ * preventing conflicts between different modules that need to interact
6
+ * with user input (raw mode, readline, password input, etc.)
7
+ */
8
+
9
+ const readline = require('readline');
10
+ const EventEmitter = require('events');
11
+ const crypto = require('crypto');
12
+ const colors = require('../ui/colors');
13
+ const i18n = require('../i18n');
14
+
15
+ /**
16
+ * Generate a cryptographically unique ID
17
+ * Uses crypto.randomUUID() when available (Node 14.17+, 15.6+),
18
+ * falls back to crypto.randomBytes-based hex string for older versions
19
+ * @returns {string} A unique identifier
20
+ */
21
+ function generateUniqueId() {
22
+ // Use randomUUID if available (Node.js 14.17+, 15.6+)
23
+ if (typeof crypto.randomUUID === 'function') {
24
+ return crypto.randomUUID();
25
+ }
26
+ // Fallback: generate 16 random bytes and convert to hex (32 characters)
27
+ return crypto.randomBytes(16).toString('hex');
28
+ }
29
+
30
+ class StdinManager extends EventEmitter {
31
+ constructor() {
32
+ super();
33
+ this.currentOwner = null;
34
+ this.activeScope = null; // Track currently active scope object
35
+ this.ownerStack = [];
36
+ this.listeners = new Map();
37
+ this.debugMode = process.env.DEBUG_STDIN === 'true';
38
+ // Suspension flag: when true, parent process should not attach/read stdin at all
39
+ this.suspended = false;
40
+
41
+ // Save initial state
42
+ this.initialState = this._captureState();
43
+
44
+ // Track if we're in a cleanup phase to prevent recursion
45
+ this.isCleaningUp = false;
46
+
47
+ // Ctrl+C double-press management
48
+ this.ctrlCCount = 0;
49
+ this.ctrlCTimer = null;
50
+ this.ctrlCEnabled = true; // Flag to enable/disable Ctrl+C monitoring
51
+ }
52
+
53
+ /**
54
+ * Capture current stdin state
55
+ */
56
+ _captureState() {
57
+ if (!process.stdin.isTTY) {
58
+ return {
59
+ isTTY: false,
60
+ isPaused: true,
61
+ isRaw: false,
62
+ encoding: 'utf8'
63
+ };
64
+ }
65
+
66
+ return {
67
+ isTTY: true,
68
+ isPaused: typeof process.stdin.isPaused === 'function' ? process.stdin.isPaused() : true,
69
+ isRaw: process.stdin.isRaw || false,
70
+ encoding: process.stdin.readableEncoding || 'utf8',
71
+ listeners: {
72
+ data: process.stdin.listenerCount('data'),
73
+ keypress: process.stdin.listenerCount('keypress'),
74
+ readable: process.stdin.listenerCount('readable')
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Apply a state to stdin
81
+ */
82
+ _applyState(state) {
83
+ if (!process.stdin.isTTY || !state.isTTY) {
84
+ return;
85
+ }
86
+
87
+ try {
88
+ // Set raw mode
89
+ if (state.isRaw !== undefined) {
90
+ process.stdin.setRawMode(state.isRaw);
91
+ }
92
+
93
+ // Set encoding
94
+ if (state.encoding) {
95
+ process.stdin.setEncoding(state.encoding);
96
+ }
97
+
98
+ // Pause/resume
99
+ if (state.isPaused) {
100
+ process.stdin.pause();
101
+ } else {
102
+ process.stdin.resume();
103
+ }
104
+ } catch (error) {
105
+ this._debug('Error applying state:', error.message);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Debug logging
111
+ */
112
+ _debug(...args) {
113
+ if (this.debugMode) {
114
+ console.error('[StdinManager]', ...args);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Acquire control of stdin with a specific mode
120
+ * @param {string} mode - 'raw', 'line', 'password'
121
+ * @param {object} options - Additional options
122
+ * @returns {StdinScope} - A scope object to manage the acquired state
123
+ */
124
+ acquire(mode, options = {}) {
125
+ // If suspended, return a no-op scope that does not touch stdin
126
+ if (this.suspended) {
127
+ this._debug(`Acquire ignored (suspended): ${options.id || 'anonymous'} requested ${mode}`);
128
+ return new NoopStdinScope();
129
+ }
130
+ const callerId = options.id || `anonymous_${generateUniqueId()}`;
131
+
132
+ if (this.currentOwner && this.currentOwner !== callerId && !options.force) {
133
+ this._debug(`Warning: ${callerId} trying to acquire while ${this.currentOwner} owns stdin`);
134
+ if (!options.allowNested) {
135
+ throw new Error(`Stdin is currently owned by ${this.currentOwner}`);
136
+ }
137
+ }
138
+
139
+ // Save current state before acquiring
140
+ const previousState = this._captureState();
141
+ const previousOwner = this.currentOwner;
142
+ const previousScope = this.activeScope;
143
+
144
+ // Push to stack if nesting
145
+ if (this.currentOwner && options.allowNested) {
146
+ // Detach listeners from previous scope but do not clear global listeners
147
+ if (previousScope && typeof previousScope.detach === 'function') {
148
+ previousScope.detach();
149
+ }
150
+ this.ownerStack.push({
151
+ owner: this.currentOwner,
152
+ state: previousState,
153
+ scope: previousScope
154
+ });
155
+ }
156
+
157
+ this.currentOwner = callerId;
158
+ this._debug(`${callerId} acquired stdin in ${mode} mode`);
159
+
160
+ // Create scope object
161
+ const scope = new StdinScope(this, callerId, mode, previousState, previousOwner);
162
+
163
+ // Apply the requested mode
164
+ switch (mode) {
165
+ case 'raw':
166
+ this._setupRawMode(scope);
167
+ break;
168
+ case 'line':
169
+ this._setupLineMode(scope);
170
+ break;
171
+ case 'password':
172
+ this._setupPasswordMode(scope);
173
+ break;
174
+ default:
175
+ throw new Error(`Unknown mode: ${mode}`);
176
+ }
177
+
178
+ // Mark this scope as active
179
+ this.activeScope = scope;
180
+
181
+ return scope;
182
+ }
183
+
184
+ /**
185
+ * Setup raw mode for navigation/menu
186
+ */
187
+ _setupRawMode(scope) {
188
+ if (!process.stdin.isTTY) {
189
+ return;
190
+ }
191
+
192
+ try {
193
+ // Setup for raw mode
194
+ process.stdin.setRawMode(true);
195
+ process.stdin.setEncoding('utf8');
196
+ process.stdin.resume();
197
+
198
+ this._debug(`Raw mode activated for ${scope.ownerId}`);
199
+ } catch (error) {
200
+ this._debug('Error setting up raw mode:', error.message);
201
+ throw error;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Setup line mode for readline interface
207
+ */
208
+ _setupLineMode(scope) {
209
+ try {
210
+ // Ensure we're not in raw mode
211
+ if (process.stdin.isTTY) {
212
+ process.stdin.setRawMode(false);
213
+ process.stdin.setEncoding('utf8');
214
+ // Ensure stream is flowing for readline
215
+ process.stdin.resume();
216
+ }
217
+
218
+ this._debug(`Line mode activated for ${scope.ownerId}`);
219
+ } catch (error) {
220
+ this._debug('Error setting up line mode:', error.message);
221
+ throw error;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Setup password mode (raw with echo off)
227
+ */
228
+ _setupPasswordMode(scope) {
229
+ if (!process.stdin.isTTY) {
230
+ return;
231
+ }
232
+
233
+ try {
234
+ // Clear any existing listeners from other modules
235
+ this._saveAndClearListeners(scope);
236
+
237
+ // Setup for password input
238
+ process.stdin.setRawMode(true);
239
+ process.stdin.setEncoding('utf8');
240
+ process.stdin.resume();
241
+
242
+ this._debug(`Password mode activated for ${scope.ownerId}`);
243
+ } catch (error) {
244
+ this._debug('Error setting up password mode:', error.message);
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Save and clear existing listeners
251
+ */
252
+ _saveAndClearListeners(scope) {
253
+ // Deprecated: avoid global removeAllListeners which causes deadlocks.
254
+ // Kept for backward compatibility but no-op now.
255
+ scope.savedListeners = new Map();
256
+ this._debug('Skipped global removeAllListeners to preserve pending scopes');
257
+ }
258
+
259
+ /**
260
+ * Restore saved listeners
261
+ */
262
+ _restoreListeners(scope) {
263
+ // No-op: we no longer clear global listeners.
264
+ }
265
+
266
+ /**
267
+ * Release control of stdin
268
+ */
269
+ release(callerId) {
270
+ if (this.currentOwner !== callerId) {
271
+ this._debug(`Warning: ${callerId} trying to release but current owner is ${this.currentOwner}`);
272
+ return false;
273
+ }
274
+
275
+ this._debug(`${callerId} releasing stdin control`);
276
+
277
+ // Check if there's a stacked owner to restore
278
+ if (this.ownerStack.length > 0) {
279
+ const previous = this.ownerStack.pop();
280
+ this.currentOwner = previous.owner;
281
+ this.activeScope = previous.scope || null;
282
+ // Reattach previous scope listeners if possible
283
+ if (this.activeScope && typeof this.activeScope.reattach === 'function') {
284
+ this.activeScope.reattach();
285
+ }
286
+ this._applyState(previous.state);
287
+ this._debug(`Restored previous owner ${previous.owner}`);
288
+ } else {
289
+ this.currentOwner = null;
290
+ this.activeScope = null;
291
+ // Restore to safe default state
292
+ this._applyState({
293
+ isTTY: process.stdin.isTTY,
294
+ isPaused: true,
295
+ isRaw: false,
296
+ encoding: 'utf8'
297
+ });
298
+ this._debug('No previous owner, restored to default state');
299
+ }
300
+
301
+ return true;
302
+ }
303
+
304
+ /**
305
+ * Force reset to initial state (emergency use only)
306
+ *
307
+ * This method cleans up only the listeners managed by StdinManager,
308
+ * preserving any listeners registered by other modules.
309
+ *
310
+ * @param {boolean} clearAll - If true, also removes ALL stdin listeners (destructive).
311
+ * Use with extreme caution as it will break other modules.
312
+ * Defaults to false for safe cleanup.
313
+ */
314
+ forceReset(clearAll = false) {
315
+ if (this.isCleaningUp) {
316
+ return; // Prevent recursion
317
+ }
318
+
319
+ this.isCleaningUp = true;
320
+ this._debug('Force resetting stdin to initial state');
321
+
322
+ try {
323
+ if (clearAll) {
324
+ // DESTRUCTIVE: Clear ALL listeners including those from other modules
325
+ // Only use this when you're absolutely sure (e.g., before process.exit)
326
+ this._debug('WARNING: Removing ALL stdin listeners (clearAll=true)');
327
+ process.stdin.removeAllListeners();
328
+ } else {
329
+ // SAFE: Only remove listeners managed by StdinManager
330
+ // Clean up active scope listeners
331
+ if (this.activeScope && typeof this.activeScope.removeAllListeners === 'function') {
332
+ this._debug('Cleaning up active scope listeners');
333
+ this.activeScope.removeAllListeners();
334
+ }
335
+
336
+ // Clean up stacked scope listeners
337
+ this.ownerStack.forEach(item => {
338
+ if (item.scope && typeof item.scope.removeAllListeners === 'function') {
339
+ this._debug(`Cleaning up stacked scope listeners for ${item.owner}`);
340
+ item.scope.removeAllListeners();
341
+ }
342
+ });
343
+ }
344
+
345
+ // Reset state
346
+ this._applyState(this.initialState);
347
+
348
+ // Clear ownership
349
+ this.currentOwner = null;
350
+ this.activeScope = null;
351
+ this.ownerStack = [];
352
+ this.listeners.clear();
353
+ } catch (error) {
354
+ this._debug('Error during force reset:', error.message);
355
+ } finally {
356
+ this.isCleaningUp = false;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Get current status for debugging
362
+ */
363
+ getStatus() {
364
+ return {
365
+ currentOwner: this.currentOwner,
366
+ stackDepth: this.ownerStack.length,
367
+ currentState: this._captureState(),
368
+ isCleaningUp: this.isCleaningUp
369
+ };
370
+ }
371
+
372
+ /**
373
+ * Check if currently waiting for second Ctrl+C
374
+ */
375
+ isCtrlCPending() {
376
+ return this.ctrlCCount > 0;
377
+ }
378
+
379
+ /**
380
+ * Cancel Ctrl+C exit intent (called when user presses any other key)
381
+ */
382
+ cancelCtrlC() {
383
+ if (this.ctrlCCount > 0) {
384
+ this.ctrlCCount = 0;
385
+ if (this.ctrlCTimer) {
386
+ clearTimeout(this.ctrlCTimer);
387
+ this.ctrlCTimer = null;
388
+ }
389
+ // Clear the warning text only in TTY mode to avoid spamming ANSI codes
390
+ if (process.stdout && process.stdout.isTTY) {
391
+ // Clear the warning text (2 lines: empty line + warning line)
392
+ process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
393
+ process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
394
+ }
395
+ this._debug('Ctrl+C cancelled by user input');
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Disable Ctrl+C monitoring
401
+ * Call this before launching Claude Code to prevent Ctrl+C interception
402
+ */
403
+ disableCtrlC() {
404
+ this.ctrlCEnabled = false;
405
+ // Also cancel any pending Ctrl+C state
406
+ if (this.ctrlCCount > 0) {
407
+ this.ctrlCCount = 0;
408
+ if (this.ctrlCTimer) {
409
+ clearTimeout(this.ctrlCTimer);
410
+ this.ctrlCTimer = null;
411
+ }
412
+ }
413
+ this._debug('Ctrl+C monitoring disabled');
414
+ }
415
+
416
+ /**
417
+ * Enable Ctrl+C monitoring
418
+ * Call this to re-enable Ctrl+C monitoring after disabling
419
+ */
420
+ enableCtrlC() {
421
+ this.ctrlCEnabled = true;
422
+ this._debug('Ctrl+C monitoring enabled');
423
+ }
424
+
425
+ /**
426
+ * Private: Reset Ctrl+C state on timeout
427
+ * Clears warning text after 3 seconds of no input
428
+ */
429
+ _resetCtrlCOnTimeout() {
430
+ this.ctrlCCount = 0;
431
+ this.ctrlCTimer = null;
432
+ // Clear the warning text only in TTY mode to avoid spamming ANSI codes
433
+ if (process.stdout && process.stdout.isTTY) {
434
+ // Clear the warning text (2 lines: empty line + warning line)
435
+ process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear warning
436
+ process.stdout.write('\x1b[1A\r\x1b[K'); // Up 1 line, clear empty line
437
+ }
438
+ this._debug('Ctrl+C timer expired - warning cleared, returning to normal operation');
439
+ }
440
+
441
+ /**
442
+ * Handle Ctrl+C with double-press confirmation
443
+ * First press: show warning and start 3-second timer
444
+ * Second press (within 3 seconds): exit program
445
+ * Timer expires: reset count and return to normal operation
446
+ * Any other key: cancel and return to normal operation
447
+ *
448
+ * If Ctrl+C monitoring is disabled (e.g., when launching Claude Code),
449
+ * this function does nothing and returns false
450
+ */
451
+ handleCtrlC() {
452
+ // If suspended, do nothing so child process can receive Ctrl+C
453
+ if (this.suspended) {
454
+ this._debug('Ctrl+C ignored (suspended)');
455
+ return false;
456
+ }
457
+ // If Ctrl+C monitoring is disabled, do nothing
458
+ if (!this.ctrlCEnabled) {
459
+ this._debug('Ctrl+C ignored (monitoring disabled)');
460
+ return false;
461
+ }
462
+
463
+ this.ctrlCCount++;
464
+
465
+ if (this.ctrlCCount === 1) {
466
+ // First Ctrl+C - show warning
467
+ console.log('');
468
+ console.log(colors.yellow + '⚠️ ' + i18n.tSync('messages.prompts.ctrl_c_again') + colors.reset);
469
+
470
+ // Clear any existing timer
471
+ if (this.ctrlCTimer) {
472
+ clearTimeout(this.ctrlCTimer);
473
+ }
474
+
475
+ // Set 3-second timeout to reset count
476
+ this.ctrlCTimer = setTimeout(() => {
477
+ this._resetCtrlCOnTimeout();
478
+ }, 3000);
479
+
480
+ return false; // Don't exit yet, continue normal operation
481
+
482
+ } else if (this.ctrlCCount >= 2) {
483
+ // Second Ctrl+C within timeout - exit
484
+ if (this.ctrlCTimer) {
485
+ clearTimeout(this.ctrlCTimer);
486
+ this.ctrlCTimer = null;
487
+ }
488
+
489
+ console.log('');
490
+ console.log(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset);
491
+
492
+ // Clean up stdin before exit
493
+ // Use clearAll=true since we're exiting anyway
494
+ try {
495
+ this.forceReset(true);
496
+ } catch (error) {
497
+ // Ignore cleanup errors during exit
498
+ }
499
+
500
+ process.exit(0);
501
+ }
502
+ }
503
+ }
504
+
505
+ /**
506
+ * StdinScope - Represents an acquired stdin context
507
+ */
508
+ class StdinScope {
509
+ constructor(manager, ownerId, mode, previousState, previousOwner) {
510
+ this.manager = manager;
511
+ this.ownerId = ownerId;
512
+ this.mode = mode;
513
+ this.previousState = previousState;
514
+ this.previousOwner = previousOwner;
515
+ this.isReleased = false;
516
+ this.listeners = new Map();
517
+ this.savedListeners = null;
518
+ this._detached = false;
519
+ }
520
+
521
+ /**
522
+ * Add an event listener that will be automatically cleaned up
523
+ */
524
+ on(event, handler) {
525
+ if (this.isReleased) {
526
+ throw new Error('Cannot add listener to released scope');
527
+ }
528
+
529
+ if (!this.listeners.has(event)) {
530
+ this.listeners.set(event, []);
531
+ }
532
+ this.listeners.get(event).push(handler);
533
+ process.stdin.on(event, handler);
534
+
535
+ return this;
536
+ }
537
+
538
+ /**
539
+ * Add a one-time event listener
540
+ */
541
+ once(event, handler) {
542
+ if (this.isReleased) {
543
+ throw new Error('Cannot add listener to released scope');
544
+ }
545
+
546
+ const wrappedHandler = (...args) => {
547
+ this.removeListener(event, wrappedHandler);
548
+ handler(...args);
549
+ };
550
+
551
+ this.on(event, wrappedHandler);
552
+ return this;
553
+ }
554
+
555
+ /**
556
+ * Remove an event listener
557
+ */
558
+ removeListener(event, handler) {
559
+ if (this.listeners.has(event)) {
560
+ const handlers = this.listeners.get(event);
561
+ const index = handlers.indexOf(handler);
562
+ if (index !== -1) {
563
+ handlers.splice(index, 1);
564
+ process.stdin.removeListener(event, handler);
565
+ }
566
+ }
567
+ return this;
568
+ }
569
+
570
+ /**
571
+ * Remove all listeners for this scope
572
+ */
573
+ removeAllListeners() {
574
+ this.listeners.forEach((handlers, event) => {
575
+ handlers.forEach(handler => {
576
+ process.stdin.removeListener(event, handler);
577
+ });
578
+ });
579
+ this.listeners.clear();
580
+ return this;
581
+ }
582
+
583
+ /**
584
+ * Detach listeners from process.stdin but keep record for later reattach
585
+ */
586
+ detach() {
587
+ if (this._detached) return;
588
+ this.listeners.forEach((handlers, event) => {
589
+ handlers.forEach(handler => {
590
+ process.stdin.removeListener(event, handler);
591
+ });
592
+ });
593
+ this._detached = true;
594
+ }
595
+
596
+ /**
597
+ * Reattach previously detached listeners
598
+ */
599
+ reattach() {
600
+ if (!this._detached) return;
601
+ this.listeners.forEach((handlers, event) => {
602
+ handlers.forEach(handler => {
603
+ process.stdin.on(event, handler);
604
+ });
605
+ });
606
+ this._detached = false;
607
+ }
608
+
609
+ /**
610
+ * Release this scope and restore previous state
611
+ */
612
+ release() {
613
+ if (this.isReleased) {
614
+ return false;
615
+ }
616
+
617
+ // Remove all our listeners
618
+ this.removeAllListeners();
619
+
620
+ // Restore any saved listeners
621
+ this.manager._restoreListeners(this);
622
+
623
+ // Release from manager (which will restore previous state if needed)
624
+ // Note: manager.release() handles state restoration to avoid double-application
625
+ const released = this.manager.release(this.ownerId);
626
+ this.isReleased = true;
627
+
628
+ return released;
629
+ }
630
+
631
+ /**
632
+ * Create a readline interface with proper cleanup
633
+ */
634
+ createReadline(options = {}) {
635
+ // Capture current stdin state before creating readline
636
+ const stateBeforeReadline = {
637
+ isPaused: typeof process.stdin.isPaused === 'function' ? process.stdin.isPaused() : true,
638
+ isRaw: process.stdin.isRaw || false,
639
+ encoding: process.stdin.readableEncoding || 'utf8'
640
+ };
641
+
642
+ const rl = readline.createInterface({
643
+ input: process.stdin,
644
+ output: process.stdout,
645
+ ...options
646
+ });
647
+
648
+ // Track for cleanup
649
+ const originalClose = rl.close.bind(rl);
650
+ rl.close = () => {
651
+ originalClose();
652
+
653
+ // Critical: restore stdin state after readline closes
654
+ // readline.close() removes its own listeners but may leave stdin in an active state
655
+ try {
656
+ // Restore raw mode state
657
+ if (process.stdin.setRawMode) {
658
+ process.stdin.setRawMode(stateBeforeReadline.isRaw);
659
+ }
660
+
661
+ // Restore pause state
662
+ if (stateBeforeReadline.isPaused && typeof process.stdin.pause === 'function') {
663
+ process.stdin.pause();
664
+ } else if (!stateBeforeReadline.isPaused && typeof process.stdin.resume === 'function') {
665
+ process.stdin.resume();
666
+ }
667
+
668
+ // Restore encoding
669
+ if (process.stdin.setEncoding && stateBeforeReadline.encoding) {
670
+ process.stdin.setEncoding(stateBeforeReadline.encoding);
671
+ }
672
+ } catch (error) {
673
+ this._debug('Error restoring stdin state after readline close:', error.message);
674
+ }
675
+ };
676
+
677
+ return rl;
678
+ }
679
+ }
680
+
681
+ // Create singleton instance
682
+ const stdinManager = new StdinManager();
683
+
684
+ // Export both the class and singleton
685
+ module.exports = stdinManager;
686
+ module.exports.StdinManager = StdinManager;
687
+ module.exports.StdinScope = StdinScope;
688
+ // Suspension API
689
+ stdinManager.suspend = function() {
690
+ this._debug('StdinManager suspended');
691
+ this.suspended = true;
692
+ // Best-effort: clear ctrl-c pending state
693
+ this.cancelCtrlC();
694
+ };
695
+ stdinManager.resume = function() {
696
+ this._debug('StdinManager resumed');
697
+ this.suspended = false;
698
+ };
699
+ stdinManager.isSuspended = function() {
700
+ return !!this.suspended;
701
+ };
702
+
703
+ /**
704
+ * Noop scope used when stdin is suspended to prevent attaching listeners
705
+ */
706
+ class NoopStdinScope {
707
+ on() { return this; }
708
+ once() { return this; }
709
+ removeListener() { return this; }
710
+ removeAllListeners() { return this; }
711
+ detach() { return this; }
712
+ reattach() { return this; }
713
+ release() { return true; }
714
+ createReadline() { return { question: (_, cb)=>cb && cb(''), close: ()=>{} }; }
715
+ }