@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
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { CONFIG_DIR, getConfig, setConfigValue } from './config.js';
|
|
6
|
+
import { getHistory } from './history.js';
|
|
7
|
+
|
|
8
|
+
const SHELL_HISTORY_FILE = path.join(CONFIG_DIR, 'shell_history.jsonl');
|
|
9
|
+
const MAX_SHELL_HISTORY = 20;
|
|
10
|
+
|
|
11
|
+
// Hook 标记,用于识别我们添加的内容
|
|
12
|
+
const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>';
|
|
13
|
+
const HOOK_END_MARKER = '# <<< pretty-please shell hook <<<';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 检测当前 shell 类型
|
|
17
|
+
*/
|
|
18
|
+
export function detectShell() {
|
|
19
|
+
const shell = process.env.SHELL || '';
|
|
20
|
+
if (shell.includes('zsh')) return 'zsh';
|
|
21
|
+
if (shell.includes('bash')) return 'bash';
|
|
22
|
+
// Windows PowerShell
|
|
23
|
+
if (process.platform === 'win32') return 'powershell';
|
|
24
|
+
return 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 获取 shell 配置文件路径
|
|
29
|
+
*/
|
|
30
|
+
export function getShellConfigPath(shellType) {
|
|
31
|
+
const home = os.homedir();
|
|
32
|
+
switch (shellType) {
|
|
33
|
+
case 'zsh':
|
|
34
|
+
return path.join(home, '.zshrc');
|
|
35
|
+
case 'bash':
|
|
36
|
+
// macOS 使用 .bash_profile,Linux 使用 .bashrc
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
return path.join(home, '.bash_profile');
|
|
39
|
+
}
|
|
40
|
+
return path.join(home, '.bashrc');
|
|
41
|
+
case 'powershell':
|
|
42
|
+
// PowerShell profile 路径
|
|
43
|
+
return path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
44
|
+
default:
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 生成 zsh hook 脚本
|
|
51
|
+
*/
|
|
52
|
+
function generateZshHook() {
|
|
53
|
+
return `
|
|
54
|
+
${HOOK_START_MARKER}
|
|
55
|
+
# 记录命令到 pretty-please 历史
|
|
56
|
+
__pls_preexec() {
|
|
57
|
+
__PLS_LAST_CMD="$1"
|
|
58
|
+
__PLS_CMD_START=$(date +%s)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
__pls_precmd() {
|
|
62
|
+
local exit_code=$?
|
|
63
|
+
if [[ -n "$__PLS_LAST_CMD" ]]; then
|
|
64
|
+
local end_time=$(date +%s)
|
|
65
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
66
|
+
# 转义命令中的特殊字符
|
|
67
|
+
local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
68
|
+
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
|
|
69
|
+
# 保持文件不超过 ${MAX_SHELL_HISTORY} 行
|
|
70
|
+
tail -n ${MAX_SHELL_HISTORY} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
|
|
71
|
+
unset __PLS_LAST_CMD
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
autoload -Uz add-zsh-hook
|
|
76
|
+
add-zsh-hook preexec __pls_preexec
|
|
77
|
+
add-zsh-hook precmd __pls_precmd
|
|
78
|
+
${HOOK_END_MARKER}
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 生成 bash hook 脚本
|
|
84
|
+
*/
|
|
85
|
+
function generateBashHook() {
|
|
86
|
+
return `
|
|
87
|
+
${HOOK_START_MARKER}
|
|
88
|
+
# 记录命令到 pretty-please 历史
|
|
89
|
+
__pls_prompt_command() {
|
|
90
|
+
local exit_code=$?
|
|
91
|
+
local last_cmd=$(history 1 | sed 's/^ *[0-9]* *//')
|
|
92
|
+
if [[ -n "$last_cmd" && "$last_cmd" != "$__PLS_LAST_CMD" ]]; then
|
|
93
|
+
__PLS_LAST_CMD="$last_cmd"
|
|
94
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
95
|
+
local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
96
|
+
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
|
|
97
|
+
tail -n ${MAX_SHELL_HISTORY} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if [[ ! "$PROMPT_COMMAND" =~ __pls_prompt_command ]]; then
|
|
102
|
+
PROMPT_COMMAND="__pls_prompt_command;\${PROMPT_COMMAND}"
|
|
103
|
+
fi
|
|
104
|
+
${HOOK_END_MARKER}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 生成 PowerShell hook 脚本
|
|
110
|
+
*/
|
|
111
|
+
function generatePowerShellHook() {
|
|
112
|
+
return `
|
|
113
|
+
${HOOK_START_MARKER}
|
|
114
|
+
# 记录命令到 pretty-please 历史
|
|
115
|
+
$Global:__PlsLastCmd = ""
|
|
116
|
+
|
|
117
|
+
function __Pls_RecordCommand {
|
|
118
|
+
$lastCmd = (Get-History -Count 1).CommandLine
|
|
119
|
+
if ($lastCmd -and $lastCmd -ne $Global:__PlsLastCmd) {
|
|
120
|
+
$Global:__PlsLastCmd = $lastCmd
|
|
121
|
+
$exitCode = $LASTEXITCODE
|
|
122
|
+
if ($null -eq $exitCode) { $exitCode = 0 }
|
|
123
|
+
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
124
|
+
$escapedCmd = $lastCmd -replace '\\\\', '\\\\\\\\' -replace '"', '\\\\"'
|
|
125
|
+
$json = "{\`"cmd\`":\`"$escapedCmd\`",\`"exit\`":$exitCode,\`"time\`":\`"$timestamp\`"}"
|
|
126
|
+
Add-Content -Path "${CONFIG_DIR}/shell_history.jsonl" -Value $json
|
|
127
|
+
# 保持文件不超过 ${MAX_SHELL_HISTORY} 行
|
|
128
|
+
$content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${MAX_SHELL_HISTORY}
|
|
129
|
+
$content | Set-Content "${CONFIG_DIR}/shell_history.jsonl"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (-not (Get-Variable -Name __PlsPromptBackup -ErrorAction SilentlyContinue)) {
|
|
134
|
+
$Global:__PlsPromptBackup = $function:prompt
|
|
135
|
+
function Global:prompt {
|
|
136
|
+
__Pls_RecordCommand
|
|
137
|
+
& $Global:__PlsPromptBackup
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
${HOOK_END_MARKER}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 生成 hook 脚本
|
|
146
|
+
*/
|
|
147
|
+
function generateHookScript(shellType) {
|
|
148
|
+
switch (shellType) {
|
|
149
|
+
case 'zsh':
|
|
150
|
+
return generateZshHook();
|
|
151
|
+
case 'bash':
|
|
152
|
+
return generateBashHook();
|
|
153
|
+
case 'powershell':
|
|
154
|
+
return generatePowerShellHook();
|
|
155
|
+
default:
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 安装 shell hook
|
|
162
|
+
*/
|
|
163
|
+
export async function installShellHook() {
|
|
164
|
+
const shellType = detectShell();
|
|
165
|
+
const configPath = getShellConfigPath(shellType);
|
|
166
|
+
|
|
167
|
+
if (!configPath) {
|
|
168
|
+
console.log(chalk.red(`❌ 不支持的 shell 类型: ${shellType}`));
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hookScript = generateHookScript(shellType);
|
|
173
|
+
if (!hookScript) {
|
|
174
|
+
console.log(chalk.red(`❌ 无法为 ${shellType} 生成 hook 脚本`));
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 检查是否已安装
|
|
179
|
+
if (fs.existsSync(configPath)) {
|
|
180
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
181
|
+
if (content.includes(HOOK_START_MARKER)) {
|
|
182
|
+
console.log(chalk.yellow('⚠️ Shell hook 已安装,跳过'));
|
|
183
|
+
setConfigValue('shellHook', true);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 确保配置目录存在
|
|
189
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
190
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 备份原配置文件
|
|
194
|
+
if (fs.existsSync(configPath)) {
|
|
195
|
+
const backupPath = configPath + '.pls-backup';
|
|
196
|
+
fs.copyFileSync(configPath, backupPath);
|
|
197
|
+
console.log(chalk.gray(`已备份原配置文件到: ${backupPath}`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 追加 hook 脚本
|
|
201
|
+
fs.appendFileSync(configPath, hookScript);
|
|
202
|
+
|
|
203
|
+
// 更新配置
|
|
204
|
+
setConfigValue('shellHook', true);
|
|
205
|
+
|
|
206
|
+
console.log(chalk.green(`✅ Shell hook 已安装到: ${configPath}`));
|
|
207
|
+
console.log(chalk.yellow('⚠️ 请重启终端或执行以下命令使其生效:'));
|
|
208
|
+
console.log(chalk.cyan(` source ${configPath}`));
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 卸载 shell hook
|
|
215
|
+
*/
|
|
216
|
+
export function uninstallShellHook() {
|
|
217
|
+
const shellType = detectShell();
|
|
218
|
+
const configPath = getShellConfigPath(shellType);
|
|
219
|
+
|
|
220
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
221
|
+
console.log(chalk.yellow('⚠️ 未找到 shell 配置文件'));
|
|
222
|
+
setConfigValue('shellHook', false);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let content = fs.readFileSync(configPath, 'utf-8');
|
|
227
|
+
|
|
228
|
+
// 移除 hook 脚本
|
|
229
|
+
const startIndex = content.indexOf(HOOK_START_MARKER);
|
|
230
|
+
const endIndex = content.indexOf(HOOK_END_MARKER);
|
|
231
|
+
|
|
232
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
233
|
+
console.log(chalk.yellow('⚠️ 未找到已安装的 hook'));
|
|
234
|
+
setConfigValue('shellHook', false);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 移除从标记开始到结束的所有内容(包括换行符)
|
|
239
|
+
const before = content.substring(0, startIndex);
|
|
240
|
+
const after = content.substring(endIndex + HOOK_END_MARKER.length);
|
|
241
|
+
content = before + after.replace(/^\n/, '');
|
|
242
|
+
|
|
243
|
+
fs.writeFileSync(configPath, content);
|
|
244
|
+
setConfigValue('shellHook', false);
|
|
245
|
+
|
|
246
|
+
// 清空 shell 历史文件
|
|
247
|
+
if (fs.existsSync(SHELL_HISTORY_FILE)) {
|
|
248
|
+
fs.unlinkSync(SHELL_HISTORY_FILE);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(chalk.green('✅ Shell hook 已卸载'));
|
|
252
|
+
console.log(chalk.yellow('⚠️ 请重启终端使其生效'));
|
|
253
|
+
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 读取 shell 历史记录
|
|
259
|
+
*/
|
|
260
|
+
export function getShellHistory() {
|
|
261
|
+
const config = getConfig();
|
|
262
|
+
|
|
263
|
+
// 如果未启用 shell hook,返回空数组
|
|
264
|
+
if (!config.shellHook) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!fs.existsSync(SHELL_HISTORY_FILE)) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const content = fs.readFileSync(SHELL_HISTORY_FILE, 'utf-8');
|
|
274
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
275
|
+
|
|
276
|
+
return lines.map(line => {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(line);
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}).filter(Boolean);
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 从 pls history 中查找匹配的记录
|
|
290
|
+
* @param {string} prompt - pls 命令后面的 prompt 部分
|
|
291
|
+
* @returns {object|null} 匹配的 pls history 记录
|
|
292
|
+
*/
|
|
293
|
+
function findPlsHistoryMatch(prompt) {
|
|
294
|
+
const plsHistory = getHistory();
|
|
295
|
+
|
|
296
|
+
// 尝试精确匹配 userPrompt
|
|
297
|
+
for (const record of plsHistory) {
|
|
298
|
+
if (record.userPrompt === prompt) {
|
|
299
|
+
return record;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 尝试模糊匹配(处理引号等情况)
|
|
304
|
+
const normalizedPrompt = prompt.trim().replace(/^["']|["']$/g, '');
|
|
305
|
+
for (const record of plsHistory) {
|
|
306
|
+
if (record.userPrompt === normalizedPrompt) {
|
|
307
|
+
return record;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 格式化 shell 历史供 AI 使用
|
|
316
|
+
* 对于 pls 命令,会从 pls history 中查找对应的详细信息
|
|
317
|
+
*/
|
|
318
|
+
export function formatShellHistoryForAI() {
|
|
319
|
+
const history = getShellHistory();
|
|
320
|
+
|
|
321
|
+
if (history.length === 0) {
|
|
322
|
+
return '';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// pls 的子命令列表(这些不是 AI prompt)
|
|
326
|
+
const plsSubcommands = ['config', 'history', 'hook', 'help', '--help', '-h', '--version', '-v'];
|
|
327
|
+
|
|
328
|
+
const lines = history.map((item, index) => {
|
|
329
|
+
const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`;
|
|
330
|
+
|
|
331
|
+
// 检查是否是 pls 命令
|
|
332
|
+
const plsMatch = item.cmd.match(/^(pls|please)\s+(.+)$/);
|
|
333
|
+
if (plsMatch) {
|
|
334
|
+
let args = plsMatch[2];
|
|
335
|
+
|
|
336
|
+
// 去掉 --debug / -d 选项,获取真正的参数
|
|
337
|
+
args = args.replace(/^(--debug|-d)\s+/, '');
|
|
338
|
+
|
|
339
|
+
const firstArg = args.split(/\s+/)[0];
|
|
340
|
+
|
|
341
|
+
// 如果是子命令,当作普通命令处理
|
|
342
|
+
if (plsSubcommands.includes(firstArg)) {
|
|
343
|
+
return `${index + 1}. ${item.cmd} ${status}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 是 AI prompt,尝试从 pls history 查找详细信息
|
|
347
|
+
const prompt = args;
|
|
348
|
+
const plsRecord = findPlsHistoryMatch(prompt);
|
|
349
|
+
|
|
350
|
+
if (plsRecord) {
|
|
351
|
+
// 找到对应的 pls 记录,展示详细信息
|
|
352
|
+
if (plsRecord.reason === 'builtin') {
|
|
353
|
+
return `${index + 1}. [pls] "${prompt}" → 生成命令: ${plsRecord.command} (包含 builtin,未执行)`;
|
|
354
|
+
} else if (plsRecord.executed) {
|
|
355
|
+
const execStatus = plsRecord.exitCode === 0 ? '✓' : `✗ 退出码:${plsRecord.exitCode}`;
|
|
356
|
+
return `${index + 1}. [pls] "${prompt}" → 实际执行: ${plsRecord.command} ${execStatus}`;
|
|
357
|
+
} else {
|
|
358
|
+
return `${index + 1}. [pls] "${prompt}" → 生成命令: ${plsRecord.command} (用户取消执行)`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// 找不到记录,只显示原始命令
|
|
362
|
+
return `${index + 1}. [pls] "${prompt}" ${status}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 普通命令
|
|
366
|
+
return `${index + 1}. ${item.cmd} ${status}`;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return `【用户终端最近执行的命令】\n${lines.join('\n')}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 获取 hook 状态
|
|
374
|
+
*/
|
|
375
|
+
export function getHookStatus() {
|
|
376
|
+
const config = getConfig();
|
|
377
|
+
const shellType = detectShell();
|
|
378
|
+
const configPath = getShellConfigPath(shellType);
|
|
379
|
+
|
|
380
|
+
let installed = false;
|
|
381
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
382
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
383
|
+
installed = content.includes(HOOK_START_MARKER);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
enabled: config.shellHook,
|
|
388
|
+
installed,
|
|
389
|
+
shellType,
|
|
390
|
+
configPath,
|
|
391
|
+
historyFile: SHELL_HISTORY_FILE
|
|
392
|
+
};
|
|
393
|
+
}
|
package/src/sysinfo.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 检测包管理器
|
|
6
|
+
*/
|
|
7
|
+
function detectPackageManager() {
|
|
8
|
+
const managers = [
|
|
9
|
+
{ name: 'brew', command: 'brew' },
|
|
10
|
+
{ name: 'apt', command: 'apt-get' },
|
|
11
|
+
{ name: 'dnf', command: 'dnf' },
|
|
12
|
+
{ name: 'yum', command: 'yum' },
|
|
13
|
+
{ name: 'pacman', command: 'pacman' },
|
|
14
|
+
{ name: 'zypper', command: 'zypper' },
|
|
15
|
+
{ name: 'apk', command: 'apk' }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const mgr of managers) {
|
|
19
|
+
try {
|
|
20
|
+
execSync(`which ${mgr.command}`, { stdio: 'ignore' });
|
|
21
|
+
return mgr.name;
|
|
22
|
+
} catch {
|
|
23
|
+
// 继续检测下一个
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取当前 Shell
|
|
32
|
+
*/
|
|
33
|
+
function getCurrentShell() {
|
|
34
|
+
return process.env.SHELL || 'unknown';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 收集系统信息
|
|
39
|
+
*/
|
|
40
|
+
export function collectSystemInfo() {
|
|
41
|
+
return {
|
|
42
|
+
os: os.platform(),
|
|
43
|
+
arch: os.arch(),
|
|
44
|
+
shell: getCurrentShell(),
|
|
45
|
+
packageManager: detectPackageManager(),
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
user: os.userInfo().username
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 将系统信息格式化为字符串(供 AI 使用)
|
|
53
|
+
*/
|
|
54
|
+
export function formatSystemInfo() {
|
|
55
|
+
const info = collectSystemInfo();
|
|
56
|
+
return `OS: ${info.os}, Arch: ${info.arch}, Shell: ${info.shell}, PkgMgr: ${info.packageManager}, CWD: ${info.cwd}`;
|
|
57
|
+
}
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 主题配置 - 优雅的配色方案
|
|
2
|
+
export const theme = {
|
|
3
|
+
// 主色调
|
|
4
|
+
primary: '#00D9FF', // 青色 - 主要交互元素
|
|
5
|
+
secondary: '#A78BFA', // 紫色 - 次要元素
|
|
6
|
+
accent: '#F472B6', // 粉色 - 强调元素
|
|
7
|
+
|
|
8
|
+
// 状态色
|
|
9
|
+
success: '#10B981', // 绿色 - 成功
|
|
10
|
+
error: '#EF4444', // 红色 - 错误
|
|
11
|
+
warning: '#F59E0B', // 橙色 - 警告
|
|
12
|
+
info: '#3B82F6', // 蓝色 - 信息
|
|
13
|
+
|
|
14
|
+
// 文本色
|
|
15
|
+
text: {
|
|
16
|
+
primary: '#E5E7EB', // 主文本
|
|
17
|
+
secondary: '#9CA3AF', // 次要文本
|
|
18
|
+
muted: '#6B7280', // 弱化文本
|
|
19
|
+
dim: '#4B5563', // 暗淡文本
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// 边框和分隔
|
|
23
|
+
border: '#374151', // 边框色
|
|
24
|
+
divider: '#1F2937', // 分隔线
|
|
25
|
+
|
|
26
|
+
// 代码相关
|
|
27
|
+
code: {
|
|
28
|
+
background: '#1F2937',
|
|
29
|
+
text: '#E5E7EB',
|
|
30
|
+
keyword: '#C678DD',
|
|
31
|
+
string: '#98C379',
|
|
32
|
+
function: '#61AFEF',
|
|
33
|
+
comment: '#5C6370',
|
|
34
|
+
}
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
export type Theme = typeof theme
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 原生控制台输出工具函数
|
|
5
|
+
* 用于不需要 Ink 的场景,避免清屏和性能问题
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// 主题色
|
|
9
|
+
const colors = {
|
|
10
|
+
primary: '#00D9FF',
|
|
11
|
+
secondary: '#A78BFA',
|
|
12
|
+
accent: '#F472B6',
|
|
13
|
+
success: '#10B981',
|
|
14
|
+
error: '#EF4444',
|
|
15
|
+
warning: '#F59E0B',
|
|
16
|
+
info: '#3B82F6',
|
|
17
|
+
muted: '#6B7280',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 计算字符串的显示宽度(中文占2个宽度)
|
|
22
|
+
*/
|
|
23
|
+
export function getDisplayWidth(str) {
|
|
24
|
+
let width = 0
|
|
25
|
+
for (const char of str) {
|
|
26
|
+
if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
|
|
27
|
+
width += 2
|
|
28
|
+
} else {
|
|
29
|
+
width += 1
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return width
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 绘制命令框(原生版本)
|
|
37
|
+
*/
|
|
38
|
+
export function drawCommandBox(command, title = '生成命令') {
|
|
39
|
+
const lines = command.split('\n')
|
|
40
|
+
const titleWidth = getDisplayWidth(title)
|
|
41
|
+
const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)))
|
|
42
|
+
const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20)
|
|
43
|
+
|
|
44
|
+
const topPadding = boxWidth - titleWidth - 5
|
|
45
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐'
|
|
46
|
+
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘'
|
|
47
|
+
|
|
48
|
+
console.log(chalk.hex(colors.warning)(topBorder))
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const lineWidth = getDisplayWidth(line)
|
|
51
|
+
const padding = ' '.repeat(boxWidth - lineWidth - 4)
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.hex(colors.warning)('│ ') +
|
|
54
|
+
chalk.hex(colors.primary)(line) +
|
|
55
|
+
padding +
|
|
56
|
+
chalk.hex(colors.warning)(' │')
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.hex(colors.warning)(bottomBorder))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 格式化耗时
|
|
64
|
+
*/
|
|
65
|
+
export function formatDuration(ms) {
|
|
66
|
+
if (ms < 1000) {
|
|
67
|
+
return `${ms}ms`
|
|
68
|
+
}
|
|
69
|
+
return `${(ms / 1000).toFixed(2)}s`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 输出分隔线
|
|
74
|
+
*/
|
|
75
|
+
export function printSeparator(text = '输出', length = 38) {
|
|
76
|
+
const textPart = text ? ` ${text} ` : ''
|
|
77
|
+
const lineLength = Math.max(0, length - textPart.length)
|
|
78
|
+
const leftDashes = '─'.repeat(Math.floor(lineLength / 2))
|
|
79
|
+
const rightDashes = '─'.repeat(Math.ceil(lineLength / 2))
|
|
80
|
+
console.log(chalk.gray(`${leftDashes}${textPart}${rightDashes}`))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 输出成功消息
|
|
85
|
+
*/
|
|
86
|
+
export function success(message) {
|
|
87
|
+
console.log(chalk.hex(colors.success)('✓ ' + message))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 输出错误消息
|
|
92
|
+
*/
|
|
93
|
+
export function error(message) {
|
|
94
|
+
console.log(chalk.hex(colors.error)('✗ ' + message))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 输出警告消息
|
|
99
|
+
*/
|
|
100
|
+
export function warning(message) {
|
|
101
|
+
console.log(chalk.hex(colors.warning)('⚠️ ' + message))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 输出信息消息
|
|
106
|
+
*/
|
|
107
|
+
export function info(message) {
|
|
108
|
+
console.log(chalk.hex(colors.info)(message))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 输出灰色文本
|
|
113
|
+
*/
|
|
114
|
+
export function muted(message) {
|
|
115
|
+
console.log(chalk.hex(colors.muted)(message))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 输出标题
|
|
120
|
+
*/
|
|
121
|
+
export function title(message) {
|
|
122
|
+
console.log(chalk.bold(message))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 输出主色文本
|
|
127
|
+
*/
|
|
128
|
+
export function primary(message) {
|
|
129
|
+
console.log(chalk.hex(colors.primary)(message))
|
|
130
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react",
|
|
8
|
+
"jsxFactory": "React.createElement",
|
|
9
|
+
"jsxFragmentFactory": "React.Fragment",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"outDir": "./dist",
|
|
17
|
+
"types": ["node"],
|
|
18
|
+
"allowJs": true,
|
|
19
|
+
"noImplicitAny": false
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*", "bin/**/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|