rl-rockcli 0.0.9 → 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.
- package/commands/attach/basic-repl.js +212 -0
- package/commands/attach/cleanup-history.js +189 -0
- package/commands/attach/cleanup-manager.js +163 -0
- package/commands/attach/copy-ui/copyRepl.js +195 -0
- package/commands/attach/copy-ui/index.js +7 -0
- package/commands/attach/copy-ui/render/outputBlock.js +25 -0
- package/commands/attach/copy-ui/viewport/viewport.js +23 -0
- package/commands/attach/copy-ui/viewport/wheel.js +14 -0
- package/commands/attach/history-manager.js +507 -0
- package/commands/attach/history-session.js +48 -0
- package/commands/attach/ink-repl/InkREPL.js +1507 -0
- package/commands/attach/ink-repl/builtinCommands.js +1253 -0
- package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
- package/commands/attach/ink-repl/components/Console.js +191 -0
- package/commands/attach/ink-repl/components/DetailView.js +148 -0
- package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
- package/commands/attach/ink-repl/components/InputArea.js +125 -0
- package/commands/attach/ink-repl/components/InputLine.js +18 -0
- package/commands/attach/ink-repl/components/OutputArea.js +22 -0
- package/commands/attach/ink-repl/components/OutputItem.js +96 -0
- package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
- package/commands/attach/ink-repl/components/Spinner.js +79 -0
- package/commands/attach/ink-repl/components/StatusBar.js +106 -0
- package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
- package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
- package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
- package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
- package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
- package/commands/attach/ink-repl/hooks/useResources.js +132 -0
- package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
- package/commands/attach/ink-repl/index.js +112 -0
- package/commands/attach/ink-repl/package.json +3 -0
- package/commands/attach/ink-repl/replState.js +947 -0
- package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
- package/commands/attach/ink-repl/shortcuts/index.js +332 -0
- package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
- package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
- package/commands/attach/ink-repl/themes/index.js +4 -0
- package/commands/attach/ink-repl/themes/themeManager.js +45 -0
- package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
- package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
- package/commands/attach/ink-repl/utils/clipboard.js +50 -0
- package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
- package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
- package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
- package/commands/attach/ink-repl/utils/formatTime.js +12 -0
- package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
- package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
- package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
- package/commands/attach/ink-repl/utils/paramHint.js +60 -0
- package/commands/attach/ink-repl/utils/parseError.js +174 -0
- package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
- package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
- package/commands/attach/ink-repl/utils/replSelection.js +205 -0
- package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
- package/commands/attach/ink-repl/utils/textWrap.js +117 -0
- package/commands/attach/ink-repl/utils/truncate.js +115 -0
- package/commands/attach/opentui-repl/App.tsx +891 -0
- package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
- package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
- package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
- package/commands/attach/opentui-repl/components/Console.tsx +73 -0
- package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
- package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
- package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
- package/commands/attach/opentui-repl/components/Header.tsx +24 -0
- package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
- package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
- package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
- package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
- package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
- package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
- package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
- package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
- package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
- package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
- package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
- package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
- package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
- package/commands/attach/opentui-repl/index.js +99 -0
- package/commands/attach/opentui-repl/keybindings.ts +39 -0
- package/commands/attach/opentui-repl/package.json +3 -0
- package/commands/attach/opentui-repl/render.tsx +72 -0
- package/commands/attach/opentui-repl/tsconfig.json +12 -0
- package/commands/attach/repl.js +791 -0
- package/commands/attach/sandbox-id-resolver.js +56 -0
- package/commands/attach/session-manager.js +307 -0
- package/commands/attach/ui-mode.js +146 -0
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote path safety helpers (for sandbox-executed completion commands).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return true if the input contains characters that are risky for shell command construction.
|
|
7
|
+
* This is a conservative deny-list for completion-only paths.
|
|
8
|
+
* @param {string} input
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function hasDangerousShellChars(input) {
|
|
12
|
+
if (!input) return false;
|
|
13
|
+
// Reject control chars and common shell metacharacters.
|
|
14
|
+
return /[\r\n\t\0]/.test(input) || /[;&|`$()<>]/.test(input);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* POSIX shell-escape a string as a single-quoted argument.
|
|
19
|
+
* @param {string} value
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
export function shellEscapePosix(value) {
|
|
23
|
+
if (value === '') return "''";
|
|
24
|
+
// ' -> '\'' (close, escape, reopen)
|
|
25
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the first unescaped space in a string, treating backslash as an escape character.
|
|
30
|
+
* Returns -1 if none.
|
|
31
|
+
* @param {string} input
|
|
32
|
+
* @returns {number}
|
|
33
|
+
*/
|
|
34
|
+
export function findFirstUnescapedSpace(input) {
|
|
35
|
+
if (!input) return -1;
|
|
36
|
+
for (let i = 0; i < input.length; i++) {
|
|
37
|
+
const ch = input[i];
|
|
38
|
+
if (ch === '\\') {
|
|
39
|
+
i += 1; // skip escaped char (if any)
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === ' ') return i;
|
|
43
|
+
}
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Escape spaces for display/value insertion (keeps paths unquoted, but makes them a single arg).
|
|
49
|
+
* @param {string} input
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function escapeSpaces(input) {
|
|
53
|
+
if (!input) return input;
|
|
54
|
+
return String(input).replace(/ /g, '\\ ');
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPL output selection utilities
|
|
3
|
+
* Handles mouse selection within output items, excluding borders
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { truncateOutputToLines, formatTruncationHint } from './truncate.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate the screen position and dimensions of each output item
|
|
10
|
+
* @param {Array} outputs - Array of output items
|
|
11
|
+
* @param {number} topPadding - Top padding of the layout
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {number} [options.contentWidth] - Outer width of cards (defaults to stdout columns)
|
|
14
|
+
* @param {number} [options.maxLines] - Max visual lines shown in a card (defaults to 10)
|
|
15
|
+
* @returns {Array} Array of position info for each output
|
|
16
|
+
*/
|
|
17
|
+
export function calculateOutputPositions(outputs, topPadding = 2, options = {}) {
|
|
18
|
+
const { contentWidth: configuredWidth, maxLines = 50 } = options || {};
|
|
19
|
+
const positions = [];
|
|
20
|
+
let currentY = topPadding;
|
|
21
|
+
|
|
22
|
+
for (const output of outputs) {
|
|
23
|
+
const { id, command, output: text, isWelcome } = output;
|
|
24
|
+
|
|
25
|
+
if (isWelcome) {
|
|
26
|
+
// Welcome message: border + padding + single line + padding + border
|
|
27
|
+
// Total height: 3 (1 border top + 1 content + 1 border bottom)
|
|
28
|
+
const height = 3;
|
|
29
|
+
positions.push({
|
|
30
|
+
id,
|
|
31
|
+
startY: currentY,
|
|
32
|
+
endY: currentY + height - 1,
|
|
33
|
+
height,
|
|
34
|
+
type: 'welcome',
|
|
35
|
+
contentStartY: currentY + 1, // Skip top border
|
|
36
|
+
contentEndY: currentY + 1, // Single line
|
|
37
|
+
lines: [text],
|
|
38
|
+
});
|
|
39
|
+
currentY += height + 1; // Add gap between outputs
|
|
40
|
+
} else {
|
|
41
|
+
// Regular output: border + command line (with paddingBottom) + output lines + border
|
|
42
|
+
// IMPORTANT: the rendered OutputItem wraps by width and truncates by *visual* lines.
|
|
43
|
+
// For selection/positioning we must mirror that behavior, otherwise mouse coordinates drift.
|
|
44
|
+
// We approximate content width by terminal width (ShellLayout is widthFraction=1 by default).
|
|
45
|
+
const terminalWidth = process.stdout?.columns || 80;
|
|
46
|
+
const contentWidth = Math.max(20, configuredWidth || terminalWidth);
|
|
47
|
+
const boxInnerWidth = Math.max(10, contentWidth - 4);
|
|
48
|
+
|
|
49
|
+
const hasText = typeof text === 'string' ? text.length > 0 : !!text;
|
|
50
|
+
const { lines: visualLines, truncated, hiddenLines } = hasText
|
|
51
|
+
? truncateOutputToLines(String(text), boxInnerWidth, maxLines)
|
|
52
|
+
: { lines: [], truncated: false, hiddenLines: 0 };
|
|
53
|
+
|
|
54
|
+
const outputLines = visualLines.slice();
|
|
55
|
+
if (truncated && hiddenLines > 0) {
|
|
56
|
+
outputLines.push(formatTruncationHint(hiddenLines));
|
|
57
|
+
}
|
|
58
|
+
const lines = [];
|
|
59
|
+
|
|
60
|
+
if (command) {
|
|
61
|
+
lines.push(command); // Command line (we'll prepend prompt when extracting)
|
|
62
|
+
}
|
|
63
|
+
lines.push(...outputLines);
|
|
64
|
+
|
|
65
|
+
// Height calculation:
|
|
66
|
+
// - 1 top border
|
|
67
|
+
// - 1 command line (if exists, includes paddingBottom space)
|
|
68
|
+
// - outputLines.length output lines
|
|
69
|
+
// - 1 bottom border
|
|
70
|
+
const height = 2 + lines.length;
|
|
71
|
+
|
|
72
|
+
const contentStartY = currentY + 1; // Skip top border
|
|
73
|
+
const contentEndY = contentStartY + lines.length - 1;
|
|
74
|
+
|
|
75
|
+
positions.push({
|
|
76
|
+
id,
|
|
77
|
+
startY: currentY,
|
|
78
|
+
endY: currentY + height - 1,
|
|
79
|
+
height,
|
|
80
|
+
type: 'output',
|
|
81
|
+
contentStartY,
|
|
82
|
+
contentEndY,
|
|
83
|
+
lines,
|
|
84
|
+
command,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
currentY += height + 1; // Add gap (rowGap: 1) between outputs
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return positions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Find which output item contains a given Y coordinate
|
|
96
|
+
* @param {number} y - Y coordinate (1-based)
|
|
97
|
+
* @param {Array} positions - Output positions from calculateOutputPositions
|
|
98
|
+
* @returns {Object|null} Position info or null if not found
|
|
99
|
+
*/
|
|
100
|
+
export function findOutputAtPosition(y, positions) {
|
|
101
|
+
if (!positions || !Array.isArray(positions)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
for (const pos of positions) {
|
|
105
|
+
if (y >= pos.contentStartY && y <= pos.contentEndY) {
|
|
106
|
+
return pos;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if a coordinate is within the content area (excluding borders)
|
|
114
|
+
* @param {number} x - X coordinate (1-based)
|
|
115
|
+
* @param {number} y - Y coordinate (1-based)
|
|
116
|
+
* @param {number} contentWidth - Content area width
|
|
117
|
+
* @param {Array} positions - Output positions
|
|
118
|
+
* @returns {boolean} True if inside content area
|
|
119
|
+
*/
|
|
120
|
+
export function isInsideContentArea(x, y, contentWidth, positions) {
|
|
121
|
+
if (!positions || !Array.isArray(positions)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// X must be inside border (left border at 0, right border at contentWidth-1)
|
|
126
|
+
// With paddingLeft=1, valid X range is [2, contentWidth-2]
|
|
127
|
+
if (x < 2 || x >= contentWidth - 1) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Y must be in a content area
|
|
132
|
+
return findOutputAtPosition(y, positions) !== null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract selected text from output items
|
|
137
|
+
* @param {number} startX - Start X (1-based, screen coordinate)
|
|
138
|
+
* @param {number} startY - Start Y (1-based, screen coordinate)
|
|
139
|
+
* @param {number} endX - End X (1-based, screen coordinate)
|
|
140
|
+
* @param {number} endY - End Y (1-based, screen coordinate)
|
|
141
|
+
* @param {Array} positions - Output positions
|
|
142
|
+
* @param {string} shellPrompt - Shell prompt for command lines
|
|
143
|
+
* @returns {string} Selected text
|
|
144
|
+
*/
|
|
145
|
+
export function extractSelectionFromOutputs(startX, startY, endX, endY, positions, shellPrompt) {
|
|
146
|
+
if (!positions || !Array.isArray(positions)) {
|
|
147
|
+
return '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Normalize selection (ensure start <= end)
|
|
151
|
+
if (startY > endY || (startY === endY && startX > endX)) {
|
|
152
|
+
[startX, endX] = [endX, startX];
|
|
153
|
+
[startY, endY] = [endY, startY];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Find the output containing the selection
|
|
157
|
+
const outputPos = findOutputAtPosition(startY, positions);
|
|
158
|
+
if (!outputPos) {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Only support single-output selection
|
|
163
|
+
if (endY < outputPos.contentStartY || endY > outputPos.contentEndY) {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Calculate line indices within the output
|
|
168
|
+
const startLineIdx = startY - outputPos.contentStartY;
|
|
169
|
+
const endLineIdx = endY - outputPos.contentStartY;
|
|
170
|
+
|
|
171
|
+
// Adjust X coordinates to account for border (left border + paddingLeft = 2)
|
|
172
|
+
const adjustedStartX = Math.max(0, startX - 2);
|
|
173
|
+
const adjustedEndX = Math.max(0, endX - 2);
|
|
174
|
+
|
|
175
|
+
const selectedLines = [];
|
|
176
|
+
|
|
177
|
+
for (let lineIdx = startLineIdx; lineIdx <= endLineIdx; lineIdx++) {
|
|
178
|
+
if (lineIdx < 0 || lineIdx >= outputPos.lines.length) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let line = outputPos.lines[lineIdx];
|
|
183
|
+
|
|
184
|
+
// Prepend prompt for command line
|
|
185
|
+
if (lineIdx === 0 && outputPos.command && outputPos.type === 'output') {
|
|
186
|
+
line = shellPrompt + line;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (lineIdx === startLineIdx && lineIdx === endLineIdx) {
|
|
190
|
+
// Single line selection
|
|
191
|
+
selectedLines.push(line.substring(adjustedStartX, adjustedEndX));
|
|
192
|
+
} else if (lineIdx === startLineIdx) {
|
|
193
|
+
// First line
|
|
194
|
+
selectedLines.push(line.substring(adjustedStartX));
|
|
195
|
+
} else if (lineIdx === endLineIdx) {
|
|
196
|
+
// Last line
|
|
197
|
+
selectedLines.push(line.substring(0, adjustedEndX));
|
|
198
|
+
} else {
|
|
199
|
+
// Middle lines
|
|
200
|
+
selectedLines.push(line);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return selectedLines.join('\n').trimEnd();
|
|
205
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 响应格式化工具
|
|
3
|
+
* 统一处理 API 响应格式化逻辑
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getExitCodeTips } from './exitCodeTips.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 响应状态码常量
|
|
10
|
+
*/
|
|
11
|
+
export const RESPONSE_CODE = {
|
|
12
|
+
SUCCESS: '2xxx', // 2000-2999
|
|
13
|
+
SERVER_ERROR: '5xxx', // 5000-5999
|
|
14
|
+
CMD_ERROR: '6xxx', // 6000-6999
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 判断命令执行是否成功 (基于 exit_code)
|
|
19
|
+
* @param {Object} result - API result 对象
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isCommandSuccess(result) {
|
|
23
|
+
return result?.exit_code === 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断响应码类型
|
|
28
|
+
* @param {number} code - result.code
|
|
29
|
+
* @returns {string} 'success' | 'server_error' | 'cmd_error' | 'unknown'
|
|
30
|
+
*/
|
|
31
|
+
export function getCodeType(code) {
|
|
32
|
+
if (code === undefined || code === null) return 'success';
|
|
33
|
+
if (code >= 2000 && code < 3000) return 'success';
|
|
34
|
+
if (code >= 5000 && code < 6000) return 'server_error';
|
|
35
|
+
if (code >= 6000 && code < 7000) return 'cmd_error';
|
|
36
|
+
return 'unknown';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 提取错误信息
|
|
41
|
+
* @param {Object} response - API 响应 (从 SDK client 捕获的异常)
|
|
42
|
+
* @returns {{ error: string|null, failureReason: string|null, code: number|null }}
|
|
43
|
+
*/
|
|
44
|
+
export function extractError(response) {
|
|
45
|
+
const data = response?.data || {};
|
|
46
|
+
const result = data.result || {};
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
error: data.error || null,
|
|
50
|
+
failureReason: result.failure_reason || null,
|
|
51
|
+
code: result.code ?? null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 格式化命令执行输出
|
|
57
|
+
* @param {Object} result - execute/run_in_session result
|
|
58
|
+
* @param {Object} options
|
|
59
|
+
* @param {string} options.locale - 'zh' | 'en'
|
|
60
|
+
* @returns {{ output: string, metaInfo: string|null, tips: string|null, exitCode: number|null }}
|
|
61
|
+
*/
|
|
62
|
+
export function formatCommandOutput(result, options = {}) {
|
|
63
|
+
const { locale = 'zh' } = options;
|
|
64
|
+
const { exit_code, stdout, stderr, output } = result;
|
|
65
|
+
|
|
66
|
+
// 兼容两种字段名: output 或 stdout/stderr
|
|
67
|
+
const combinedOutput = output || [stderr, stdout].filter(Boolean).join('\n');
|
|
68
|
+
|
|
69
|
+
// 成功时:不显示 metaInfo 和 tips
|
|
70
|
+
if (exit_code === 0) {
|
|
71
|
+
return { output: combinedOutput, metaInfo: null, tips: null, exitCode: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 没有 exit_code 时(如超时/网络错误):显示 tips (等同于 exit_code: -1),但不显示 metaInfo
|
|
75
|
+
if (exit_code === undefined || exit_code === null) {
|
|
76
|
+
const tips = getExitCodeTips(-1, locale);
|
|
77
|
+
return { output: combinedOutput, metaInfo: null, tips, exitCode: null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 失败时(有 exit_code 且不为 0):显示 metaInfo 和 tips
|
|
81
|
+
const metaInfo = `[exit code: ${exit_code}]`;
|
|
82
|
+
const tips = getExitCodeTips(exit_code, locale);
|
|
83
|
+
|
|
84
|
+
return { output: combinedOutput, metaInfo, tips, exitCode: exit_code };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 格式化上传结果
|
|
89
|
+
* @param {Object} result - uploadFile 返回结果
|
|
90
|
+
* @param {Object} options
|
|
91
|
+
* @returns {{ output: string, exitCode: number }}
|
|
92
|
+
*/
|
|
93
|
+
export function formatUploadResult(result, options = {}) {
|
|
94
|
+
const { localPath, remotePath } = options;
|
|
95
|
+
|
|
96
|
+
if (result.success) {
|
|
97
|
+
return {
|
|
98
|
+
output: `✓ ${localPath} → ${remotePath}`,
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
output: `✗ 上传失败\n\nerror: ${result.message}`,
|
|
105
|
+
exitCode: 1,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 格式化接口错误 (5xxx 服务端错误)
|
|
111
|
+
* @param {Object} errorInfo - extractError 返回的错误信息
|
|
112
|
+
* @param {string} operation - 操作名称 (用于提示)
|
|
113
|
+
* @param {string} locale
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function formatServerError(errorInfo, operation, locale = 'zh') {
|
|
117
|
+
const tips = locale === 'en'
|
|
118
|
+
? '💡 Server error, please retry'
|
|
119
|
+
: '💡 服务端异常,建议重试';
|
|
120
|
+
|
|
121
|
+
let msg = `${operation} 失败\n\n`;
|
|
122
|
+
if (errorInfo.error) msg += `error: ${errorInfo.error}\n`;
|
|
123
|
+
if (errorInfo.failureReason) msg += `${errorInfo.failureReason}\n`;
|
|
124
|
+
msg += tips;
|
|
125
|
+
|
|
126
|
+
return msg;
|
|
127
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text wrapping utilities for Ink terminal rendering.
|
|
3
|
+
*
|
|
4
|
+
* Important: this implements a lightweight width calculation that treats a range
|
|
5
|
+
* of CJK code points as width=2. It is not a full wcwidth implementation, but
|
|
6
|
+
* is consistent with the existing OutputItem behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Slice a string by display width.
|
|
11
|
+
* @param {string} str
|
|
12
|
+
* @param {number} maxWidth
|
|
13
|
+
* @returns {{ head: string, tail: string }}
|
|
14
|
+
*/
|
|
15
|
+
export function sliceByWidth(str, maxWidth) {
|
|
16
|
+
if (maxWidth <= 0) return { head: '', tail: str };
|
|
17
|
+
let width = 0;
|
|
18
|
+
let idx = 0;
|
|
19
|
+
const chars = Array.from(str);
|
|
20
|
+
for (; idx < chars.length; idx++) {
|
|
21
|
+
const char = chars[idx];
|
|
22
|
+
const code = char.codePointAt(0);
|
|
23
|
+
const w =
|
|
24
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
25
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
26
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
27
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
28
|
+
(code >= 0xfe10 && code <= 0xfe1f) ||
|
|
29
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
30
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
31
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
32
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
33
|
+
(code >= 0x30000 && code <= 0x3fffd)
|
|
34
|
+
? 2
|
|
35
|
+
: 1;
|
|
36
|
+
if (width + w > maxWidth) break;
|
|
37
|
+
width += w;
|
|
38
|
+
}
|
|
39
|
+
return { head: chars.slice(0, idx).join(''), tail: chars.slice(idx).join('') };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a lightweight display width for a string (same width model as sliceByWidth).
|
|
44
|
+
* @param {string} str
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
export function getDisplayWidth(str) {
|
|
48
|
+
if (!str) return 0;
|
|
49
|
+
let width = 0;
|
|
50
|
+
for (const char of Array.from(String(str))) {
|
|
51
|
+
const code = char.codePointAt(0);
|
|
52
|
+
width +=
|
|
53
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
54
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
55
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
56
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
57
|
+
(code >= 0xfe10 && code <= 0xfe1f) ||
|
|
58
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
59
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
60
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
61
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
62
|
+
(code >= 0x30000 && code <= 0x3fffd)
|
|
63
|
+
? 2
|
|
64
|
+
: 1;
|
|
65
|
+
}
|
|
66
|
+
return width;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pad a line with spaces up to the given display width.
|
|
71
|
+
* This is useful in Ink to avoid leaving "stale" characters on screen when a
|
|
72
|
+
* new line is shorter than the previous frame.
|
|
73
|
+
* @param {string} line
|
|
74
|
+
* @param {number} width
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function padToWidth(line, width) {
|
|
78
|
+
if (width <= 0) return '';
|
|
79
|
+
const { head } = sliceByWidth(String(line ?? ''), width);
|
|
80
|
+
const current = getDisplayWidth(head);
|
|
81
|
+
if (current >= width) return head;
|
|
82
|
+
return head + ' '.repeat(width - current);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Wrap a string to lines by display width.
|
|
87
|
+
* @param {string} text
|
|
88
|
+
* @param {number} width
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
export function wrapTextToLines(text, width) {
|
|
92
|
+
if (!text && text !== '') return [''];
|
|
93
|
+
if (width <= 0) return String(text).split('\n');
|
|
94
|
+
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const rawLine of String(text).split('\n')) {
|
|
97
|
+
if (rawLine === '') {
|
|
98
|
+
out.push('');
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
let remaining = rawLine;
|
|
102
|
+
while (remaining.length > 0) {
|
|
103
|
+
const { head, tail } = sliceByWidth(remaining, width);
|
|
104
|
+
if (tail) {
|
|
105
|
+
const lastSpace = head.lastIndexOf(' ');
|
|
106
|
+
if (lastSpace > 0) {
|
|
107
|
+
out.push(head.slice(0, lastSpace));
|
|
108
|
+
remaining = head.slice(lastSpace + 1) + tail;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
out.push(head);
|
|
113
|
+
remaining = tail;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out.length === 0 ? [''] : out;
|
|
117
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { keybindingManager } from '../shortcuts/index.js';
|
|
2
|
+
import { wrapTextToLines } from './textWrap.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MAX_LINES = 50;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Process carriage returns (\r) in output to simulate terminal behavior.
|
|
8
|
+
* - \r\n is treated as a regular newline
|
|
9
|
+
* - \r alone moves cursor to start of line, subsequent text overwrites
|
|
10
|
+
*
|
|
11
|
+
* @param {string} output - Raw output with possible \r characters
|
|
12
|
+
* @returns {string} Processed output with \r behavior applied
|
|
13
|
+
*/
|
|
14
|
+
export function processCarriageReturns(output) {
|
|
15
|
+
if (!output || !output.includes('\r')) {
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// First normalize \r\n to \n
|
|
20
|
+
let normalized = output.replace(/\r\n/g, '\n');
|
|
21
|
+
|
|
22
|
+
// Process remaining \r characters
|
|
23
|
+
// Split by \n, then process each line for \r
|
|
24
|
+
const lines = normalized.split('\n');
|
|
25
|
+
const processedLines = lines.map(line => {
|
|
26
|
+
if (!line.includes('\r')) {
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Split by \r and keep only the last segment that "overwrites" previous content
|
|
31
|
+
// For more accurate simulation, we'd need to handle partial overwrites,
|
|
32
|
+
// but for progress bars, usually the last segment is what matters
|
|
33
|
+
const segments = line.split('\r');
|
|
34
|
+
// Filter out empty segments and take the last non-empty one
|
|
35
|
+
const nonEmptySegments = segments.filter(s => s.length > 0);
|
|
36
|
+
return nonEmptySegments.length > 0 ? nonEmptySegments[nonEmptySegments.length - 1] : '';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return processedLines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Truncate output to maximum number of lines
|
|
44
|
+
* @param {string} output - Output text
|
|
45
|
+
* @param {number} maxLines - Maximum lines to show
|
|
46
|
+
* @returns {{ text: string, truncated: boolean, totalLines: number, hiddenLines: number }}
|
|
47
|
+
*/
|
|
48
|
+
export function truncateOutput(output, maxLines = DEFAULT_MAX_LINES) {
|
|
49
|
+
const lines = output.split('\n');
|
|
50
|
+
|
|
51
|
+
if (lines.length <= maxLines) {
|
|
52
|
+
return {
|
|
53
|
+
text: output,
|
|
54
|
+
truncated: false,
|
|
55
|
+
totalLines: lines.length,
|
|
56
|
+
hiddenLines: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const visibleLines = lines.slice(0, maxLines);
|
|
61
|
+
const hiddenLines = lines.length - maxLines;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
text: visibleLines.join('\n'),
|
|
65
|
+
truncated: true,
|
|
66
|
+
totalLines: lines.length,
|
|
67
|
+
hiddenLines,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Truncate output by *visual* line count after wrapping to terminal width.
|
|
73
|
+
*
|
|
74
|
+
* This avoids a common layout issue where a single very long logical line
|
|
75
|
+
* (e.g. JSON log line) expands to many wrapped rows and breaks the page layout
|
|
76
|
+
* even when logical newline count is small.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} output
|
|
79
|
+
* @param {number} width Visual line width
|
|
80
|
+
* @param {number} maxLines Max visual lines to show
|
|
81
|
+
* @returns {{ lines: string[], truncated: boolean, totalLines: number, hiddenLines: number, text: string }}
|
|
82
|
+
*/
|
|
83
|
+
export function truncateOutputToLines(output, width, maxLines = DEFAULT_MAX_LINES) {
|
|
84
|
+
const allLines = wrapTextToLines(output ?? '', width);
|
|
85
|
+
if (allLines.length <= maxLines) {
|
|
86
|
+
return {
|
|
87
|
+
lines: allLines,
|
|
88
|
+
text: allLines.join('\n'),
|
|
89
|
+
truncated: false,
|
|
90
|
+
totalLines: allLines.length,
|
|
91
|
+
hiddenLines: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const visibleLines = allLines.slice(0, maxLines);
|
|
96
|
+
const hiddenLines = allLines.length - maxLines;
|
|
97
|
+
return {
|
|
98
|
+
lines: visibleLines,
|
|
99
|
+
text: visibleLines.join('\n'),
|
|
100
|
+
truncated: true,
|
|
101
|
+
totalLines: allLines.length,
|
|
102
|
+
hiddenLines,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format truncation hint text
|
|
108
|
+
* @param {number} hiddenLines - Number of hidden lines
|
|
109
|
+
* @returns {string} Hint text
|
|
110
|
+
*/
|
|
111
|
+
export function formatTruncationHint(hiddenLines) {
|
|
112
|
+
if (hiddenLines === 0) return '';
|
|
113
|
+
const keyLabel = keybindingManager.getActionKeyLabel('viewOutput');
|
|
114
|
+
return `... [${hiddenLines} more lines] press '${keyLabel}' to view`;
|
|
115
|
+
}
|