rl-rockcli 0.0.8 → 0.0.10

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 (162) 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/log/core/constants.js +237 -0
  90. package/commands/log/core/display.js +370 -0
  91. package/commands/log/core/search.js +330 -0
  92. package/commands/log/core/tail.js +216 -0
  93. package/commands/log/core/utils.js +424 -0
  94. package/commands/log.js +298 -0
  95. package/commands/sandbox/core/log-bridge.js +119 -0
  96. package/commands/sandbox/core/replay/analyzer.js +311 -0
  97. package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
  98. package/commands/sandbox/core/replay/batch-task.js +369 -0
  99. package/commands/sandbox/core/replay/concurrent-display.js +70 -0
  100. package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
  101. package/commands/sandbox/core/replay/data-source.js +86 -0
  102. package/commands/sandbox/core/replay/display.js +231 -0
  103. package/commands/sandbox/core/replay/executor.js +634 -0
  104. package/commands/sandbox/core/replay/history-fetcher.js +124 -0
  105. package/commands/sandbox/core/replay/index.js +338 -0
  106. package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
  107. package/commands/sandbox/core/replay/pid-mapping.js +26 -0
  108. package/commands/sandbox/core/replay/request.js +109 -0
  109. package/commands/sandbox/core/replay/worker.js +166 -0
  110. package/commands/sandbox/core/session.js +346 -0
  111. package/commands/sandbox/log-bridge.js +2 -0
  112. package/commands/sandbox/ray.js +2 -0
  113. package/commands/sandbox/replay/analyzer.js +311 -0
  114. package/commands/sandbox/replay/batch-orchestrator.js +536 -0
  115. package/commands/sandbox/replay/batch-task.js +369 -0
  116. package/commands/sandbox/replay/concurrent-display.js +70 -0
  117. package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
  118. package/commands/sandbox/replay/display.js +231 -0
  119. package/commands/sandbox/replay/executor.js +634 -0
  120. package/commands/sandbox/replay/history-fetcher.js +118 -0
  121. package/commands/sandbox/replay/index.js +338 -0
  122. package/commands/sandbox/replay/pid-mapping.js +26 -0
  123. package/commands/sandbox/replay/request.js +109 -0
  124. package/commands/sandbox/replay/worker.js +166 -0
  125. package/commands/sandbox/replay.js +2 -0
  126. package/commands/sandbox/session.js +2 -0
  127. package/commands/sandbox-original.js +1393 -0
  128. package/commands/sandbox.js +499 -0
  129. package/help/help.json +1071 -0
  130. package/help/middleware.js +71 -0
  131. package/help/renderer.js +800 -0
  132. package/index.js +5 -15
  133. package/lib/plugin-context.js +40 -0
  134. package/package.json +2 -2
  135. package/sdks/sandbox/core/client.js +845 -0
  136. package/sdks/sandbox/core/config.js +70 -0
  137. package/sdks/sandbox/core/types.js +74 -0
  138. package/sdks/sandbox/httpLogger.js +251 -0
  139. package/sdks/sandbox/index.js +9 -0
  140. package/utils/asciiArt.js +138 -0
  141. package/utils/bun-compat.js +59 -0
  142. package/utils/ciPipelines.js +138 -0
  143. package/utils/cli.js +17 -0
  144. package/utils/command-router.js +79 -0
  145. package/utils/configManager.js +503 -0
  146. package/utils/dependency-resolver.js +135 -0
  147. package/utils/eagleeye_traceid.js +151 -0
  148. package/utils/envDetector.js +78 -0
  149. package/utils/execution_logger.js +415 -0
  150. package/utils/featureManager.js +68 -0
  151. package/utils/firstTimeTip.js +44 -0
  152. package/utils/hook-manager.js +125 -0
  153. package/utils/http-logger.js +264 -0
  154. package/utils/i18n.js +139 -0
  155. package/utils/image-progress.js +159 -0
  156. package/utils/logger.js +154 -0
  157. package/utils/plugin-loader.js +124 -0
  158. package/utils/plugin-manager.js +348 -0
  159. package/utils/ray_cli_wrapper.js +746 -0
  160. package/utils/sandbox-client.js +419 -0
  161. package/utils/terminal.js +32 -0
  162. package/utils/tips.js +106 -0
@@ -0,0 +1,346 @@
1
+ /**
2
+ * @ Trigger File Completion Module
3
+ * Provides file path auto-completion triggered by @ symbol
4
+ *
5
+ * Supported commands:
6
+ * - /upload @<local-path> @<remote-path>
7
+ * First @ completes local files, second @ completes remote files
8
+ *
9
+ * - /download @<remote-path> @<local-path>
10
+ * First @ completes remote files, second @ completes local files
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import {
16
+ hasDangerousShellChars,
17
+ shellEscapePosix,
18
+ findFirstUnescapedSpace,
19
+ escapeSpaces,
20
+ } from './remotePathSafety.js';
21
+
22
+ /**
23
+ * Maximum number of completion items to show
24
+ */
25
+ const MAX_COMPLETIONS = 20;
26
+
27
+ /**
28
+ * Parse @ completion context from input buffer
29
+ * Determines if we're completing local (first @) or remote (second @) path
30
+ *
31
+ * @param {string} buffer - Current input buffer
32
+ * @param {number} cursorPos - Cursor position (defaults to end of buffer)
33
+ * @returns {Object|null} Context object or null if no @ completion needed
34
+ */
35
+ export function parseAtContext(buffer, cursorPos = buffer.length) {
36
+ if (!buffer) {
37
+ return null;
38
+ }
39
+
40
+ // Support both /upload and /download commands
41
+ const isUploadCmd = buffer.startsWith('/upload');
42
+ const isDownloadCmd = buffer.startsWith('/download');
43
+
44
+ if (!isUploadCmd && !isDownloadCmd) {
45
+ return null;
46
+ }
47
+
48
+ // Find all @ positions in the buffer
49
+ const atPositions = [];
50
+ for (let i = 0; i < buffer.length; i++) {
51
+ if (buffer[i] === '@') {
52
+ atPositions.push(i);
53
+ }
54
+ }
55
+
56
+ if (atPositions.length === 0) {
57
+ return null;
58
+ }
59
+
60
+ // Find which @ the cursor is in (cursor is after @, before next space or end)
61
+ let activeAtIndex = -1;
62
+ let activeAtPosition = -1;
63
+
64
+ for (let i = atPositions.length - 1; i >= 0; i--) {
65
+ const atPos = atPositions[i];
66
+ if (atPos < cursorPos) {
67
+ // Check if cursor is still within this @ argument (no space between @ and cursor)
68
+ const textAfterAt = buffer.substring(atPos + 1, cursorPos);
69
+ if (findFirstUnescapedSpace(textAfterAt) === -1) {
70
+ activeAtIndex = i;
71
+ activeAtPosition = atPos;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ if (activeAtIndex === -1) {
78
+ return null;
79
+ }
80
+
81
+ // Extract partial path after @ (up to cursor position)
82
+ const afterAt = buffer.substring(activeAtPosition + 1);
83
+ const spaceIdx = findFirstUnescapedSpace(afterAt);
84
+
85
+ // Calculate the end of the current @ argument
86
+ const argEndInAfterAt = spaceIdx === -1 ? afterAt.length : spaceIdx;
87
+ const argEndInBuffer = activeAtPosition + 1 + argEndInAfterAt;
88
+
89
+ // Partial path is from @ to cursor (or end of argument if cursor is past it)
90
+ const cursorRelativeToAt = cursorPos - (activeAtPosition + 1);
91
+ const partialPathEnd = Math.min(cursorRelativeToAt, argEndInAfterAt);
92
+ const partialPath = afterAt.substring(0, partialPathEnd);
93
+
94
+ // For /upload: First @ = local file, second @ = remote file
95
+ // For /download: First @ = remote file, second @ = local file
96
+ const isLocal = isUploadCmd ? (activeAtIndex === 0) : (activeAtIndex === 1);
97
+
98
+ // Calculate replace range (from @ to end of argument)
99
+ const replaceStart = activeAtPosition;
100
+ const replaceEnd = argEndInBuffer;
101
+
102
+ return {
103
+ atIndex: activeAtIndex,
104
+ atPosition: activeAtPosition,
105
+ isLocal,
106
+ partialPath,
107
+ replaceStart,
108
+ replaceEnd,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Fuzzy match filter - checks if query characters appear in order in target
114
+ * @param {string} target - String to search in
115
+ * @param {string} query - Query to match
116
+ * @returns {boolean}
117
+ */
118
+ export function fuzzyMatch(target, query) {
119
+ if (!query) return true;
120
+
121
+ const lowerTarget = target.toLowerCase();
122
+ const lowerQuery = query.toLowerCase();
123
+
124
+ let queryIdx = 0;
125
+ for (let i = 0; i < lowerTarget.length && queryIdx < lowerQuery.length; i++) {
126
+ if (lowerTarget[i] === lowerQuery[queryIdx]) {
127
+ queryIdx++;
128
+ }
129
+ }
130
+
131
+ return queryIdx === lowerQuery.length;
132
+ }
133
+
134
+ /**
135
+ * Parse directory and file prefix from partial path
136
+ * @param {string} partialPath - Path like "src/comp" or "test/"
137
+ * @returns {{ dirPath: string, filePrefix: string }}
138
+ */
139
+ export function parsePartialPath(partialPath) {
140
+ if (!partialPath) {
141
+ return { dirPath: '.', filePrefix: '' };
142
+ }
143
+
144
+ if (partialPath.endsWith('/')) {
145
+ return { dirPath: partialPath, filePrefix: '' };
146
+ }
147
+
148
+ const lastSlash = partialPath.lastIndexOf('/');
149
+ if (lastSlash === -1) {
150
+ return { dirPath: '.', filePrefix: partialPath };
151
+ }
152
+
153
+ return {
154
+ dirPath: partialPath.substring(0, lastSlash + 1) || '.',
155
+ filePrefix: partialPath.substring(lastSlash + 1),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Get local file completions
161
+ * @param {string} partialPath - Partial path entered after @
162
+ * @param {string} cwd - Current working directory
163
+ * @returns {Array<{label: string, value: string, description: string, isDir: boolean}>}
164
+ */
165
+ export function getLocalCompletions(partialPath, cwd = process.cwd()) {
166
+ const { dirPath, filePrefix } = parsePartialPath(partialPath);
167
+
168
+ // Resolve directory path
169
+ let resolvedDir;
170
+ if (path.isAbsolute(dirPath)) {
171
+ resolvedDir = dirPath;
172
+ } else if (dirPath === '.') {
173
+ resolvedDir = cwd;
174
+ } else {
175
+ resolvedDir = path.resolve(cwd, dirPath);
176
+ }
177
+
178
+ try {
179
+ const entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
180
+
181
+ const results = [];
182
+ for (const entry of entries) {
183
+ const name = entry.name;
184
+
185
+ // Skip hidden files unless user typed a dot
186
+ if (name.startsWith('.') && !filePrefix.startsWith('.')) {
187
+ continue;
188
+ }
189
+
190
+ // Apply fuzzy filter
191
+ if (!fuzzyMatch(name, filePrefix)) {
192
+ continue;
193
+ }
194
+
195
+ const isDir = entry.isDirectory();
196
+
197
+ // Skip exact match for files - path is already complete
198
+ if (!isDir && name === filePrefix) {
199
+ continue;
200
+ }
201
+
202
+ const displayName = isDir ? `${name}/` : name;
203
+
204
+ // Build the full value to replace
205
+ const valuePath = dirPath === '.'
206
+ ? (isDir ? `${name}/` : name)
207
+ : (isDir ? `${dirPath}${name}/` : `${dirPath}${name}`);
208
+
209
+ results.push({
210
+ label: displayName,
211
+ value: `@${escapeSpaces(valuePath)}`,
212
+ description: isDir ? 'directory' : 'file',
213
+ isDir,
214
+ });
215
+
216
+ if (results.length >= MAX_COMPLETIONS) {
217
+ break;
218
+ }
219
+ }
220
+
221
+ // Sort: directories first, then alphabetically
222
+ results.sort((a, b) => {
223
+ if (a.isDir !== b.isDir) {
224
+ return a.isDir ? -1 : 1;
225
+ }
226
+ return a.label.localeCompare(b.label);
227
+ });
228
+
229
+ return results;
230
+ } catch (err) {
231
+ // Directory doesn't exist or not readable
232
+ return [];
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Get remote file completions from sandbox
238
+ * @param {Object} sessionManager - Session manager for sandbox communication
239
+ * @param {string} partialPath - Partial path entered after @
240
+ * @returns {Promise<Array<{label: string, value: string, description: string, isDir: boolean}>>}
241
+ */
242
+ export async function getRemoteCompletions(sessionManager, partialPath, options = {}) {
243
+ const signal = options && options.signal ? options.signal : null;
244
+ if (!sessionManager) {
245
+ return [];
246
+ }
247
+
248
+ if (signal?.aborted) {
249
+ return [];
250
+ }
251
+
252
+ if (hasDangerousShellChars(partialPath)) {
253
+ return [];
254
+ }
255
+
256
+ const { dirPath, filePrefix } = parsePartialPath(partialPath);
257
+
258
+ // Use absolute path for remote, default to /
259
+ const remoteDirPath = dirPath === '.' ? '/' : dirPath;
260
+
261
+ try {
262
+ // Use ls -1Ap to get files with / suffix for directories
263
+ const result = await sessionManager.execute(`ls -1Ap -- ${shellEscapePosix(remoteDirPath)} 2>/dev/null`);
264
+
265
+ if (signal?.aborted) {
266
+ return [];
267
+ }
268
+
269
+ if (!result.output) {
270
+ return [];
271
+ }
272
+
273
+ const entries = result.output
274
+ .split(/[\r\n]+/)
275
+ .filter(f => f.trim())
276
+ .slice(0, 100)
277
+ .map(name => ({
278
+ name: name.endsWith('/') ? name.slice(0, -1) : name,
279
+ isDir: name.endsWith('/'),
280
+ }));
281
+
282
+ const results = [];
283
+ for (const entry of entries) {
284
+ const name = entry.name;
285
+
286
+ // Skip hidden files unless user typed a dot
287
+ if (name.startsWith('.') && !filePrefix.startsWith('.')) {
288
+ continue;
289
+ }
290
+
291
+ // Apply fuzzy filter
292
+ if (!fuzzyMatch(name, filePrefix)) {
293
+ continue;
294
+ }
295
+
296
+ // Skip exact match for files - path is already complete
297
+ if (!entry.isDir && name === filePrefix) {
298
+ continue;
299
+ }
300
+
301
+ const displayName = entry.isDir ? `${name}/` : name;
302
+
303
+ // Build the full value to replace
304
+ const valuePath = dirPath === '.'
305
+ ? (entry.isDir ? `/${name}/` : `/${name}`)
306
+ : (entry.isDir ? `${remoteDirPath}${name}/` : `${remoteDirPath}${name}`);
307
+
308
+ results.push({
309
+ label: displayName,
310
+ value: `@${escapeSpaces(valuePath)}`,
311
+ description: entry.isDir ? 'remote directory' : 'remote file',
312
+ isDir: entry.isDir,
313
+ });
314
+
315
+ if (results.length >= MAX_COMPLETIONS) {
316
+ break;
317
+ }
318
+ }
319
+
320
+ // Sort: directories first, then alphabetically
321
+ results.sort((a, b) => {
322
+ if (a.isDir !== b.isDir) {
323
+ return a.isDir ? -1 : 1;
324
+ }
325
+ return a.label.localeCompare(b.label);
326
+ });
327
+
328
+ return results;
329
+ } catch (err) {
330
+ return [];
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Replace the @ path in buffer with selected value
336
+ * @param {string} buffer - Current buffer
337
+ * @param {Object} atContext - Context from parseAtContext
338
+ * @param {string} selectedValue - Selected completion value (including @)
339
+ * @returns {string} New buffer with replaced path
340
+ */
341
+ export function replaceAtPath(buffer, atContext, selectedValue) {
342
+ const before = buffer.substring(0, atContext.replaceStart);
343
+ const after = buffer.substring(atContext.replaceEnd);
344
+
345
+ return before + selectedValue + after;
346
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Clipboard utilities for terminal
3
+ * Uses native commands (pbcopy on macOS, xclip on Linux)
4
+ */
5
+
6
+ import { spawn } from 'child_process';
7
+ import os from 'os';
8
+
9
+ /**
10
+ * Copy text to system clipboard
11
+ * @param {string} text Text to copy
12
+ * @returns {Promise<boolean>} Success status
13
+ */
14
+ export async function copyToClipboard(text) {
15
+ const platform = os.platform();
16
+
17
+ let command;
18
+ let args = [];
19
+
20
+ if (platform === 'darwin') {
21
+ command = 'pbcopy';
22
+ } else if (platform === 'linux') {
23
+ // Try xclip first, fallback to xsel
24
+ command = 'xclip';
25
+ args = ['-selection', 'clipboard'];
26
+ } else if (platform === 'win32') {
27
+ command = 'clip';
28
+ } else {
29
+ return false;
30
+ }
31
+
32
+ return new Promise((resolve) => {
33
+ try {
34
+ const proc = spawn(command, args, { stdio: ['pipe', 'ignore', 'ignore'] });
35
+
36
+ proc.stdin.write(text);
37
+ proc.stdin.end();
38
+
39
+ proc.on('close', (code) => {
40
+ resolve(code === 0);
41
+ });
42
+
43
+ proc.on('error', () => {
44
+ resolve(false);
45
+ });
46
+ } catch {
47
+ resolve(false);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Console Logger - Global logger for InkREPL console panel
3
+ *
4
+ * Usage:
5
+ * import { consoleLogger } from './utils/consoleLogger.js';
6
+ * consoleLogger.info('Message');
7
+ * consoleLogger.warn('Warning');
8
+ * consoleLogger.error('Error');
9
+ * consoleLogger.debug('Debug info');
10
+ */
11
+
12
+ // Store the setState function reference
13
+ let _setStateRef = null;
14
+
15
+ /**
16
+ * Initialize the console logger with setState function
17
+ * Called from InkREPL component
18
+ */
19
+ export function initConsoleLogger(setState) {
20
+ _setStateRef = setState;
21
+ }
22
+
23
+ /**
24
+ * Clear the console logger reference
25
+ * Called when InkREPL unmounts
26
+ */
27
+ export function clearConsoleLogger() {
28
+ _setStateRef = null;
29
+ }
30
+
31
+ // Console height (visible lines) - should match Console component
32
+ const CONSOLE_HEIGHT = 7;
33
+
34
+ /**
35
+ * Add a log entry to the console
36
+ * Auto-scrolls to bottom to show latest logs
37
+ * @param {string} level - Log level
38
+ * @param {string} message - Display message (may be truncated)
39
+ * @param {string} fullMessage - Full message for copying (optional)
40
+ */
41
+ function addLog(level, message, fullMessage = null) {
42
+ if (!_setStateRef) {
43
+ // Fallback to real console if not initialized
44
+ const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
45
+ console[method](`[ConsoleLogger] ${message}`);
46
+ return;
47
+ }
48
+
49
+ _setStateRef(state => {
50
+ const newLog = {
51
+ id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
52
+ level,
53
+ message: String(message),
54
+ fullMessage: fullMessage ? String(fullMessage) : String(message), // For copying
55
+ timestamp: new Date(),
56
+ };
57
+ const newLogs = [...state.consoleLogs, newLog];
58
+ // Auto-scroll to bottom: offset = total logs - visible lines
59
+ const newScrollOffset = Math.max(0, newLogs.length - CONSOLE_HEIGHT);
60
+
61
+ return {
62
+ ...state,
63
+ consoleLogs: newLogs,
64
+ consoleScrollOffset: newScrollOffset,
65
+ };
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Console Logger API
71
+ * Each method accepts optional fullMessage for copying
72
+ */
73
+ export const consoleLogger = {
74
+ info: (message, fullMessage) => addLog('info', message, fullMessage),
75
+ warn: (message, fullMessage) => addLog('warn', message, fullMessage),
76
+ error: (message, fullMessage) => addLog('error', message, fullMessage),
77
+ debug: (message, fullMessage) => addLog('debug', message, fullMessage),
78
+ log: (message, fullMessage) => addLog('info', message, fullMessage),
79
+ };
80
+
81
+ export default consoleLogger;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Handle abnormal exit codes with helpful messages.
3
+ *
4
+ * @param {number} exitCode - The exit code from command execution
5
+ * @param {string} output - The original output (can be empty)
6
+ * @returns {{ output: string, hint: string|null }} - Processed output and optional hint
7
+ */
8
+ export function handleAbnormalExitCode(exitCode, output = '') {
9
+ let hint = null;
10
+
11
+ if (exitCode === -1) {
12
+ // -1 typically indicates process was killed or internal error
13
+ hint = 'Command terminated abnormally (exit code -1). This may indicate a timeout, signal termination, or internal error.';
14
+ if (!output) {
15
+ return { output: hint, hint };
16
+ }
17
+ return { output, hint };
18
+ }
19
+
20
+ if (exitCode === 137) {
21
+ // 128 + 9 (SIGKILL) - often OOM killed
22
+ hint = '[Process killed (exit code 137). Possibly OOM or manually terminated.]';
23
+ return {
24
+ output: output ? `${output}\n${hint}` : hint,
25
+ hint
26
+ };
27
+ }
28
+
29
+ if (exitCode === 143) {
30
+ // 128 + 15 (SIGTERM) - graceful termination
31
+ hint = '[Process terminated (exit code 143). Received SIGTERM.]';
32
+ return {
33
+ output: output ? `${output}\n${hint}` : hint,
34
+ hint
35
+ };
36
+ }
37
+
38
+ return { output, hint: null };
39
+ }
40
+
41
+ /**
42
+ * Check if exit code indicates an abnormal termination.
43
+ *
44
+ * @param {number} exitCode - The exit code
45
+ * @returns {boolean} - True if abnormal
46
+ */
47
+ export function isAbnormalExitCode(exitCode) {
48
+ return exitCode === -1 || exitCode === 137 || exitCode === 143;
49
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 退出码提示工具
3
+ * 根据退出码提供用户友好的提示信息
4
+ */
5
+
6
+ export const EXIT_CODE_TIPS = {
7
+ zh: {
8
+ 0: null, // 成功无提示
9
+ '-1': '命令执行异常(超时/网络/会话失效)',
10
+ 1: '命令执行失败,请检查参数或路径是否正确',
11
+ 2: '命令用法错误,使用 --help 查看帮助',
12
+ 126: '没有执行权限,尝试 chmod +x 或检查权限',
13
+ 127: '命令不存在,请检查拼写或是否已安装',
14
+ 128: '无效的参数,请检查命令语法',
15
+ 130: '命令被用户中断',
16
+ 137: '命令被强制终止(可能是 OOM)',
17
+ 139: '程序发生段错误(Segmentation Fault)',
18
+ 143: '命令被终止信号终止',
19
+ },
20
+ en: {
21
+ 0: null,
22
+ '-1': 'Command execution failed (timeout/network/session expired)',
23
+ 1: 'Command failed. Please check arguments or path',
24
+ 2: 'Command usage error. Use --help for help',
25
+ 126: 'No execute permission. Try chmod +x or check permissions',
26
+ 127: 'Command not found. Check spelling or if installed',
27
+ 128: 'Invalid argument. Check command syntax',
28
+ 130: 'Command interrupted by user',
29
+ 137: 'Command killed (possibly OOM)',
30
+ 139: 'Segmentation fault',
31
+ 143: 'Command terminated by signal',
32
+ },
33
+ };
34
+
35
+ /**
36
+ * 获取退出码对应的提示
37
+ * @param {number} exitCode - 退出码
38
+ * @param {string} locale - 'zh' | 'en'
39
+ * @returns {string|null}
40
+ */
41
+ export function getExitCodeTips(exitCode, locale = 'zh') {
42
+ const tips = EXIT_CODE_TIPS[locale] || EXIT_CODE_TIPS.zh;
43
+
44
+ // 将 exitCode 转换为字符串进行查找(因为对象的键是字符串)
45
+ const exitCodeStr = String(exitCode);
46
+
47
+ if (tips[exitCodeStr] !== undefined) {
48
+ return tips[exitCodeStr];
49
+ }
50
+
51
+ // 未知退出码
52
+ if (locale === 'en') {
53
+ return `Command exited with code: ${exitCode}`;
54
+ }
55
+ return `命令返回非零状态码: ${exitCode}`;
56
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Format timestamp to HH:MM:SS
3
+ * @param {number} timestamp - Unix timestamp in milliseconds
4
+ * @returns {string} Formatted time string
5
+ */
6
+ export function formatTimestamp(timestamp) {
7
+ const date = new Date(timestamp);
8
+ const hours = String(date.getHours()).padStart(2, '0');
9
+ const minutes = String(date.getMinutes()).padStart(2, '0');
10
+ const seconds = String(date.getSeconds()).padStart(2, '0');
11
+ return `${hours}:${minutes}:${seconds}`;
12
+ }