closer-code 1.0.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.
- package/.env.example +83 -0
- package/API_GUIDE.md +1411 -0
- package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
- package/CLAUDE.md +55 -0
- package/CTRL_C_EXPERIMENT.md +90 -0
- package/PROJECT_CLEANUP_SUMMARY.md +121 -0
- package/README.md +686 -0
- package/cloco.md +51 -0
- package/config.example.json +116 -0
- package/dist/bash-runner.js +128 -0
- package/dist/batch-cli.js +20736 -0
- package/dist/closer-cli.js +21190 -0
- package/dist/index.js +31228 -0
- package/docs/EXPORT_COMMAND.md +152 -0
- package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
- package/docs/GLOBAL_CONFIG.md +128 -0
- package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
- package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
- package/docs/QUICK_START_HISTORY.md +207 -0
- package/docs/TASK_PROGRESS_FEATURE.md +190 -0
- package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
- package/docs/THINKING_FEATURE.md +187 -0
- package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
- package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
- package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
- package/docs/WHY_THINKING_SHORT.md +201 -0
- package/package.json +49 -0
- package/scenarios/README.md +234 -0
- package/scenarios/run-all-scenarios.js +342 -0
- package/scenarios/scenario1-batch-converter.js +247 -0
- package/scenarios/scenario2-code-analyzer.js +375 -0
- package/scenarios/scenario3-doc-generator.js +371 -0
- package/scenarios/scenario4-log-analyzer.js +496 -0
- package/scenarios/scenario5-tdd-helper.js +681 -0
- package/src/ai-client-legacy.js +171 -0
- package/src/ai-client.js +221 -0
- package/src/bash-runner.js +148 -0
- package/src/batch-cli.js +327 -0
- package/src/cli.jsx +166 -0
- package/src/closer-cli.jsx +1103 -0
- package/src/closer-cli.jsx.backup +948 -0
- package/src/commands/batch.js +62 -0
- package/src/commands/chat.js +10 -0
- package/src/commands/config.js +154 -0
- package/src/commands/help.js +76 -0
- package/src/commands/history.js +192 -0
- package/src/commands/setup.js +17 -0
- package/src/commands/upgrade.js +101 -0
- package/src/commands/workflow-tests.js +125 -0
- package/src/config.js +343 -0
- package/src/conversation.js +962 -0
- package/src/git-helper.js +349 -0
- package/src/index.js +88 -0
- package/src/logger.js +347 -0
- package/src/plan.js +193 -0
- package/src/planner.js +397 -0
- package/src/search.js +195 -0
- package/src/setup.js +147 -0
- package/src/shortcuts.js +269 -0
- package/src/snippets.js +430 -0
- package/src/test-modules.js +118 -0
- package/src/tools.js +398 -0
- package/src/utils/cli.js +124 -0
- package/src/utils/validator.js +184 -0
- package/src/utils/version.js +33 -0
- package/src/utils/workflow-test.js +271 -0
- package/src/utils/workflow.js +268 -0
- package/test/demo-file-naming.js +92 -0
- package/test/demo-thinking.js +124 -0
- package/test/final-verification-report.md +303 -0
- package/test/research-thinking.js +130 -0
- package/test/test-auto-mkdir.js +123 -0
- package/test/test-e2e-empty-dir.md +108 -0
- package/test/test-export-logic.js +119 -0
- package/test/test-global-cloco.js +126 -0
- package/test/test-history-isolation.js +291 -0
- package/test/test-improved-thinking.js +43 -0
- package/test/test-long-message.js +65 -0
- package/test/test-plan-functionality.js +95 -0
- package/test/test-real-scenario.js +216 -0
- package/test/test-thinking-display.js +65 -0
- package/test/ui-verification-test.js +203 -0
- package/test/verify-history-isolation.sh +71 -0
- package/test/verify-thinking.js +339 -0
- package/test/workflows/empty-dir-creation.md +51 -0
- package/test/workflows/inventor/ascii-teacup.js +199 -0
- package/test/workflows/inventor/ascii-teacup.mjs +199 -0
- package/test/workflows/inventor/ascii_apple.hs +84 -0
- package/test/workflows/inventor/ascii_apple.py +91 -0
- package/test/workflows/inventor/cloco.md +3 -0
- package/test/workflows/longtalk/cloco.md +19 -0
- package/test/workflows/longtalk/emoji_500.txt +63 -0
- package/test/workflows/longtalk/emoji_list.txt +20 -0
- package/test/workflows/programmer/adder.md +33 -0
- package/test/workflows/programmer/expect.md +2 -0
- package/test/workflows/programmer/prompt.md +3 -0
- package/test/workflows/test-empty-dir-creation.js +113 -0
- package/test-ctrl-c.jsx +126 -0
- package/test-manual-file-creation.js +151 -0
- package/winfix.md +3 -0
package/src/batch-cli.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Closer Code - Batch Mode (非交互模式)
|
|
4
|
+
*
|
|
5
|
+
* 用于批处理场景,直接输出结果而不启动交互式 UI
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* closer-batch "你的问题"
|
|
9
|
+
* echo "你的问题" | closer-batch
|
|
10
|
+
* closer-batch --file prompt.txt
|
|
11
|
+
* closer-batch --json "你的问题"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createConversation } from './conversation.js';
|
|
15
|
+
import { getConfig } from './config.js';
|
|
16
|
+
import {
|
|
17
|
+
initLogger,
|
|
18
|
+
closeLogger,
|
|
19
|
+
logSessionSummary
|
|
20
|
+
} from './logger.js';
|
|
21
|
+
import fs from 'fs/promises';
|
|
22
|
+
import readline from 'readline';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析命令行参数
|
|
26
|
+
*/
|
|
27
|
+
function parseArgs() {
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const options = {
|
|
30
|
+
prompt: null,
|
|
31
|
+
file: null,
|
|
32
|
+
output: 'text', // text, json, verbose
|
|
33
|
+
debug: false,
|
|
34
|
+
help: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
|
|
40
|
+
if (arg === '--help' || arg === '-h') {
|
|
41
|
+
options.help = true;
|
|
42
|
+
} else if (arg === '--file' || arg === '-f') {
|
|
43
|
+
options.file = args[++i];
|
|
44
|
+
} else if (arg === '--json' || arg === '-j') {
|
|
45
|
+
options.output = 'json';
|
|
46
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
47
|
+
options.output = 'verbose';
|
|
48
|
+
} else if (arg === '--debug' || arg === '-d') {
|
|
49
|
+
options.debug = true;
|
|
50
|
+
} else if (!arg.startsWith('-')) {
|
|
51
|
+
options.prompt = arg;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 显示帮助信息
|
|
60
|
+
*/
|
|
61
|
+
function showHelp() {
|
|
62
|
+
console.log(`
|
|
63
|
+
Closer Code - Batch Mode (非交互模式)
|
|
64
|
+
|
|
65
|
+
使用方式:
|
|
66
|
+
closer-batch [选项] "你的问题"
|
|
67
|
+
echo "你的问题" | closer-batch
|
|
68
|
+
closer-batch --file prompt.txt
|
|
69
|
+
|
|
70
|
+
选项:
|
|
71
|
+
-f, --file <文件> 从文件读取提示词
|
|
72
|
+
-j, --json 以 JSON 格式输出
|
|
73
|
+
-v, --verbose 详细输出(包含工具调用)
|
|
74
|
+
-d, --debug 启用调试日志
|
|
75
|
+
-h, --help 显示帮助信息
|
|
76
|
+
|
|
77
|
+
环境变量:
|
|
78
|
+
CLOSER_DEBUG_LOG=1 启用调试日志
|
|
79
|
+
CLOSER_AI_PROVIDER AI 提供商 (anthropic, openai, ollama)
|
|
80
|
+
CLOSER_ANTHROPIC_API_KEY Anthropic API Key
|
|
81
|
+
|
|
82
|
+
输出格式:
|
|
83
|
+
text 纯文本输出(默认)
|
|
84
|
+
json JSON 格式,包含完整响应信息
|
|
85
|
+
verbose 详细输出,包含工具调用详情
|
|
86
|
+
|
|
87
|
+
退出码:
|
|
88
|
+
0 成功
|
|
89
|
+
1 错误
|
|
90
|
+
2 参数错误
|
|
91
|
+
|
|
92
|
+
示例:
|
|
93
|
+
closer-batch "列出当前目录的文件"
|
|
94
|
+
closer-batch --json "分析 package.json"
|
|
95
|
+
cat prompt.txt | closer-batch
|
|
96
|
+
closer-batch --file prompt.txt --verbose
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 从标准输入读取提示词
|
|
102
|
+
*/
|
|
103
|
+
async function readStdin() {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
resolve('');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rl = readline.createInterface({
|
|
111
|
+
input: process.stdin,
|
|
112
|
+
terminal: false
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
let content = '';
|
|
116
|
+
rl.on('line', (line) => {
|
|
117
|
+
content += line + '\n';
|
|
118
|
+
});
|
|
119
|
+
rl.on('close', () => {
|
|
120
|
+
resolve(content.trim());
|
|
121
|
+
});
|
|
122
|
+
rl.on('error', reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 格式化输出
|
|
128
|
+
*/
|
|
129
|
+
class OutputFormatter {
|
|
130
|
+
constructor(mode) {
|
|
131
|
+
this.mode = mode;
|
|
132
|
+
this.toolCalls = [];
|
|
133
|
+
this.startTime = Date.now();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 输出进度信息
|
|
138
|
+
*/
|
|
139
|
+
progress(message) {
|
|
140
|
+
if (this.mode === 'verbose') {
|
|
141
|
+
console.error(`[INFO] ${message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 记录工具调用
|
|
147
|
+
*/
|
|
148
|
+
recordToolCall(toolName, input, result) {
|
|
149
|
+
this.toolCalls.push({
|
|
150
|
+
tool: toolName,
|
|
151
|
+
input,
|
|
152
|
+
success: result.success,
|
|
153
|
+
result: result.success ? result.data : result.error,
|
|
154
|
+
timestamp: new Date().toISOString()
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (this.mode === 'verbose') {
|
|
158
|
+
console.error(`[TOOL] ${toolName}`);
|
|
159
|
+
if (result.success) {
|
|
160
|
+
console.error(` ✓ Success`);
|
|
161
|
+
} else {
|
|
162
|
+
console.error(` ✗ Failed: ${result.error}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 格式化最终输出
|
|
169
|
+
*/
|
|
170
|
+
format(content) {
|
|
171
|
+
const duration = Date.now() - this.startTime;
|
|
172
|
+
|
|
173
|
+
if (this.mode === 'json') {
|
|
174
|
+
return JSON.stringify({
|
|
175
|
+
success: true,
|
|
176
|
+
content,
|
|
177
|
+
toolCalls: this.toolCalls,
|
|
178
|
+
metadata: {
|
|
179
|
+
duration,
|
|
180
|
+
toolCount: this.toolCalls.length,
|
|
181
|
+
timestamp: new Date().toISOString()
|
|
182
|
+
}
|
|
183
|
+
}, null, 2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (this.mode === 'verbose') {
|
|
187
|
+
return `
|
|
188
|
+
${content}
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
工具调用: ${this.toolCalls.length} 次
|
|
192
|
+
耗时: ${duration}ms
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// text 模式:只输出内容
|
|
197
|
+
return content;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 格式化错误
|
|
202
|
+
*/
|
|
203
|
+
error(message) {
|
|
204
|
+
if (this.mode === 'json') {
|
|
205
|
+
return JSON.stringify({
|
|
206
|
+
success: false,
|
|
207
|
+
error: message,
|
|
208
|
+
timestamp: new Date().toISOString()
|
|
209
|
+
}, null, 2);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `Error: ${message}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 批处理模式主函数
|
|
218
|
+
*/
|
|
219
|
+
async function runBatch() {
|
|
220
|
+
const options = parseArgs();
|
|
221
|
+
|
|
222
|
+
// 显示帮助
|
|
223
|
+
if (options.help) {
|
|
224
|
+
showHelp();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 启用调试日志
|
|
229
|
+
if (options.debug) {
|
|
230
|
+
process.env.CLOSER_DEBUG_LOG = '1';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 初始化输出格式化器
|
|
234
|
+
const formatter = new OutputFormatter(options.output);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// 获取提示词
|
|
238
|
+
let prompt = options.prompt;
|
|
239
|
+
|
|
240
|
+
if (options.file) {
|
|
241
|
+
formatter.progress(`从文件读取: ${options.file}`);
|
|
242
|
+
prompt = await fs.readFile(options.file, 'utf-8');
|
|
243
|
+
prompt = prompt.trim();
|
|
244
|
+
} else if (!prompt) {
|
|
245
|
+
formatter.progress('从标准输入读取...');
|
|
246
|
+
prompt = await readStdin();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!prompt) {
|
|
250
|
+
console.error(formatter.error('未提供提示词。使用 --help 查看使用说明。'));
|
|
251
|
+
process.exit(2);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
formatter.progress('初始化配置...');
|
|
255
|
+
const config = getConfig();
|
|
256
|
+
|
|
257
|
+
formatter.progress('创建对话会话...');
|
|
258
|
+
const conversation = await createConversation(config);
|
|
259
|
+
|
|
260
|
+
formatter.progress('发送消息到 AI...');
|
|
261
|
+
const response = await conversation.sendMessage(
|
|
262
|
+
prompt,
|
|
263
|
+
(progress) => {
|
|
264
|
+
if (progress.type === 'tool_start') {
|
|
265
|
+
formatter.progress(`执行工具: ${progress.tool}`);
|
|
266
|
+
} else if (progress.type === 'tool_complete') {
|
|
267
|
+
formatter.recordToolCall(
|
|
268
|
+
progress.tool,
|
|
269
|
+
null, // input 已经在 tool_start 中记录
|
|
270
|
+
progress.result
|
|
271
|
+
);
|
|
272
|
+
} else if (progress.type === 'token') {
|
|
273
|
+
// 流式输出 token(仅 text 模式)
|
|
274
|
+
if (options.output === 'text') {
|
|
275
|
+
process.stdout.write(progress.content);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// 输出最终结果(非 text 模式或没有流式输出时)
|
|
282
|
+
if (options.output !== 'text') {
|
|
283
|
+
console.log(formatter.format(response.content));
|
|
284
|
+
} else if (!response.content) {
|
|
285
|
+
// 如果 content 为空,至少输出一个换行
|
|
286
|
+
console.log();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
formatter.progress('完成!');
|
|
290
|
+
|
|
291
|
+
// 记录会话摘要到日志
|
|
292
|
+
await logSessionSummary(conversation);
|
|
293
|
+
|
|
294
|
+
// 关闭日志
|
|
295
|
+
await closeLogger();
|
|
296
|
+
|
|
297
|
+
process.exit(0);
|
|
298
|
+
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(formatter.error(error.message));
|
|
301
|
+
console.error('');
|
|
302
|
+
console.error('详细错误信息:');
|
|
303
|
+
console.error(error);
|
|
304
|
+
|
|
305
|
+
// 关闭日志
|
|
306
|
+
await closeLogger();
|
|
307
|
+
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 导出运行函数,供 cloco 命令使用
|
|
313
|
+
export { runBatch };
|
|
314
|
+
|
|
315
|
+
// 只在直接运行时执行
|
|
316
|
+
// 检查当前模块是否是主入口模块
|
|
317
|
+
const modulePath = new URL(import.meta.url).pathname;
|
|
318
|
+
const argvPath = process.argv[1];
|
|
319
|
+
// 在 Windows 和 Unix 系统上都能正常工作的路径比较
|
|
320
|
+
const isMainModule = argvPath === modulePath ||
|
|
321
|
+
argvPath === modulePath.replace(/^\//, '') ||
|
|
322
|
+
argvPath.endsWith('batch-cli.js') ||
|
|
323
|
+
argvPath.endsWith('batch-cli');
|
|
324
|
+
|
|
325
|
+
if (isMainModule) {
|
|
326
|
+
runBatch();
|
|
327
|
+
}
|
package/src/cli.jsx
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { executeBashCommand } from './bash-runner.js';
|
|
6
|
+
|
|
7
|
+
// 历史记录管理
|
|
8
|
+
let commandHistory = [];
|
|
9
|
+
let resultHistory = [];
|
|
10
|
+
|
|
11
|
+
// 命令输入组件
|
|
12
|
+
function CommandInput({ onSubmit }) {
|
|
13
|
+
const [value, setValue] = useState('');
|
|
14
|
+
|
|
15
|
+
const handleSubmit = () => {
|
|
16
|
+
if (value.trim()) {
|
|
17
|
+
onSubmit(value);
|
|
18
|
+
setValue('');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Box borderStyle="double" borderColor="cyan" paddingX={1} marginBottom={1}>
|
|
24
|
+
<Box marginRight={1}>
|
|
25
|
+
<Text bold color="cyan">$</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
<TextInput
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={setValue}
|
|
30
|
+
onSubmit={handleSubmit}
|
|
31
|
+
placeholder="输入 bash 命令..."
|
|
32
|
+
/>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 输出区域组件
|
|
38
|
+
function OutputArea({ label, content, color, borderColor }) {
|
|
39
|
+
return (
|
|
40
|
+
<Box
|
|
41
|
+
borderStyle="round"
|
|
42
|
+
borderColor={borderColor}
|
|
43
|
+
paddingX={1}
|
|
44
|
+
marginBottom={1}
|
|
45
|
+
flexDirection="column"
|
|
46
|
+
width="100%"
|
|
47
|
+
>
|
|
48
|
+
<Text bold color={color}>
|
|
49
|
+
{label}
|
|
50
|
+
</Text>
|
|
51
|
+
<Box flexGrow={1}>
|
|
52
|
+
{content || <Text dim>(空)</Text>}
|
|
53
|
+
</Box>
|
|
54
|
+
</Box>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 信息区域组件
|
|
59
|
+
function InfoArea({ result }) {
|
|
60
|
+
if (!result) {
|
|
61
|
+
return (
|
|
62
|
+
<OutputArea
|
|
63
|
+
label="[INFO] 执行信息"
|
|
64
|
+
content={<Text>等待命令执行...</Text>}
|
|
65
|
+
color="yellow"
|
|
66
|
+
borderColor="yellow"
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const status = result.success ? '✓ 成功' : '✗ 失败';
|
|
72
|
+
const statusColor = result.success ? 'green' : 'red';
|
|
73
|
+
const time = result.timestamp.split('T')[1].split('.')[0];
|
|
74
|
+
|
|
75
|
+
const infoText = [
|
|
76
|
+
<Text key="line1">状态: <Text color={statusColor} bold>{status}</Text></Text>,
|
|
77
|
+
<Text key="line2">退出码: <Text bold>{result.exitCode ?? 'N/A'}</Text></Text>,
|
|
78
|
+
<Text key="line3">时间: {time}</Text>,
|
|
79
|
+
<Text key="line4" dim>命令: {result.command.slice(0, 60)}{result.command.length > 60 ? '...' : ''}</Text>,
|
|
80
|
+
result.signal && (
|
|
81
|
+
<Text key="line5" color="red">信号: {result.signal}</Text>
|
|
82
|
+
),
|
|
83
|
+
result.error && (
|
|
84
|
+
<Text key="line6" color="red">错误: {result.error}</Text>
|
|
85
|
+
)
|
|
86
|
+
].filter(Boolean);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<OutputArea
|
|
90
|
+
label="[INFO] 执行信息"
|
|
91
|
+
content={<Box flexDirection="column">{infoText}</Box>}
|
|
92
|
+
color="yellow"
|
|
93
|
+
borderColor="yellow"
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 主应用组件
|
|
99
|
+
function App() {
|
|
100
|
+
const [lastResult, setLastResult] = useState(null);
|
|
101
|
+
const [isExecuting, setIsExecuting] = useState(false);
|
|
102
|
+
|
|
103
|
+
const handleCommand = useCallback(async (command) => {
|
|
104
|
+
setIsExecuting(true);
|
|
105
|
+
commandHistory.push(command);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await executeBashCommand(command, {
|
|
109
|
+
timeout: 30000
|
|
110
|
+
});
|
|
111
|
+
resultHistory.push(result);
|
|
112
|
+
setLastResult(result);
|
|
113
|
+
} catch (errorResult) {
|
|
114
|
+
resultHistory.push(errorResult);
|
|
115
|
+
setLastResult(errorResult);
|
|
116
|
+
} finally {
|
|
117
|
+
setIsExecuting(false);
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Box flexDirection="column" padding={1}>
|
|
123
|
+
<Box
|
|
124
|
+
borderStyle="bold"
|
|
125
|
+
borderColor="magenta"
|
|
126
|
+
paddingX={1}
|
|
127
|
+
marginBottom={1}
|
|
128
|
+
>
|
|
129
|
+
<Box flexDirection="column">
|
|
130
|
+
<Text bold color="magenta">Bash 调用工具</Text>
|
|
131
|
+
<Text dim>按 Ctrl+C 退出</Text>
|
|
132
|
+
</Box>
|
|
133
|
+
</Box>
|
|
134
|
+
|
|
135
|
+
<InfoArea result={lastResult} />
|
|
136
|
+
|
|
137
|
+
<OutputArea
|
|
138
|
+
label="[STDOUT] 标准输出"
|
|
139
|
+
content={<Text>{lastResult?.stdout || ''}</Text>}
|
|
140
|
+
color="green"
|
|
141
|
+
borderColor="green"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<OutputArea
|
|
145
|
+
label="[STDERR] 错误输出"
|
|
146
|
+
content={<Text>{lastResult?.stderr || ''}</Text>}
|
|
147
|
+
color="red"
|
|
148
|
+
borderColor="red"
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
{isExecuting && (
|
|
152
|
+
<Text color="cyan" dim>⏳ 执行中...</Text>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<CommandInput onSubmit={handleCommand} />
|
|
156
|
+
</Box>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 启动应用
|
|
161
|
+
render(<App />);
|
|
162
|
+
|
|
163
|
+
// 优雅退出
|
|
164
|
+
process.on('SIGINT', () => {
|
|
165
|
+
process.exit(0);
|
|
166
|
+
});
|