@yivan-lab/pretty-please 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/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/pls.js +681 -0
- package/bin/pls.tsx +541 -0
- package/dist/bin/pls.d.ts +2 -0
- package/dist/bin/pls.js +429 -0
- package/dist/src/ai.d.ts +48 -0
- package/dist/src/ai.js +295 -0
- package/dist/src/builtin-detector.d.ts +15 -0
- package/dist/src/builtin-detector.js +83 -0
- package/dist/src/chat-history.d.ts +26 -0
- package/dist/src/chat-history.js +81 -0
- package/dist/src/components/Chat.d.ts +13 -0
- package/dist/src/components/Chat.js +80 -0
- package/dist/src/components/ChatStatus.d.ts +9 -0
- package/dist/src/components/ChatStatus.js +34 -0
- package/dist/src/components/CodeColorizer.d.ts +12 -0
- package/dist/src/components/CodeColorizer.js +82 -0
- package/dist/src/components/CommandBox.d.ts +10 -0
- package/dist/src/components/CommandBox.js +45 -0
- package/dist/src/components/CommandGenerator.d.ts +20 -0
- package/dist/src/components/CommandGenerator.js +116 -0
- package/dist/src/components/ConfigDisplay.d.ts +9 -0
- package/dist/src/components/ConfigDisplay.js +42 -0
- package/dist/src/components/ConfigWizard.d.ts +9 -0
- package/dist/src/components/ConfigWizard.js +72 -0
- package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
- package/dist/src/components/ConfirmationPrompt.js +26 -0
- package/dist/src/components/Duration.d.ts +9 -0
- package/dist/src/components/Duration.js +21 -0
- package/dist/src/components/HistoryDisplay.d.ts +9 -0
- package/dist/src/components/HistoryDisplay.js +51 -0
- package/dist/src/components/HookManager.d.ts +10 -0
- package/dist/src/components/HookManager.js +88 -0
- package/dist/src/components/InlineRenderer.d.ts +12 -0
- package/dist/src/components/InlineRenderer.js +75 -0
- package/dist/src/components/MarkdownDisplay.d.ts +13 -0
- package/dist/src/components/MarkdownDisplay.js +197 -0
- package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
- package/dist/src/components/MultiStepCommandGenerator.js +142 -0
- package/dist/src/components/TableRenderer.d.ts +12 -0
- package/dist/src/components/TableRenderer.js +66 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +203 -0
- package/dist/src/history.d.ts +20 -0
- package/dist/src/history.js +113 -0
- package/dist/src/mastra-agent.d.ts +7 -0
- package/dist/src/mastra-agent.js +31 -0
- package/dist/src/multi-step.d.ts +41 -0
- package/dist/src/multi-step.js +67 -0
- package/dist/src/shell-hook.d.ts +35 -0
- package/dist/src/shell-hook.js +348 -0
- package/dist/src/sysinfo.d.ts +15 -0
- package/dist/src/sysinfo.js +52 -0
- package/dist/src/ui/theme.d.ts +26 -0
- package/dist/src/ui/theme.js +31 -0
- package/dist/src/utils/console.d.ts +44 -0
- package/dist/src/utils/console.js +114 -0
- package/package.json +78 -0
- package/src/ai.js +324 -0
- package/src/builtin-detector.js +98 -0
- package/src/chat-history.js +94 -0
- package/src/components/Chat.tsx +122 -0
- package/src/components/ChatStatus.tsx +53 -0
- package/src/components/CodeColorizer.tsx +128 -0
- package/src/components/CommandBox.tsx +60 -0
- package/src/components/CommandGenerator.tsx +184 -0
- package/src/components/ConfigDisplay.tsx +64 -0
- package/src/components/ConfigWizard.tsx +101 -0
- package/src/components/ConfirmationPrompt.tsx +41 -0
- package/src/components/Duration.tsx +24 -0
- package/src/components/HistoryDisplay.tsx +69 -0
- package/src/components/HookManager.tsx +150 -0
- package/src/components/InlineRenderer.tsx +123 -0
- package/src/components/MarkdownDisplay.tsx +288 -0
- package/src/components/MultiStepCommandGenerator.tsx +229 -0
- package/src/components/TableRenderer.tsx +110 -0
- package/src/config.js +221 -0
- package/src/history.js +131 -0
- package/src/mastra-agent.ts +35 -0
- package/src/multi-step.ts +93 -0
- package/src/shell-hook.js +393 -0
- package/src/sysinfo.js +57 -0
- package/src/ui/theme.ts +37 -0
- package/src/utils/console.js +130 -0
- package/tsconfig.json +23 -0
package/bin/pls.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getConfig,
|
|
13
|
+
setConfigValue,
|
|
14
|
+
isConfigValid,
|
|
15
|
+
displayConfig,
|
|
16
|
+
runConfigWizard
|
|
17
|
+
} from '../src/config.js';
|
|
18
|
+
import { generateCommand } from '../src/ai.js';
|
|
19
|
+
import { chatWithAI } from '../src/ai.js';
|
|
20
|
+
import {
|
|
21
|
+
addHistory,
|
|
22
|
+
getHistory,
|
|
23
|
+
clearHistory,
|
|
24
|
+
getHistoryFilePath
|
|
25
|
+
} from '../src/history.js';
|
|
26
|
+
import {
|
|
27
|
+
clearChatHistory,
|
|
28
|
+
getChatRoundCount,
|
|
29
|
+
getChatHistoryFilePath
|
|
30
|
+
} from '../src/chat-history.js';
|
|
31
|
+
import {
|
|
32
|
+
installShellHook,
|
|
33
|
+
uninstallShellHook,
|
|
34
|
+
getHookStatus,
|
|
35
|
+
detectShell
|
|
36
|
+
} from '../src/shell-hook.js';
|
|
37
|
+
import { detectBuiltin, formatBuiltins } from '../src/builtin-detector.js';
|
|
38
|
+
|
|
39
|
+
// 获取 package.json 版本
|
|
40
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
41
|
+
const __dirname = dirname(__filename);
|
|
42
|
+
const packageJson = JSON.parse(fs.readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
|
43
|
+
|
|
44
|
+
const program = new Command();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 计算字符串的显示宽度(中文占2个宽度)
|
|
48
|
+
*/
|
|
49
|
+
function getDisplayWidth(str) {
|
|
50
|
+
let width = 0;
|
|
51
|
+
for (const char of str) {
|
|
52
|
+
// 中文、日文、韩文等宽字符占 2 个宽度
|
|
53
|
+
if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
|
|
54
|
+
width += 2;
|
|
55
|
+
} else {
|
|
56
|
+
width += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return width;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 绘制命令框
|
|
64
|
+
* @param {string} command - 要显示的命令
|
|
65
|
+
* @param {string} title - 框框标题
|
|
66
|
+
*/
|
|
67
|
+
function drawCommandBox(command, title = '生成命令') {
|
|
68
|
+
const lines = command.split('\n');
|
|
69
|
+
const titleWidth = getDisplayWidth(title);
|
|
70
|
+
const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
|
|
71
|
+
const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20);
|
|
72
|
+
|
|
73
|
+
// 顶部边框:┌─ 生成命令 ─────┐
|
|
74
|
+
const topPadding = boxWidth - titleWidth - 5;
|
|
75
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐';
|
|
76
|
+
|
|
77
|
+
// 底部边框
|
|
78
|
+
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
|
|
79
|
+
|
|
80
|
+
console.log(chalk.yellow(topBorder));
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const lineWidth = getDisplayWidth(line);
|
|
83
|
+
const padding = ' '.repeat(boxWidth - lineWidth - 4);
|
|
84
|
+
console.log(chalk.yellow('│ ') + chalk.cyan(line) + padding + chalk.yellow(' │'));
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.yellow(bottomBorder));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 格式化耗时
|
|
91
|
+
* @param {number} ms - 毫秒数
|
|
92
|
+
*/
|
|
93
|
+
function formatDuration(ms) {
|
|
94
|
+
if (ms < 1000) {
|
|
95
|
+
return `${ms}ms`;
|
|
96
|
+
}
|
|
97
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 询问用户确认(单键模式)
|
|
102
|
+
* 回车 = 确认执行,Esc = 取消
|
|
103
|
+
*/
|
|
104
|
+
function askConfirmation(prompt) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
process.stdout.write(prompt);
|
|
107
|
+
|
|
108
|
+
// 启用原始模式以捕获单个按键
|
|
109
|
+
if (process.stdin.isTTY) {
|
|
110
|
+
process.stdin.setRawMode(true);
|
|
111
|
+
}
|
|
112
|
+
process.stdin.resume();
|
|
113
|
+
|
|
114
|
+
const onKeyPress = (key) => {
|
|
115
|
+
// 恢复正常模式
|
|
116
|
+
if (process.stdin.isTTY) {
|
|
117
|
+
process.stdin.setRawMode(false);
|
|
118
|
+
}
|
|
119
|
+
process.stdin.pause();
|
|
120
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
121
|
+
|
|
122
|
+
// 换行,让后续输出在新行显示
|
|
123
|
+
process.stdout.write('\n');
|
|
124
|
+
|
|
125
|
+
// 检测按键
|
|
126
|
+
if (key[0] === 0x0d || key[0] === 0x0a) {
|
|
127
|
+
// Enter 键 (回车)
|
|
128
|
+
resolve(true);
|
|
129
|
+
} else if (key[0] === 0x1b) {
|
|
130
|
+
// Esc 键
|
|
131
|
+
resolve(false);
|
|
132
|
+
} else if (key[0] === 0x03) {
|
|
133
|
+
// Ctrl+C
|
|
134
|
+
process.exit(0);
|
|
135
|
+
} else {
|
|
136
|
+
// 其他键,忽略,继续等待
|
|
137
|
+
process.stdout.write(prompt);
|
|
138
|
+
if (process.stdin.isTTY) {
|
|
139
|
+
process.stdin.setRawMode(true);
|
|
140
|
+
}
|
|
141
|
+
process.stdin.resume();
|
|
142
|
+
process.stdin.once('data', onKeyPress);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
process.stdin.once('data', onKeyPress);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 执行命令并返回结果
|
|
152
|
+
*/
|
|
153
|
+
function executeCommand(command) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
let output = '';
|
|
156
|
+
|
|
157
|
+
const child = exec(command, { shell: true });
|
|
158
|
+
|
|
159
|
+
child.stdout?.on('data', (data) => {
|
|
160
|
+
output += data;
|
|
161
|
+
process.stdout.write(data);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
child.stderr?.on('data', (data) => {
|
|
165
|
+
output += data;
|
|
166
|
+
process.stderr.write(data);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
child.on('close', (code) => {
|
|
170
|
+
resolve({ exitCode: code, output });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on('error', (err) => {
|
|
174
|
+
resolve({ exitCode: 1, output: err.message });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 执行命令(配合 spinner 使用)
|
|
181
|
+
* 先停止 spinner,显示输出,执行完成后再更新 spinner 状态
|
|
182
|
+
*/
|
|
183
|
+
function executeCommandWithSpinner(command, spinner) {
|
|
184
|
+
return new Promise((resolve) => {
|
|
185
|
+
let output = '';
|
|
186
|
+
let hasOutput = false; // 标记是否有输出
|
|
187
|
+
let topSeparatorShown = false; // 标记是否已显示顶部分隔线
|
|
188
|
+
|
|
189
|
+
// 停止 spinner 动画,但不改变状态
|
|
190
|
+
spinner.stop();
|
|
191
|
+
|
|
192
|
+
const child = exec(command, { shell: true });
|
|
193
|
+
|
|
194
|
+
child.stdout?.on('data', (data) => {
|
|
195
|
+
output += data;
|
|
196
|
+
hasOutput = true;
|
|
197
|
+
|
|
198
|
+
// 第一次有输出时显示顶部分隔线
|
|
199
|
+
if (!topSeparatorShown) {
|
|
200
|
+
console.log(chalk.gray('\n─── 输出 ' + '─'.repeat(30)));
|
|
201
|
+
topSeparatorShown = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
process.stdout.write(data);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
child.stderr?.on('data', (data) => {
|
|
208
|
+
output += data;
|
|
209
|
+
hasOutput = true;
|
|
210
|
+
|
|
211
|
+
// 第一次有输出时显示顶部分隔线
|
|
212
|
+
if (!topSeparatorShown) {
|
|
213
|
+
console.log(chalk.gray('\n─── 输出 ' + '─'.repeat(30)));
|
|
214
|
+
topSeparatorShown = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
process.stderr.write(data);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
child.on('close', (code) => {
|
|
221
|
+
// 只在有输出时显示底部分隔线
|
|
222
|
+
if (hasOutput) {
|
|
223
|
+
console.log(chalk.gray('─'.repeat(38)));
|
|
224
|
+
}
|
|
225
|
+
resolve({ exitCode: code, output });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
child.on('error', (err) => {
|
|
229
|
+
// error 也算有输出
|
|
230
|
+
if (!topSeparatorShown) {
|
|
231
|
+
console.log(chalk.gray('\n─── 输出 ' + '─'.repeat(30)));
|
|
232
|
+
}
|
|
233
|
+
console.log(chalk.gray('─'.repeat(38)));
|
|
234
|
+
resolve({ exitCode: 1, output: err.message });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 显示调试信息
|
|
241
|
+
*/
|
|
242
|
+
function displayDebugInfo(debug) {
|
|
243
|
+
console.log(chalk.magenta('\n━━━ 调试信息 ━━━'));
|
|
244
|
+
console.log(chalk.gray('系统信息: ') + debug.sysinfo);
|
|
245
|
+
console.log(chalk.gray('模型: ') + debug.model);
|
|
246
|
+
console.log(chalk.gray('System Prompt:'));
|
|
247
|
+
console.log(chalk.dim(debug.systemPrompt));
|
|
248
|
+
console.log(chalk.gray('User Prompt: ') + debug.userPrompt);
|
|
249
|
+
console.log(chalk.magenta('━'.repeat(16)));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 主要的命令执行流程
|
|
254
|
+
*/
|
|
255
|
+
async function runPrompt(promptArgs, options = {}) {
|
|
256
|
+
const prompt = promptArgs.join(' ');
|
|
257
|
+
const debug = options.debug || false;
|
|
258
|
+
|
|
259
|
+
if (!prompt.trim()) {
|
|
260
|
+
console.log(chalk.red('请提供你想执行的操作描述'));
|
|
261
|
+
console.log(chalk.gray('示例: pls 安装 git'));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 检查配置
|
|
266
|
+
if (!isConfigValid()) {
|
|
267
|
+
console.log(chalk.yellow('\n⚠️ 检测到尚未配置 API Key'));
|
|
268
|
+
console.log(chalk.gray('请运行 ') + chalk.cyan('pls config') + chalk.gray(' 进行配置\n'));
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// 思考中 spinner
|
|
274
|
+
const thinkingSpinner = ora({
|
|
275
|
+
text: '正在思考...',
|
|
276
|
+
spinner: 'dots'
|
|
277
|
+
}).start();
|
|
278
|
+
|
|
279
|
+
const thinkStartTime = Date.now();
|
|
280
|
+
const result = await generateCommand(prompt, { debug });
|
|
281
|
+
const thinkDuration = Date.now() - thinkStartTime;
|
|
282
|
+
|
|
283
|
+
// 根据是否调试模式,解构结果
|
|
284
|
+
const command = debug ? result.command : result;
|
|
285
|
+
|
|
286
|
+
thinkingSpinner.succeed(chalk.gray(`思考完成 (${formatDuration(thinkDuration)})`));
|
|
287
|
+
|
|
288
|
+
// 调试模式下显示调试信息
|
|
289
|
+
if (debug) {
|
|
290
|
+
displayDebugInfo(result.debug);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 显示生成的命令(框框样式)
|
|
294
|
+
console.log('');
|
|
295
|
+
drawCommandBox(command);
|
|
296
|
+
|
|
297
|
+
// 检测是否包含 builtin 命令
|
|
298
|
+
const { hasBuiltin, builtins } = detectBuiltin(command);
|
|
299
|
+
|
|
300
|
+
if (hasBuiltin) {
|
|
301
|
+
// 包含 builtin,不执行,只提示
|
|
302
|
+
console.log(chalk.red('\n⚠️ 此命令包含 shell 内置命令(' + formatBuiltins(builtins) + '),无法在子进程中生效'));
|
|
303
|
+
console.log(chalk.yellow('💡 请手动复制到终端执行\n'));
|
|
304
|
+
|
|
305
|
+
// 记录历史(标记为未执行,原因是 builtin)
|
|
306
|
+
addHistory({
|
|
307
|
+
userPrompt: prompt,
|
|
308
|
+
command,
|
|
309
|
+
executed: false,
|
|
310
|
+
exitCode: null,
|
|
311
|
+
output: '',
|
|
312
|
+
reason: 'builtin'
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 询问确认
|
|
319
|
+
const confirmed = await askConfirmation(
|
|
320
|
+
chalk.bold.yellow('执行?') + chalk.gray(' [回车执行 / Esc 取消] ')
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (confirmed) {
|
|
324
|
+
// 执行中 spinner
|
|
325
|
+
const execSpinner = ora({
|
|
326
|
+
text: '执行中...',
|
|
327
|
+
spinner: 'dots'
|
|
328
|
+
}).start();
|
|
329
|
+
|
|
330
|
+
const execStartTime = Date.now();
|
|
331
|
+
const { exitCode, output } = await executeCommandWithSpinner(command, execSpinner);
|
|
332
|
+
const execDuration = Date.now() - execStartTime;
|
|
333
|
+
|
|
334
|
+
// 记录历史
|
|
335
|
+
addHistory({
|
|
336
|
+
userPrompt: prompt,
|
|
337
|
+
command,
|
|
338
|
+
executed: true,
|
|
339
|
+
exitCode,
|
|
340
|
+
output
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (exitCode === 0) {
|
|
344
|
+
execSpinner.succeed(chalk.green(`执行完成 (${formatDuration(execDuration)})`));
|
|
345
|
+
} else {
|
|
346
|
+
execSpinner.fail(chalk.red(`执行失败,退出码: ${exitCode} (${formatDuration(execDuration)})`));
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// 记录未执行的历史
|
|
350
|
+
addHistory({
|
|
351
|
+
userPrompt: prompt,
|
|
352
|
+
command,
|
|
353
|
+
executed: false,
|
|
354
|
+
exitCode: null,
|
|
355
|
+
output: ''
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
console.log(chalk.gray('\n已取消执行\n'));
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error(chalk.red('\n❌ 错误: ') + error.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 设置程序
|
|
367
|
+
program
|
|
368
|
+
.name('pls')
|
|
369
|
+
.description('AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令')
|
|
370
|
+
.version(packageJson.version, '-v, --version', '显示版本号')
|
|
371
|
+
.helpOption('-h, --help', '显示帮助信息');
|
|
372
|
+
|
|
373
|
+
// config 子命令
|
|
374
|
+
const configCmd = program
|
|
375
|
+
.command('config')
|
|
376
|
+
.description('管理配置');
|
|
377
|
+
|
|
378
|
+
configCmd
|
|
379
|
+
.command('list')
|
|
380
|
+
.description('查看当前配置')
|
|
381
|
+
.action(() => {
|
|
382
|
+
displayConfig();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
configCmd
|
|
386
|
+
.command('show')
|
|
387
|
+
.description('查看当前配置')
|
|
388
|
+
.action(() => {
|
|
389
|
+
displayConfig();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
configCmd
|
|
393
|
+
.command('set <key> <value>')
|
|
394
|
+
.description('设置配置项 (apiKey, baseUrl, model, shellHook, chatHistoryLimit)')
|
|
395
|
+
.action((key, value) => {
|
|
396
|
+
try {
|
|
397
|
+
setConfigValue(key, value);
|
|
398
|
+
console.log(chalk.green(`✅ 已设置 ${key}`));
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error(chalk.red(`❌ ${error.message}`));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// 默认 config 命令(交互式配置)
|
|
406
|
+
configCmd
|
|
407
|
+
.action(async () => {
|
|
408
|
+
await runConfigWizard();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// history 子命令
|
|
412
|
+
const historyCmd = program
|
|
413
|
+
.command('history')
|
|
414
|
+
.description('查看或管理命令历史');
|
|
415
|
+
|
|
416
|
+
historyCmd
|
|
417
|
+
.command('show')
|
|
418
|
+
.description('显示历史记录')
|
|
419
|
+
.action(() => {
|
|
420
|
+
const history = getHistory();
|
|
421
|
+
if (history.length === 0) {
|
|
422
|
+
console.log(chalk.gray('\n暂无历史记录\n'));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
console.log(chalk.bold('\n📜 命令历史:'));
|
|
427
|
+
console.log(chalk.gray('━'.repeat(50)));
|
|
428
|
+
|
|
429
|
+
history.forEach((item, index) => {
|
|
430
|
+
const status = item.executed
|
|
431
|
+
? (item.exitCode === 0 ? chalk.green('✓') : chalk.red(`✗ 退出码:${item.exitCode}`))
|
|
432
|
+
: chalk.gray('(未执行)');
|
|
433
|
+
|
|
434
|
+
console.log(`${chalk.gray(`${index + 1}.`)} ${chalk.cyan(item.userPrompt)}`);
|
|
435
|
+
console.log(` ${chalk.dim('→')} ${item.command} ${status}`);
|
|
436
|
+
console.log(` ${chalk.gray(item.timestamp)}`);
|
|
437
|
+
console.log();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
console.log(chalk.gray(`历史文件: ${getHistoryFilePath()}\n`));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
historyCmd
|
|
444
|
+
.command('clear')
|
|
445
|
+
.description('清空历史记录')
|
|
446
|
+
.action(() => {
|
|
447
|
+
clearHistory();
|
|
448
|
+
console.log(chalk.green('✅ 历史记录已清空'));
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// 默认 history 命令(显示历史)
|
|
452
|
+
historyCmd
|
|
453
|
+
.action(() => {
|
|
454
|
+
const history = getHistory();
|
|
455
|
+
if (history.length === 0) {
|
|
456
|
+
console.log(chalk.gray('\n暂无历史记录\n'));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log(chalk.bold('\n📜 命令历史:'));
|
|
461
|
+
console.log(chalk.gray('━'.repeat(50)));
|
|
462
|
+
|
|
463
|
+
history.forEach((item, index) => {
|
|
464
|
+
const status = item.executed
|
|
465
|
+
? (item.exitCode === 0 ? chalk.green('✓') : chalk.red(`✗ 退出码:${item.exitCode}`))
|
|
466
|
+
: chalk.gray('(未执行)');
|
|
467
|
+
|
|
468
|
+
console.log(`${chalk.gray(`${index + 1}.`)} ${chalk.cyan(item.userPrompt)}`);
|
|
469
|
+
console.log(` ${chalk.dim('→')} ${item.command} ${status}`);
|
|
470
|
+
console.log(` ${chalk.gray(item.timestamp)}`);
|
|
471
|
+
console.log();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
console.log(chalk.gray(`历史文件: ${getHistoryFilePath()}\n`));
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// hook 子命令 - 安装/卸载 shell hook
|
|
478
|
+
const hookCmd = program
|
|
479
|
+
.command('hook')
|
|
480
|
+
.description('管理 shell hook(增强功能:记录终端命令历史)');
|
|
481
|
+
|
|
482
|
+
hookCmd
|
|
483
|
+
.command('install')
|
|
484
|
+
.description('安装 shell hook')
|
|
485
|
+
.action(async () => {
|
|
486
|
+
const status = getHookStatus();
|
|
487
|
+
console.log(chalk.bold('\n🔧 Shell Hook 安装向导'));
|
|
488
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
489
|
+
console.log(chalk.gray(`检测到 Shell: ${status.shellType}`));
|
|
490
|
+
console.log(chalk.gray(`配置文件: ${status.configPath || '未知'}`));
|
|
491
|
+
console.log();
|
|
492
|
+
|
|
493
|
+
if (status.shellType === 'unknown') {
|
|
494
|
+
console.log(chalk.red('❌ 不支持的 shell 类型'));
|
|
495
|
+
console.log(chalk.gray('支持的 shell: zsh, bash, powershell'));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
console.log(chalk.yellow('此功能会在你的 shell 配置文件中添加 hook,'));
|
|
500
|
+
console.log(chalk.yellow('用于记录你在终端执行的每条命令,让 AI 更智能。'));
|
|
501
|
+
console.log();
|
|
502
|
+
|
|
503
|
+
await installShellHook();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
hookCmd
|
|
507
|
+
.command('uninstall')
|
|
508
|
+
.description('卸载 shell hook')
|
|
509
|
+
.action(() => {
|
|
510
|
+
uninstallShellHook();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
hookCmd
|
|
514
|
+
.command('status')
|
|
515
|
+
.description('查看 shell hook 状态')
|
|
516
|
+
.action(() => {
|
|
517
|
+
const status = getHookStatus();
|
|
518
|
+
console.log(chalk.bold('\n📊 Shell Hook 状态'));
|
|
519
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
520
|
+
console.log(` ${chalk.cyan('Shell 类型')}: ${status.shellType}`);
|
|
521
|
+
console.log(` ${chalk.cyan('配置文件')}: ${status.configPath || '未知'}`);
|
|
522
|
+
console.log(` ${chalk.cyan('已安装')}: ${status.installed ? chalk.green('是') : chalk.gray('否')}`);
|
|
523
|
+
console.log(` ${chalk.cyan('已启用')}: ${status.enabled ? chalk.green('是') : chalk.gray('否')}`);
|
|
524
|
+
console.log(` ${chalk.cyan('历史文件')}: ${status.historyFile}`);
|
|
525
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
526
|
+
|
|
527
|
+
if (!status.installed) {
|
|
528
|
+
console.log(chalk.gray('\n提示: 运行 ') + chalk.cyan('pls hook install') + chalk.gray(' 安装 shell hook'));
|
|
529
|
+
}
|
|
530
|
+
console.log();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// 默认 hook 命令(显示状态)
|
|
534
|
+
hookCmd
|
|
535
|
+
.action(() => {
|
|
536
|
+
const status = getHookStatus();
|
|
537
|
+
console.log(chalk.bold('\n📊 Shell Hook 状态'));
|
|
538
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
539
|
+
console.log(` ${chalk.cyan('Shell 类型')}: ${status.shellType}`);
|
|
540
|
+
console.log(` ${chalk.cyan('配置文件')}: ${status.configPath || '未知'}`);
|
|
541
|
+
console.log(` ${chalk.cyan('已安装')}: ${status.installed ? chalk.green('是') : chalk.gray('否')}`);
|
|
542
|
+
console.log(` ${chalk.cyan('已启用')}: ${status.enabled ? chalk.green('是') : chalk.gray('否')}`);
|
|
543
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
544
|
+
|
|
545
|
+
if (!status.installed) {
|
|
546
|
+
console.log(chalk.gray('\n提示: 运行 ') + chalk.cyan('pls hook install') + chalk.gray(' 安装 shell hook'));
|
|
547
|
+
console.log(chalk.gray(' 运行 ') + chalk.cyan('pls hook uninstall') + chalk.gray(' 卸载 shell hook'));
|
|
548
|
+
}
|
|
549
|
+
console.log();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// chat 子命令 - AI 对话模式
|
|
553
|
+
const chatCmd = program
|
|
554
|
+
.command('chat')
|
|
555
|
+
.description('AI 对话模式,问答、讲解命令');
|
|
556
|
+
|
|
557
|
+
chatCmd
|
|
558
|
+
.command('clear')
|
|
559
|
+
.description('清空对话历史')
|
|
560
|
+
.action(() => {
|
|
561
|
+
clearChatHistory();
|
|
562
|
+
console.log(chalk.green('✅ 对话历史已清空'));
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// 默认 chat 命令(进行对话)
|
|
566
|
+
chatCmd
|
|
567
|
+
.argument('[prompt...]', '你的问题')
|
|
568
|
+
.option('-d, --debug', '显示调试信息')
|
|
569
|
+
.action(async (promptArgs, options) => {
|
|
570
|
+
const prompt = promptArgs.join(' ');
|
|
571
|
+
|
|
572
|
+
if (!prompt.trim()) {
|
|
573
|
+
// 没有输入,显示对话状态
|
|
574
|
+
const roundCount = getChatRoundCount();
|
|
575
|
+
console.log(chalk.bold('\n💬 AI 对话模式'));
|
|
576
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
577
|
+
console.log(` ${chalk.cyan('当前对话轮数')}: ${roundCount}`);
|
|
578
|
+
console.log(` ${chalk.cyan('历史文件')}: ${getChatHistoryFilePath()}`);
|
|
579
|
+
console.log(chalk.gray('━'.repeat(40)));
|
|
580
|
+
console.log(chalk.gray('\n用法:'));
|
|
581
|
+
console.log(chalk.cyan(' pls chat <问题>') + chalk.gray(' 与 AI 对话'));
|
|
582
|
+
console.log(chalk.cyan(' pls chat clear') + chalk.gray(' 清空对话历史'));
|
|
583
|
+
console.log();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 检查配置
|
|
588
|
+
if (!isConfigValid()) {
|
|
589
|
+
console.log(chalk.yellow('\n⚠️ 检测到尚未配置 API Key'));
|
|
590
|
+
console.log(chalk.gray('请运行 ') + chalk.cyan('pls config') + chalk.gray(' 进行配置\n'));
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// 显示对话轮数
|
|
596
|
+
const roundCount = getChatRoundCount();
|
|
597
|
+
if (roundCount > 0) {
|
|
598
|
+
console.log(chalk.gray(`(对话轮数: ${roundCount})`));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 思考中 spinner
|
|
602
|
+
const spinner = ora({
|
|
603
|
+
text: '思考中...',
|
|
604
|
+
spinner: 'dots'
|
|
605
|
+
}).start();
|
|
606
|
+
|
|
607
|
+
const startTime = Date.now();
|
|
608
|
+
let firstChunk = true;
|
|
609
|
+
|
|
610
|
+
// 流式输出回调 - 逐字符输出原始 markdown
|
|
611
|
+
const onChunk = (content) => {
|
|
612
|
+
if (firstChunk) {
|
|
613
|
+
// 第一个 chunk 到来,清理 spinner
|
|
614
|
+
spinner.stop();
|
|
615
|
+
process.stdout.write('\r\x1b[K'); // 清除当前行
|
|
616
|
+
firstChunk = false;
|
|
617
|
+
}
|
|
618
|
+
// 直接输出原始内容(逐字符)
|
|
619
|
+
process.stdout.write(content);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const result = await chatWithAI(prompt, {
|
|
623
|
+
debug: options.debug,
|
|
624
|
+
onChunk
|
|
625
|
+
});
|
|
626
|
+
const duration = Date.now() - startTime;
|
|
627
|
+
|
|
628
|
+
// 输出完成后换行
|
|
629
|
+
console.log();
|
|
630
|
+
console.log(chalk.gray(`(${formatDuration(duration)})`));
|
|
631
|
+
|
|
632
|
+
// 调试模式下显示调试信息
|
|
633
|
+
if (options.debug) {
|
|
634
|
+
console.log(chalk.magenta('\n━━━ 调试信息 ━━━'));
|
|
635
|
+
console.log(chalk.gray('系统信息: ') + result.debug.sysinfo);
|
|
636
|
+
console.log(chalk.gray('模型: ') + result.debug.model);
|
|
637
|
+
console.log(chalk.gray('对话历史轮数: ') + Math.floor(result.debug.chatHistory.length / 2));
|
|
638
|
+
console.log(chalk.gray('System Prompt:'));
|
|
639
|
+
console.log(chalk.dim(result.debug.systemPrompt));
|
|
640
|
+
console.log(chalk.gray('User Prompt: ') + result.debug.userPrompt);
|
|
641
|
+
console.log(chalk.magenta('━'.repeat(16)));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error(chalk.red('\n❌ 错误: ') + error.message);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// 默认命令(执行 prompt)
|
|
651
|
+
program
|
|
652
|
+
.argument('[prompt...]', '自然语言描述你想执行的操作')
|
|
653
|
+
.option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
|
|
654
|
+
.action(async (promptArgs, options) => {
|
|
655
|
+
if (promptArgs.length === 0) {
|
|
656
|
+
program.help();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
await runPrompt(promptArgs, { debug: options.debug });
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// 自定义帮助信息
|
|
663
|
+
program.addHelpText('after', `
|
|
664
|
+
|
|
665
|
+
${chalk.bold('示例:')}
|
|
666
|
+
${chalk.cyan('pls 安装 git')} 让 AI 生成安装 git 的命令
|
|
667
|
+
${chalk.cyan('pls 查找大于 100MB 的文件')} 查找大文件
|
|
668
|
+
${chalk.cyan('pls 删除刚才创建的文件')} AI 会参考历史记录
|
|
669
|
+
${chalk.cyan('pls --debug 压缩 logs 目录')} 显示调试信息
|
|
670
|
+
${chalk.cyan('pls chat tar 命令怎么用')} AI 对话模式
|
|
671
|
+
${chalk.cyan('pls chat clear')} 清空对话历史
|
|
672
|
+
${chalk.cyan('pls history')} 查看 pls 命令历史
|
|
673
|
+
${chalk.cyan('pls history clear')} 清空历史记录
|
|
674
|
+
${chalk.cyan('pls hook')} 查看 shell hook 状态
|
|
675
|
+
${chalk.cyan('pls hook install')} 安装 shell hook(增强功能)
|
|
676
|
+
${chalk.cyan('pls hook uninstall')} 卸载 shell hook
|
|
677
|
+
${chalk.cyan('pls config')} 交互式配置
|
|
678
|
+
${chalk.cyan('pls config list')} 查看当前配置
|
|
679
|
+
`);
|
|
680
|
+
|
|
681
|
+
program.parse();
|