rl-rockcli 0.0.9 → 0.0.11

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 (90) hide show
  1. package/commands/attach/basic-repl.js +212 -0
  2. package/commands/attach/cleanup-history.js +189 -0
  3. package/commands/attach/cleanup-manager.js +163 -0
  4. package/commands/attach/copy-ui/copyRepl.js +195 -0
  5. package/commands/attach/copy-ui/index.js +7 -0
  6. package/commands/attach/copy-ui/render/outputBlock.js +25 -0
  7. package/commands/attach/copy-ui/viewport/viewport.js +23 -0
  8. package/commands/attach/copy-ui/viewport/wheel.js +14 -0
  9. package/commands/attach/history-manager.js +507 -0
  10. package/commands/attach/history-session.js +48 -0
  11. package/commands/attach/ink-repl/InkREPL.js +1507 -0
  12. package/commands/attach/ink-repl/builtinCommands.js +1253 -0
  13. package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
  14. package/commands/attach/ink-repl/components/Console.js +191 -0
  15. package/commands/attach/ink-repl/components/DetailView.js +148 -0
  16. package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
  17. package/commands/attach/ink-repl/components/InputArea.js +125 -0
  18. package/commands/attach/ink-repl/components/InputLine.js +18 -0
  19. package/commands/attach/ink-repl/components/OutputArea.js +22 -0
  20. package/commands/attach/ink-repl/components/OutputItem.js +96 -0
  21. package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
  22. package/commands/attach/ink-repl/components/Spinner.js +79 -0
  23. package/commands/attach/ink-repl/components/StatusBar.js +106 -0
  24. package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
  25. package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
  26. package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
  27. package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
  28. package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
  29. package/commands/attach/ink-repl/hooks/useResources.js +132 -0
  30. package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
  31. package/commands/attach/ink-repl/index.js +112 -0
  32. package/commands/attach/ink-repl/package.json +3 -0
  33. package/commands/attach/ink-repl/replState.js +947 -0
  34. package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
  35. package/commands/attach/ink-repl/shortcuts/index.js +332 -0
  36. package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
  37. package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
  38. package/commands/attach/ink-repl/themes/index.js +4 -0
  39. package/commands/attach/ink-repl/themes/themeManager.js +45 -0
  40. package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
  41. package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
  42. package/commands/attach/ink-repl/utils/clipboard.js +50 -0
  43. package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
  44. package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
  45. package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
  46. package/commands/attach/ink-repl/utils/formatTime.js +12 -0
  47. package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
  48. package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
  49. package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
  50. package/commands/attach/ink-repl/utils/paramHint.js +60 -0
  51. package/commands/attach/ink-repl/utils/parseError.js +174 -0
  52. package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
  53. package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
  54. package/commands/attach/ink-repl/utils/replSelection.js +205 -0
  55. package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
  56. package/commands/attach/ink-repl/utils/textWrap.js +117 -0
  57. package/commands/attach/ink-repl/utils/truncate.js +115 -0
  58. package/commands/attach/opentui-repl/App.tsx +891 -0
  59. package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
  60. package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
  61. package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
  62. package/commands/attach/opentui-repl/components/Console.tsx +73 -0
  63. package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
  64. package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
  65. package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
  66. package/commands/attach/opentui-repl/components/Header.tsx +24 -0
  67. package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
  68. package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
  69. package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
  70. package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
  71. package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
  72. package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
  73. package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
  74. package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
  75. package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
  76. package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
  77. package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
  78. package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
  79. package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
  80. package/commands/attach/opentui-repl/index.js +99 -0
  81. package/commands/attach/opentui-repl/keybindings.ts +39 -0
  82. package/commands/attach/opentui-repl/package.json +3 -0
  83. package/commands/attach/opentui-repl/render.tsx +72 -0
  84. package/commands/attach/opentui-repl/tsconfig.json +12 -0
  85. package/commands/attach/repl.js +791 -0
  86. package/commands/attach/sandbox-id-resolver.js +56 -0
  87. package/commands/attach/session-manager.js +307 -0
  88. package/commands/attach/ui-mode.js +146 -0
  89. package/commands/attach.js +186 -0
  90. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ /**
6
+ * 底层错误消息友好化(简化版)
7
+ * 用于 CommonJS 模块
8
+ */
9
+ function friendlyErrorMessage(message) {
10
+ if (!message || typeof message !== 'string') {
11
+ return '未知错误';
12
+ }
13
+
14
+ // 常见底层错误映射
15
+ const errorMap = {
16
+ 'First argument must be an Error object': '网络连接异常,请检查网络后重试',
17
+ 'ECONNREFUSED': '无法连接到服务器',
18
+ 'ETIMEDOUT': '请求超时,请重试',
19
+ 'ENOTFOUND': '无法解析服务器地址',
20
+ 'ECONNRESET': '网络连接已断开',
21
+ 'socket hang up': '网络连接已断开',
22
+ 'Network Error': '网络异常',
23
+ };
24
+
25
+ // 精确匹配
26
+ if (errorMap[message]) {
27
+ return errorMap[message];
28
+ }
29
+
30
+ // 部分匹配
31
+ for (const [key, friendly] of Object.entries(errorMap)) {
32
+ if (message.includes(key)) {
33
+ return friendly;
34
+ }
35
+ }
36
+
37
+ // 网络相关关键词
38
+ const networkKeywords = ['network', 'socket', 'connection', 'connect', '网络'];
39
+ const lowerMessage = message.toLowerCase();
40
+ if (networkKeywords.some(kw => lowerMessage.includes(kw))) {
41
+ return '网络连接异常,请检查网络后重试';
42
+ }
43
+
44
+ return message;
45
+ }
46
+
47
+ function normalizeLine(line) {
48
+ if (typeof line !== 'string') return '';
49
+ return line.replace(/\r?\n/g, '').trim();
50
+ }
51
+
52
+ function isExitCommand(line) {
53
+ return line === 'exit' || line === 'quit' || line === '/exit' || line === '/quit';
54
+ }
55
+
56
+ function isCloseCommand(line) {
57
+ return line === 'close' || line === '/close';
58
+ }
59
+
60
+ function isHelpCommand(line) {
61
+ return line === 'help' || line === '/help' || line === '?';
62
+ }
63
+
64
+ function printHelp() {
65
+ console.log('');
66
+ console.log(' Basic attach REPL');
67
+ console.log(' - Type any shell command to run it in the sandbox session');
68
+ console.log(' - Builtins: help, exit, close');
69
+ console.log('');
70
+ }
71
+
72
+ /**
73
+ * Start a minimal readline-based REPL.
74
+ *
75
+ * This mode intentionally avoids importing Ink/React so `attach` can still work
76
+ * on older Node runtimes.
77
+ */
78
+ async function startBasicREPL(options) {
79
+ const {
80
+ initialPrompt = '$ ',
81
+ sessionManager,
82
+ historyManager,
83
+ onExit,
84
+ } = options || {};
85
+
86
+ if (!sessionManager) {
87
+ throw new Error('startBasicREPL requires sessionManager');
88
+ }
89
+
90
+ let closed = false;
91
+ let pending = Promise.resolve();
92
+
93
+ const rl = readline.createInterface({
94
+ input: process.stdin,
95
+ output: process.stdout,
96
+ terminal: true,
97
+ historySize: 500,
98
+ });
99
+
100
+ // Restore input history if available (best-effort)
101
+ if (historyManager && typeof historyManager.getHistory === 'function') {
102
+ try {
103
+ const history = await historyManager.getHistory();
104
+ if (Array.isArray(history) && history.length > 0) {
105
+ // Node readline expects most-recent-first.
106
+ rl.history = history.slice().reverse();
107
+ }
108
+ } catch (e) {
109
+ // ignore history restore errors
110
+ }
111
+ }
112
+
113
+ const cleanup = async () => {
114
+ if (closed) return;
115
+ closed = true;
116
+ try {
117
+ rl.close();
118
+ } catch (e) {
119
+ // ignore
120
+ }
121
+ if (onExit) {
122
+ await onExit();
123
+ }
124
+ };
125
+
126
+ rl.on('SIGINT', () => {
127
+ // Keep behavior simple and predictable
128
+ process.stdout.write('\n');
129
+ cleanup().catch(() => {});
130
+ });
131
+
132
+ rl.on('close', () => {
133
+ closed = true;
134
+ });
135
+
136
+ rl.setPrompt(initialPrompt);
137
+ rl.prompt();
138
+
139
+ return new Promise((resolve) => {
140
+ rl.on('line', (rawLine) => {
141
+ const line = normalizeLine(rawLine);
142
+
143
+ pending = pending.then(async () => {
144
+ if (closed) return;
145
+
146
+ if (!line) {
147
+ rl.prompt();
148
+ return;
149
+ }
150
+
151
+ // Save command to history before processing
152
+ try {
153
+ if (historyManager && typeof historyManager.addCommand === 'function') {
154
+ await historyManager.addCommand(line);
155
+ }
156
+ } catch (e) {
157
+ // ignore history errors
158
+ }
159
+
160
+ if (isHelpCommand(line)) {
161
+ printHelp();
162
+ rl.prompt();
163
+ return;
164
+ }
165
+
166
+ if (isExitCommand(line) || isCloseCommand(line)) {
167
+ await cleanup();
168
+ resolve();
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const result = await sessionManager.execute(line);
174
+ const exitCode = result?.exit_code ?? 0;
175
+ let output = result?.output || '';
176
+
177
+ // Handle abnormal exit codes with helpful messages
178
+ if (exitCode === -1) {
179
+ const hint = output ? '' : 'Command terminated abnormally (exit code -1). This may indicate a timeout, signal termination, or internal error.';
180
+ output = output || hint;
181
+ } else if (exitCode === 137) {
182
+ output += output ? '\n' : '';
183
+ output += '[Process killed (exit code 137). Possibly OOM or manually terminated.]';
184
+ } else if (exitCode === 143) {
185
+ output += output ? '\n' : '';
186
+ output += '[Process terminated (exit code 143). Received SIGTERM.]';
187
+ }
188
+
189
+ if (output && typeof output === 'string' && output.length > 0) {
190
+ process.stdout.write(output);
191
+ if (!output.endsWith('\n')) {
192
+ process.stdout.write('\n');
193
+ }
194
+ }
195
+ } catch (error) {
196
+ const message = error && error.message ? error.message : String(error);
197
+ process.stderr.write(`Error: ${friendlyErrorMessage(message)}\n`);
198
+ }
199
+
200
+ rl.prompt();
201
+ });
202
+ });
203
+
204
+ rl.on('close', () => {
205
+ pending.finally(() => resolve());
206
+ });
207
+ });
208
+ }
209
+
210
+ module.exports = {
211
+ startBasicREPL,
212
+ };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const logger = require('../../utils/logger');
6
+
7
+ class CleanupHistoryHandler {
8
+ constructor(historyManager, options = {}) {
9
+ this.historyManager = historyManager;
10
+ this.sandboxId = options.targetSandboxId || historyManager.sandboxId;
11
+ this.targetSandboxId = options.targetSandboxId || null;
12
+ this.terminal = options.terminal || null;
13
+ this.os = require('os');
14
+ }
15
+
16
+ async getSessionsToCleanup() {
17
+ const index = await this._getIndex();
18
+ const currentSessionId = index.current_session || this.historyManager.sessionId;
19
+
20
+ if (!currentSessionId) {
21
+ throw new Error('No current session found');
22
+ }
23
+
24
+ const currentExists = await this._sessionExists(currentSessionId);
25
+ if (!currentExists) {
26
+ throw new Error('Current session does not exist');
27
+ }
28
+
29
+ const sessions = index.sessions || [];
30
+ return sessions
31
+ .filter(session => session.id !== currentSessionId)
32
+ .map(session => session.id);
33
+ }
34
+
35
+ async _getIndex() {
36
+ const sandboxDir = this._getSandboxDir();
37
+ const indexFile = path.join(sandboxDir, 'index.json');
38
+
39
+ if (!fs.existsSync(indexFile)) {
40
+ return { current_session: null, sessions: [] };
41
+ }
42
+
43
+ try {
44
+ const content = fs.readFileSync(indexFile, 'utf8');
45
+ return JSON.parse(content);
46
+ } catch (error) {
47
+ logger.debug(`Failed to read index.json: ${error.message}`);
48
+ return { current_session: null, sessions: [] };
49
+ }
50
+ }
51
+
52
+ async _sessionExists(sessionId) {
53
+ const sessionDir = path.join(this._getSandboxDir(), sessionId);
54
+ return fs.existsSync(sessionDir);
55
+ }
56
+
57
+ _getSandboxDir() {
58
+ if (this.targetSandboxId) {
59
+ return path.join(this.os.homedir(), '.rock', 'history', this.targetSandboxId);
60
+ }
61
+ return this.historyManager._getSandboxDir();
62
+ }
63
+
64
+ async confirmCleanup(sessions) {
65
+ if (!sessions || sessions.length === 0) {
66
+ if (this.terminal) {
67
+ this.terminal.yellow('无需清理的历史 session\n');
68
+ }
69
+ return false;
70
+ }
71
+
72
+ if (this.terminal) {
73
+ this.terminal.yellow(`找到 ${sessions.length} 个历史 session 将被清理\n`);
74
+ this.terminal.green('确认清理? (y/N): ');
75
+ }
76
+
77
+ return new Promise((resolve) => {
78
+ if (!this.terminal) {
79
+ resolve(true);
80
+ return;
81
+ }
82
+
83
+ this.terminal.inputField({ cancelable: true }, (error, input) => {
84
+ if (error || !input) {
85
+ resolve(false);
86
+ return;
87
+ }
88
+ const trimmed = input.trim().toLowerCase();
89
+ resolve(trimmed === 'y' || trimmed === 'yes');
90
+ });
91
+ });
92
+ }
93
+
94
+ async deleteSessions(sessions) {
95
+ const result = { success: 0, failed: 0, failedSessions: [] };
96
+
97
+ if (!sessions || sessions.length === 0) {
98
+ return result;
99
+ }
100
+
101
+ const sandboxDir = this.historyManager._getSandboxDir();
102
+
103
+ for (const sessionId of sessions) {
104
+ const sessionDir = path.join(sandboxDir, sessionId);
105
+ try {
106
+ if (fs.existsSync(sessionDir)) {
107
+ fs.rmSync(sessionDir, { recursive: true, force: true });
108
+ result.success++;
109
+ }
110
+ } catch (error) {
111
+ logger.debug(`Failed to delete session ${sessionId}: ${error.message}`);
112
+ result.failed++;
113
+ result.failedSessions.push(sessionId);
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ async updateIndex(currentSessionId) {
121
+ const sandboxDir = this.historyManager._getSandboxDir();
122
+ const indexFile = path.join(sandboxDir, 'index.json');
123
+
124
+ await this.historyManager._withIndexLock(async () => {
125
+ let index = { sandbox_id: this.sandboxId, current_session: currentSessionId, sessions: [] };
126
+
127
+ if (fs.existsSync(indexFile)) {
128
+ try {
129
+ const content = fs.readFileSync(indexFile, 'utf8');
130
+ index = JSON.parse(content);
131
+ } catch (error) {
132
+ logger.debug(`Failed to read index.json: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ index.current_session = currentSessionId;
137
+ index.sessions = index.sessions.filter(session =>
138
+ session.id === currentSessionId || session.session_id === currentSessionId
139
+ );
140
+
141
+ fs.writeFileSync(indexFile, JSON.stringify(index, null, 2));
142
+ });
143
+ }
144
+
145
+ async execute() {
146
+ const sessions = await this.getSessionsToCleanup();
147
+
148
+ if (sessions.length === 0) {
149
+ if (this.terminal) {
150
+ this.terminal.yellow('无需清理的历史 session\n');
151
+ }
152
+ return;
153
+ }
154
+
155
+ const confirmed = await this.confirmCleanup(sessions);
156
+ if (!confirmed) {
157
+ return;
158
+ }
159
+
160
+ const result = await this.deleteSessions(sessions);
161
+
162
+ if (result.failed > 0) {
163
+ if (this.terminal) {
164
+ this.terminal.yellow(`清理完成:成功 ${result.success} 个,失败 ${result.failed} 个\n`);
165
+ if (result.failedSessions.length > 0) {
166
+ this.terminal.yellow(`失败的 session: ${result.failedSessions.join(', ')}\n`);
167
+ }
168
+ }
169
+ logger.warn(`Cleanup completed with failures: ${result.success} succeeded, ${result.failed} failed`);
170
+ } else {
171
+ if (this.terminal) {
172
+ this.terminal.green(`清理完成:成功 ${result.success} 个 session\n`);
173
+ }
174
+ }
175
+
176
+ // Only update index if cleaning current sandbox
177
+ if (!this.targetSandboxId) {
178
+ await this.updateIndex(this.historyManager.sessionId);
179
+ } else {
180
+ // For other sandboxes, update index to empty or keep only current
181
+ const index = await this._getIndex();
182
+ if (index.current_session) {
183
+ await this.updateIndex(index.current_session);
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ module.exports = CleanupHistoryHandler;
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../../utils/logger');
4
+
5
+ let syncCleanups = [];
6
+ let asyncCleanups = [];
7
+
8
+ let installed = false;
9
+ let installedSignalHandlers = [];
10
+
11
+ let cleanupStarted = false;
12
+ let cleanupPromise = null;
13
+
14
+ function registerSyncCleanup(fn) {
15
+ if (typeof fn !== 'function') {
16
+ throw new Error('registerSyncCleanup(fn) requires a function');
17
+ }
18
+ syncCleanups.push(fn);
19
+ }
20
+
21
+ function registerCleanup(fn) {
22
+ if (typeof fn !== 'function') {
23
+ throw new Error('registerCleanup(fn) requires a function');
24
+ }
25
+ asyncCleanups.push(fn);
26
+ }
27
+
28
+ async function drainStdin({ durationMs = 30 } = {}) {
29
+ const stdin = process.stdin;
30
+ if (!stdin || !stdin.isTTY) return;
31
+
32
+ try {
33
+ if (typeof stdin.setEncoding === 'function') {
34
+ stdin.setEncoding('utf8');
35
+ }
36
+ } catch {
37
+ // ignore
38
+ }
39
+
40
+ // Best-effort drain: resume stdin and briefly swallow any buffered/pasted input.
41
+ await new Promise((resolve) => {
42
+ let resolved = false;
43
+ const finish = () => {
44
+ if (resolved) return;
45
+ resolved = true;
46
+ if (typeof stdin.off === 'function') {
47
+ stdin.off('data', onData);
48
+ } else if (typeof stdin.removeListener === 'function') {
49
+ stdin.removeListener('data', onData);
50
+ }
51
+ resolve();
52
+ };
53
+
54
+ const onData = () => {};
55
+
56
+ try {
57
+ if (typeof stdin.on === 'function') {
58
+ stdin.on('data', onData);
59
+ }
60
+ if (typeof stdin.resume === 'function') {
61
+ stdin.resume();
62
+ }
63
+ } catch {
64
+ return finish();
65
+ }
66
+
67
+ setTimeout(finish, durationMs);
68
+ });
69
+ }
70
+
71
+ async function runExitCleanup({ reason = 'unknown', exitCode } = {}) {
72
+ if (cleanupStarted) {
73
+ return cleanupPromise;
74
+ }
75
+ cleanupStarted = true;
76
+
77
+ cleanupPromise = (async () => {
78
+ try {
79
+ await drainStdin();
80
+ } catch (e) {
81
+ logger.debug(`attach cleanup: drain stdin failed (${reason}): ${e && e.message ? e.message : String(e)}`);
82
+ }
83
+
84
+ for (const fn of syncCleanups) {
85
+ try {
86
+ fn();
87
+ } catch (e) {
88
+ logger.debug(`attach cleanup: sync cleanup failed (${reason}): ${e && e.message ? e.message : String(e)}`);
89
+ }
90
+ }
91
+
92
+ for (const fn of asyncCleanups) {
93
+ try {
94
+ // eslint-disable-next-line no-await-in-loop
95
+ await fn();
96
+ } catch (e) {
97
+ logger.debug(`attach cleanup: async cleanup failed (${reason}): ${e && e.message ? e.message : String(e)}`);
98
+ }
99
+ }
100
+
101
+ if (typeof exitCode === 'number') {
102
+ process.exitCode = exitCode;
103
+ }
104
+ })();
105
+
106
+ return cleanupPromise;
107
+ }
108
+
109
+ function installSignalHandlers() {
110
+ if (installed) return;
111
+ installed = true;
112
+
113
+ const register = (event, handler, opts = {}) => {
114
+ const { once = false } = opts;
115
+ if (once && typeof process.once === 'function') {
116
+ process.once(event, handler);
117
+ } else if (typeof process.on === 'function') {
118
+ process.on(event, handler);
119
+ }
120
+ installedSignalHandlers.push({ event, handler });
121
+ };
122
+
123
+ register('SIGINT', () => void runExitCleanup({ reason: 'SIGINT', exitCode: 0 }), { once: true });
124
+ register('SIGTERM', () => void runExitCleanup({ reason: 'SIGTERM', exitCode: 143 }), { once: true });
125
+
126
+ // Best-effort: 'exit' does not wait for async work, but running sync cleanups helps restore TTY.
127
+ register('exit', () => {
128
+ for (const fn of syncCleanups) {
129
+ try {
130
+ fn();
131
+ } catch {
132
+ // ignore
133
+ }
134
+ }
135
+ });
136
+ }
137
+
138
+ async function resetForTesting() {
139
+ for (const { event, handler } of installedSignalHandlers) {
140
+ if (typeof process.off === 'function') {
141
+ process.off(event, handler);
142
+ } else if (typeof process.removeListener === 'function') {
143
+ process.removeListener(event, handler);
144
+ }
145
+ }
146
+
147
+ syncCleanups = [];
148
+ asyncCleanups = [];
149
+ installed = false;
150
+ installedSignalHandlers = [];
151
+ cleanupStarted = false;
152
+ cleanupPromise = null;
153
+ }
154
+
155
+ module.exports = {
156
+ registerSyncCleanup,
157
+ registerCleanup,
158
+ drainStdin,
159
+ runExitCleanup,
160
+ installSignalHandlers,
161
+ resetForTesting,
162
+ };
163
+