@yivan-lab/pretty-please 1.1.0 → 1.3.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/README.md +390 -1
- package/bin/pls.tsx +1255 -123
- package/dist/bin/pls.js +1098 -103
- package/dist/package.json +4 -4
- package/dist/src/alias.d.ts +41 -0
- package/dist/src/alias.js +240 -0
- package/dist/src/chat-history.js +10 -1
- package/dist/src/components/Chat.js +54 -26
- package/dist/src/components/CodeColorizer.js +26 -20
- package/dist/src/components/CommandBox.js +19 -8
- package/dist/src/components/ConfirmationPrompt.js +2 -1
- package/dist/src/components/Duration.js +2 -1
- package/dist/src/components/InlineRenderer.js +2 -1
- package/dist/src/components/MarkdownDisplay.js +2 -1
- package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
- package/dist/src/components/MultiStepCommandGenerator.js +20 -10
- package/dist/src/components/TableRenderer.js +2 -1
- package/dist/src/config.d.ts +33 -3
- package/dist/src/config.js +83 -34
- package/dist/src/mastra-agent.d.ts +1 -0
- package/dist/src/mastra-agent.js +3 -11
- package/dist/src/mastra-chat.d.ts +13 -6
- package/dist/src/mastra-chat.js +31 -31
- package/dist/src/multi-step.d.ts +23 -7
- package/dist/src/multi-step.js +45 -26
- package/dist/src/prompts.d.ts +30 -4
- package/dist/src/prompts.js +218 -70
- package/dist/src/remote-history.d.ts +63 -0
- package/dist/src/remote-history.js +315 -0
- package/dist/src/remote.d.ts +113 -0
- package/dist/src/remote.js +634 -0
- package/dist/src/shell-hook.d.ts +58 -0
- package/dist/src/shell-hook.js +295 -26
- package/dist/src/ui/theme.d.ts +60 -23
- package/dist/src/ui/theme.js +544 -22
- package/dist/src/upgrade.d.ts +41 -0
- package/dist/src/upgrade.js +348 -0
- package/dist/src/utils/console.d.ts +4 -0
- package/dist/src/utils/console.js +89 -17
- package/package.json +4 -4
- package/src/alias.ts +301 -0
- package/src/chat-history.ts +11 -1
- package/src/components/Chat.tsx +71 -26
- package/src/components/CodeColorizer.tsx +27 -19
- package/src/components/CommandBox.tsx +26 -8
- package/src/components/ConfirmationPrompt.tsx +2 -1
- package/src/components/Duration.tsx +2 -1
- package/src/components/InlineRenderer.tsx +2 -1
- package/src/components/MarkdownDisplay.tsx +2 -1
- package/src/components/MultiStepCommandGenerator.tsx +25 -11
- package/src/components/TableRenderer.tsx +2 -1
- package/src/config.ts +126 -35
- package/src/mastra-agent.ts +3 -12
- package/src/mastra-chat.ts +40 -34
- package/src/multi-step.ts +62 -30
- package/src/prompts.ts +236 -78
- package/src/remote-history.ts +390 -0
- package/src/remote.ts +800 -0
- package/src/shell-hook.ts +339 -26
- package/src/ui/theme.ts +632 -23
- package/src/upgrade.ts +397 -0
- package/src/utils/console.ts +99 -17
package/src/shell-hook.ts
CHANGED
|
@@ -4,9 +4,21 @@ import os from 'os'
|
|
|
4
4
|
import chalk from 'chalk'
|
|
5
5
|
import { CONFIG_DIR, getConfig, setConfigValue } from './config.js'
|
|
6
6
|
import { getHistory } from './history.js'
|
|
7
|
+
import { getCurrentTheme } from './ui/theme.js'
|
|
8
|
+
|
|
9
|
+
// 获取主题颜色
|
|
10
|
+
function getColors() {
|
|
11
|
+
const theme = getCurrentTheme()
|
|
12
|
+
return {
|
|
13
|
+
primary: theme.primary,
|
|
14
|
+
success: theme.success,
|
|
15
|
+
error: theme.error,
|
|
16
|
+
warning: theme.warning,
|
|
17
|
+
secondary: theme.secondary,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
const SHELL_HISTORY_FILE = path.join(CONFIG_DIR, 'shell_history.jsonl')
|
|
9
|
-
const MAX_SHELL_HISTORY = 20
|
|
10
22
|
|
|
11
23
|
// Hook 标记,用于识别我们添加的内容
|
|
12
24
|
const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>'
|
|
@@ -73,6 +85,9 @@ export function getShellConfigPath(shellType: ShellType): string | null {
|
|
|
73
85
|
* 生成 zsh hook 脚本
|
|
74
86
|
*/
|
|
75
87
|
function generateZshHook(): string {
|
|
88
|
+
const config = getConfig()
|
|
89
|
+
const limit = config.shellHistoryLimit || 10 // 从配置读取
|
|
90
|
+
|
|
76
91
|
return `
|
|
77
92
|
${HOOK_START_MARKER}
|
|
78
93
|
# 记录命令到 pretty-please 历史
|
|
@@ -89,8 +104,8 @@ __pls_precmd() {
|
|
|
89
104
|
# 转义命令中的特殊字符
|
|
90
105
|
local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
91
106
|
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
|
|
92
|
-
# 保持文件不超过 ${
|
|
93
|
-
tail -n ${
|
|
107
|
+
# 保持文件不超过 ${limit} 行(从配置读取)
|
|
108
|
+
tail -n ${limit} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
|
|
94
109
|
unset __PLS_LAST_CMD
|
|
95
110
|
fi
|
|
96
111
|
}
|
|
@@ -106,6 +121,9 @@ ${HOOK_END_MARKER}
|
|
|
106
121
|
* 生成 bash hook 脚本
|
|
107
122
|
*/
|
|
108
123
|
function generateBashHook(): string {
|
|
124
|
+
const config = getConfig()
|
|
125
|
+
const limit = config.shellHistoryLimit || 10 // 从配置读取
|
|
126
|
+
|
|
109
127
|
return `
|
|
110
128
|
${HOOK_START_MARKER}
|
|
111
129
|
# 记录命令到 pretty-please 历史
|
|
@@ -117,7 +135,7 @@ __pls_prompt_command() {
|
|
|
117
135
|
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
118
136
|
local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
119
137
|
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
|
|
120
|
-
tail -n ${
|
|
138
|
+
tail -n ${limit} "${CONFIG_DIR}/shell_history.jsonl" > "${CONFIG_DIR}/shell_history.jsonl.tmp" && mv "${CONFIG_DIR}/shell_history.jsonl.tmp" "${CONFIG_DIR}/shell_history.jsonl"
|
|
121
139
|
fi
|
|
122
140
|
}
|
|
123
141
|
|
|
@@ -132,6 +150,9 @@ ${HOOK_END_MARKER}
|
|
|
132
150
|
* 生成 PowerShell hook 脚本
|
|
133
151
|
*/
|
|
134
152
|
function generatePowerShellHook(): string {
|
|
153
|
+
const config = getConfig()
|
|
154
|
+
const limit = config.shellHistoryLimit || 10 // 从配置读取
|
|
155
|
+
|
|
135
156
|
return `
|
|
136
157
|
${HOOK_START_MARKER}
|
|
137
158
|
# 记录命令到 pretty-please 历史
|
|
@@ -147,8 +168,8 @@ function __Pls_RecordCommand {
|
|
|
147
168
|
$escapedCmd = $lastCmd -replace '\\\\', '\\\\\\\\' -replace '"', '\\\\"'
|
|
148
169
|
$json = "{\`"cmd\`":\`"$escapedCmd\`",\`"exit\`":$exitCode,\`"time\`":\`"$timestamp\`"}"
|
|
149
170
|
Add-Content -Path "${CONFIG_DIR}/shell_history.jsonl" -Value $json
|
|
150
|
-
# 保持文件不超过 ${
|
|
151
|
-
$content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${
|
|
171
|
+
# 保持文件不超过 ${limit} 行(从配置读取)
|
|
172
|
+
$content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${limit}
|
|
152
173
|
$content | Set-Content "${CONFIG_DIR}/shell_history.jsonl"
|
|
153
174
|
}
|
|
154
175
|
}
|
|
@@ -186,15 +207,16 @@ function generateHookScript(shellType: ShellType): string | null {
|
|
|
186
207
|
export async function installShellHook(): Promise<boolean> {
|
|
187
208
|
const shellType = detectShell()
|
|
188
209
|
const configPath = getShellConfigPath(shellType)
|
|
210
|
+
const colors = getColors()
|
|
189
211
|
|
|
190
212
|
if (!configPath) {
|
|
191
|
-
console.log(chalk.
|
|
213
|
+
console.log(chalk.hex(colors.error)(`❌ 不支持的 shell 类型: ${shellType}`))
|
|
192
214
|
return false
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
const hookScript = generateHookScript(shellType)
|
|
196
218
|
if (!hookScript) {
|
|
197
|
-
console.log(chalk.
|
|
219
|
+
console.log(chalk.hex(colors.error)(`❌ 无法为 ${shellType} 生成 hook 脚本`))
|
|
198
220
|
return false
|
|
199
221
|
}
|
|
200
222
|
|
|
@@ -202,7 +224,7 @@ export async function installShellHook(): Promise<boolean> {
|
|
|
202
224
|
if (fs.existsSync(configPath)) {
|
|
203
225
|
const content = fs.readFileSync(configPath, 'utf-8')
|
|
204
226
|
if (content.includes(HOOK_START_MARKER)) {
|
|
205
|
-
console.log(chalk.
|
|
227
|
+
console.log(chalk.hex(colors.warning)('⚠️ Shell hook 已安装,跳过'))
|
|
206
228
|
setConfigValue('shellHook', true)
|
|
207
229
|
return true
|
|
208
230
|
}
|
|
@@ -226,9 +248,9 @@ export async function installShellHook(): Promise<boolean> {
|
|
|
226
248
|
// 更新配置
|
|
227
249
|
setConfigValue('shellHook', true)
|
|
228
250
|
|
|
229
|
-
console.log(chalk.
|
|
230
|
-
console.log(chalk.
|
|
231
|
-
console.log(chalk.
|
|
251
|
+
console.log(chalk.hex(colors.success)(`✅ Shell hook 已安装到: ${configPath}`))
|
|
252
|
+
console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或执行以下命令使其生效:'))
|
|
253
|
+
console.log(chalk.hex(colors.primary)(` source ${configPath}`))
|
|
232
254
|
|
|
233
255
|
return true
|
|
234
256
|
}
|
|
@@ -239,9 +261,10 @@ export async function installShellHook(): Promise<boolean> {
|
|
|
239
261
|
export function uninstallShellHook(): boolean {
|
|
240
262
|
const shellType = detectShell()
|
|
241
263
|
const configPath = getShellConfigPath(shellType)
|
|
264
|
+
const colors = getColors()
|
|
242
265
|
|
|
243
266
|
if (!configPath || !fs.existsSync(configPath)) {
|
|
244
|
-
console.log(chalk.
|
|
267
|
+
console.log(chalk.hex(colors.warning)('⚠️ 未找到 shell 配置文件'))
|
|
245
268
|
setConfigValue('shellHook', false)
|
|
246
269
|
return true
|
|
247
270
|
}
|
|
@@ -253,7 +276,7 @@ export function uninstallShellHook(): boolean {
|
|
|
253
276
|
const endIndex = content.indexOf(HOOK_END_MARKER)
|
|
254
277
|
|
|
255
278
|
if (startIndex === -1 || endIndex === -1) {
|
|
256
|
-
console.log(chalk.
|
|
279
|
+
console.log(chalk.hex(colors.warning)('⚠️ 未找到已安装的 hook'))
|
|
257
280
|
setConfigValue('shellHook', false)
|
|
258
281
|
return true
|
|
259
282
|
}
|
|
@@ -271,8 +294,8 @@ export function uninstallShellHook(): boolean {
|
|
|
271
294
|
fs.unlinkSync(SHELL_HISTORY_FILE)
|
|
272
295
|
}
|
|
273
296
|
|
|
274
|
-
console.log(chalk.
|
|
275
|
-
console.log(chalk.
|
|
297
|
+
console.log(chalk.hex(colors.success)('✅ Shell hook 已卸载'))
|
|
298
|
+
console.log(chalk.hex(colors.warning)('⚠️ 请重启终端使其生效'))
|
|
276
299
|
|
|
277
300
|
return true
|
|
278
301
|
}
|
|
@@ -299,7 +322,7 @@ export function getShellHistory(): ShellHistoryItem[] {
|
|
|
299
322
|
.split('\n')
|
|
300
323
|
.filter((line) => line.trim())
|
|
301
324
|
|
|
302
|
-
|
|
325
|
+
const allHistory = lines
|
|
303
326
|
.map((line) => {
|
|
304
327
|
try {
|
|
305
328
|
return JSON.parse(line) as ShellHistoryItem
|
|
@@ -308,6 +331,10 @@ export function getShellHistory(): ShellHistoryItem[] {
|
|
|
308
331
|
}
|
|
309
332
|
})
|
|
310
333
|
.filter((item): item is ShellHistoryItem => item !== null)
|
|
334
|
+
|
|
335
|
+
// 应用 shellHistoryLimit 限制:只返回最近的 N 条
|
|
336
|
+
const limit = config.shellHistoryLimit || 15
|
|
337
|
+
return allHistory.slice(-limit)
|
|
311
338
|
} catch {
|
|
312
339
|
return []
|
|
313
340
|
}
|
|
@@ -430,11 +457,12 @@ export function getHookStatus(): HookStatus {
|
|
|
430
457
|
export function displayShellHistory(): void {
|
|
431
458
|
const config = getConfig()
|
|
432
459
|
const history = getShellHistory()
|
|
460
|
+
const colors = getColors()
|
|
433
461
|
|
|
434
462
|
if (!config.shellHook) {
|
|
435
463
|
console.log('')
|
|
436
|
-
console.log(chalk.
|
|
437
|
-
console.log(chalk.gray('运行 ') + chalk.
|
|
464
|
+
console.log(chalk.hex(colors.warning)('⚠️ Shell Hook 未启用'))
|
|
465
|
+
console.log(chalk.gray('运行 ') + chalk.hex(colors.primary)('pls hook install') + chalk.gray(' 启用 Shell Hook'))
|
|
438
466
|
console.log('')
|
|
439
467
|
return
|
|
440
468
|
}
|
|
@@ -452,7 +480,7 @@ export function displayShellHistory(): void {
|
|
|
452
480
|
|
|
453
481
|
history.forEach((item, index) => {
|
|
454
482
|
const num = index + 1
|
|
455
|
-
const status = item.exit === 0 ? chalk.
|
|
483
|
+
const status = item.exit === 0 ? chalk.hex(colors.success)('✓') : chalk.hex(colors.error)(`✗ (${item.exit})`)
|
|
456
484
|
|
|
457
485
|
// 检查是否是 pls 命令
|
|
458
486
|
const isPls = item.cmd.startsWith('pls ') || item.cmd.startsWith('please ')
|
|
@@ -465,21 +493,21 @@ export function displayShellHistory(): void {
|
|
|
465
493
|
if (plsRecord && plsRecord.executed) {
|
|
466
494
|
// 检查用户是否修改了命令
|
|
467
495
|
if (plsRecord.userModified && plsRecord.aiGeneratedCommand) {
|
|
468
|
-
console.log(` ${chalk.
|
|
496
|
+
console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}"`)
|
|
469
497
|
console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(plsRecord.aiGeneratedCommand)}`)
|
|
470
498
|
console.log(
|
|
471
|
-
` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.
|
|
499
|
+
` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.hex(colors.warning)('(已修改)')}`
|
|
472
500
|
)
|
|
473
501
|
} else {
|
|
474
502
|
console.log(
|
|
475
|
-
` ${chalk.
|
|
503
|
+
` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}" → ${plsRecord.command} ${status}`
|
|
476
504
|
)
|
|
477
505
|
}
|
|
478
506
|
} else {
|
|
479
|
-
console.log(` ${chalk.
|
|
507
|
+
console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} ${args} ${status}`)
|
|
480
508
|
}
|
|
481
509
|
} else {
|
|
482
|
-
console.log(` ${chalk.
|
|
510
|
+
console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`)
|
|
483
511
|
}
|
|
484
512
|
})
|
|
485
513
|
|
|
@@ -489,6 +517,49 @@ export function displayShellHistory(): void {
|
|
|
489
517
|
console.log('')
|
|
490
518
|
}
|
|
491
519
|
|
|
520
|
+
/**
|
|
521
|
+
* 当 shellHistoryLimit 变化时,自动重装 Hook
|
|
522
|
+
* 返回是否成功重装
|
|
523
|
+
*/
|
|
524
|
+
export async function reinstallHookForLimitChange(oldLimit: number, newLimit: number): Promise<boolean> {
|
|
525
|
+
const config = getConfig()
|
|
526
|
+
|
|
527
|
+
// 只有在 hook 已启用时才重装
|
|
528
|
+
if (!config.shellHook) {
|
|
529
|
+
return false
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 值没有变化,不需要重装
|
|
533
|
+
if (oldLimit === newLimit) {
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const colors = getColors()
|
|
538
|
+
|
|
539
|
+
console.log('')
|
|
540
|
+
console.log(chalk.hex(colors.primary)(`检测到 shellHistoryLimit 变化 (${oldLimit} → ${newLimit})`))
|
|
541
|
+
console.log(chalk.hex(colors.primary)('正在更新 Shell Hook...'))
|
|
542
|
+
|
|
543
|
+
uninstallShellHook()
|
|
544
|
+
await installShellHook()
|
|
545
|
+
|
|
546
|
+
console.log('')
|
|
547
|
+
console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或运行以下命令使新配置生效:'))
|
|
548
|
+
|
|
549
|
+
const shellType = detectShell()
|
|
550
|
+
let configFile = '~/.zshrc'
|
|
551
|
+
if (shellType === 'bash') {
|
|
552
|
+
configFile = process.platform === 'darwin' ? '~/.bash_profile' : '~/.bashrc'
|
|
553
|
+
} else if (shellType === 'powershell') {
|
|
554
|
+
configFile = '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1'
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log(chalk.gray(` source ${configFile}`))
|
|
558
|
+
console.log('')
|
|
559
|
+
|
|
560
|
+
return true
|
|
561
|
+
}
|
|
562
|
+
|
|
492
563
|
/**
|
|
493
564
|
* 清空 shell 历史
|
|
494
565
|
*/
|
|
@@ -496,7 +567,249 @@ export function clearShellHistory(): void {
|
|
|
496
567
|
if (fs.existsSync(SHELL_HISTORY_FILE)) {
|
|
497
568
|
fs.unlinkSync(SHELL_HISTORY_FILE)
|
|
498
569
|
}
|
|
570
|
+
const colors = getColors()
|
|
499
571
|
console.log('')
|
|
500
|
-
console.log(chalk.
|
|
572
|
+
console.log(chalk.hex(colors.success)('✓ Shell 历史已清空'))
|
|
501
573
|
console.log('')
|
|
502
574
|
}
|
|
575
|
+
|
|
576
|
+
// ================== 远程 Shell Hook ==================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 生成远程 zsh hook 脚本
|
|
580
|
+
*/
|
|
581
|
+
function generateRemoteZshHook(): string {
|
|
582
|
+
const config = getConfig()
|
|
583
|
+
const limit = config.shellHistoryLimit || 10 // 从配置读取
|
|
584
|
+
|
|
585
|
+
return `
|
|
586
|
+
${HOOK_START_MARKER}
|
|
587
|
+
# 记录命令到 pretty-please 历史
|
|
588
|
+
__pls_preexec() {
|
|
589
|
+
__PLS_LAST_CMD="$1"
|
|
590
|
+
__PLS_CMD_START=$(date +%s)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
__pls_precmd() {
|
|
594
|
+
local exit_code=$?
|
|
595
|
+
if [[ -n "$__PLS_LAST_CMD" ]]; then
|
|
596
|
+
local end_time=$(date +%s)
|
|
597
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
598
|
+
# 确保目录存在
|
|
599
|
+
mkdir -p ~/.please
|
|
600
|
+
# 转义命令中的特殊字符
|
|
601
|
+
local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
602
|
+
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
|
|
603
|
+
# 保持文件不超过 ${limit} 行(从配置读取)
|
|
604
|
+
tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
|
|
605
|
+
unset __PLS_LAST_CMD
|
|
606
|
+
fi
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
autoload -Uz add-zsh-hook
|
|
610
|
+
add-zsh-hook preexec __pls_preexec
|
|
611
|
+
add-zsh-hook precmd __pls_precmd
|
|
612
|
+
${HOOK_END_MARKER}
|
|
613
|
+
`
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 生成远程 bash hook 脚本
|
|
618
|
+
*/
|
|
619
|
+
function generateRemoteBashHook(): string {
|
|
620
|
+
const config = getConfig()
|
|
621
|
+
const limit = config.shellHistoryLimit || 10 // 从配置读取
|
|
622
|
+
|
|
623
|
+
return `
|
|
624
|
+
${HOOK_START_MARKER}
|
|
625
|
+
# 记录命令到 pretty-please 历史
|
|
626
|
+
__pls_prompt_command() {
|
|
627
|
+
local exit_code=$?
|
|
628
|
+
local last_cmd=$(history 1 | sed 's/^ *[0-9]* *//')
|
|
629
|
+
if [[ -n "$last_cmd" && "$last_cmd" != "$__PLS_LAST_CMD" ]]; then
|
|
630
|
+
__PLS_LAST_CMD="$last_cmd"
|
|
631
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
632
|
+
# 确保目录存在
|
|
633
|
+
mkdir -p ~/.please
|
|
634
|
+
local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
635
|
+
echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
|
|
636
|
+
tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
|
|
637
|
+
fi
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if [[ ! "$PROMPT_COMMAND" =~ __pls_prompt_command ]]; then
|
|
641
|
+
PROMPT_COMMAND="__pls_prompt_command;\${PROMPT_COMMAND}"
|
|
642
|
+
fi
|
|
643
|
+
${HOOK_END_MARKER}
|
|
644
|
+
`
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 检测远程服务器的 shell 类型
|
|
649
|
+
*/
|
|
650
|
+
export async function detectRemoteShell(sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>): Promise<ShellType> {
|
|
651
|
+
try {
|
|
652
|
+
const result = await sshExecFn('basename "$SHELL"')
|
|
653
|
+
if (result.exitCode === 0) {
|
|
654
|
+
const shell = result.stdout.trim()
|
|
655
|
+
if (shell === 'zsh') return 'zsh'
|
|
656
|
+
if (shell === 'bash') return 'bash'
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
// 忽略错误
|
|
660
|
+
}
|
|
661
|
+
return 'bash' // 默认 bash
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 获取远程 shell 配置文件路径
|
|
666
|
+
*/
|
|
667
|
+
export function getRemoteShellConfigPath(shellType: ShellType): string {
|
|
668
|
+
switch (shellType) {
|
|
669
|
+
case 'zsh':
|
|
670
|
+
return '~/.zshrc'
|
|
671
|
+
case 'bash':
|
|
672
|
+
return '~/.bashrc'
|
|
673
|
+
default:
|
|
674
|
+
return '~/.bashrc'
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* 生成远程 hook 脚本
|
|
680
|
+
*/
|
|
681
|
+
export function generateRemoteHookScript(shellType: ShellType): string | null {
|
|
682
|
+
switch (shellType) {
|
|
683
|
+
case 'zsh':
|
|
684
|
+
return generateRemoteZshHook()
|
|
685
|
+
case 'bash':
|
|
686
|
+
return generateRemoteBashHook()
|
|
687
|
+
default:
|
|
688
|
+
return null
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 检查远程 hook 是否已安装
|
|
694
|
+
*/
|
|
695
|
+
export async function checkRemoteHookInstalled(
|
|
696
|
+
sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
|
|
697
|
+
configPath: string
|
|
698
|
+
): Promise<boolean> {
|
|
699
|
+
try {
|
|
700
|
+
const result = await sshExecFn(`grep -q "${HOOK_START_MARKER}" ${configPath} 2>/dev/null && echo "installed" || echo "not_installed"`)
|
|
701
|
+
return result.stdout.trim() === 'installed'
|
|
702
|
+
} catch {
|
|
703
|
+
return false
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 在远程服务器安装 shell hook
|
|
709
|
+
*/
|
|
710
|
+
export async function installRemoteShellHook(
|
|
711
|
+
sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
|
|
712
|
+
shellType: ShellType
|
|
713
|
+
): Promise<{ success: boolean; message: string }> {
|
|
714
|
+
const colors = getColors()
|
|
715
|
+
const configPath = getRemoteShellConfigPath(shellType)
|
|
716
|
+
const hookScript = generateRemoteHookScript(shellType)
|
|
717
|
+
|
|
718
|
+
if (!hookScript) {
|
|
719
|
+
return { success: false, message: chalk.hex(colors.error)(`不支持的 shell 类型: ${shellType}`) }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// 检查是否已安装
|
|
723
|
+
const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
|
|
724
|
+
if (installed) {
|
|
725
|
+
return { success: true, message: chalk.hex(colors.warning)('Shell hook 已安装,跳过') }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// 备份原配置文件
|
|
729
|
+
try {
|
|
730
|
+
await sshExecFn(`cp ${configPath} ${configPath}.pls-backup 2>/dev/null || true`)
|
|
731
|
+
} catch {
|
|
732
|
+
// 忽略备份错误
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// 安装 hook
|
|
736
|
+
// 使用 cat 和 heredoc 来追加内容
|
|
737
|
+
const escapedScript = hookScript.replace(/'/g, "'\"'\"'")
|
|
738
|
+
const installCmd = `echo '${escapedScript}' >> ${configPath}`
|
|
739
|
+
|
|
740
|
+
try {
|
|
741
|
+
const result = await sshExecFn(installCmd)
|
|
742
|
+
if (result.exitCode !== 0) {
|
|
743
|
+
return { success: false, message: chalk.hex(colors.error)(`安装失败: ${result.stdout}`) }
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 确保 ~/.please 目录存在
|
|
747
|
+
await sshExecFn('mkdir -p ~/.please')
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
success: true,
|
|
751
|
+
message: chalk.hex(colors.success)(`Shell hook 已安装到 ${configPath}`),
|
|
752
|
+
}
|
|
753
|
+
} catch (error) {
|
|
754
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
755
|
+
return { success: false, message: chalk.hex(colors.error)(`安装失败: ${message}`) }
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* 从远程服务器卸载 shell hook
|
|
761
|
+
*/
|
|
762
|
+
export async function uninstallRemoteShellHook(
|
|
763
|
+
sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>,
|
|
764
|
+
shellType: ShellType
|
|
765
|
+
): Promise<{ success: boolean; message: string }> {
|
|
766
|
+
const colors = getColors()
|
|
767
|
+
const configPath = getRemoteShellConfigPath(shellType)
|
|
768
|
+
|
|
769
|
+
// 检查是否已安装
|
|
770
|
+
const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
|
|
771
|
+
if (!installed) {
|
|
772
|
+
return { success: true, message: chalk.hex(colors.warning)('Shell hook 未安装,跳过') }
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// 使用 sed 删除 hook 代码块
|
|
776
|
+
// 注意:需要处理特殊字符
|
|
777
|
+
const startMarkerEscaped = HOOK_START_MARKER.replace(/[[\]]/g, '\\$&')
|
|
778
|
+
const endMarkerEscaped = HOOK_END_MARKER.replace(/[[\]]/g, '\\$&')
|
|
779
|
+
|
|
780
|
+
// 在 macOS 和 Linux 上 sed -i 行为不同,使用 sed + 临时文件
|
|
781
|
+
const uninstallCmd = `
|
|
782
|
+
sed '/${startMarkerEscaped}/,/${endMarkerEscaped}/d' ${configPath} > ${configPath}.tmp && mv ${configPath}.tmp ${configPath}
|
|
783
|
+
`
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const result = await sshExecFn(uninstallCmd)
|
|
787
|
+
if (result.exitCode !== 0) {
|
|
788
|
+
return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${result.stdout}`) }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
success: true,
|
|
793
|
+
message: chalk.hex(colors.success)('Shell hook 已卸载'),
|
|
794
|
+
}
|
|
795
|
+
} catch (error) {
|
|
796
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
797
|
+
return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${message}`) }
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* 获取远程 hook 状态
|
|
803
|
+
*/
|
|
804
|
+
export async function getRemoteHookStatus(
|
|
805
|
+
sshExecFn: (cmd: string) => Promise<{ stdout: string; exitCode: number }>
|
|
806
|
+
): Promise<{ installed: boolean; shellType: ShellType; configPath: string }> {
|
|
807
|
+
// 检测 shell 类型
|
|
808
|
+
const shellType = await detectRemoteShell(sshExecFn)
|
|
809
|
+
const configPath = getRemoteShellConfigPath(shellType)
|
|
810
|
+
|
|
811
|
+
// 检查是否已安装
|
|
812
|
+
const installed = await checkRemoteHookInstalled(sshExecFn, configPath)
|
|
813
|
+
|
|
814
|
+
return { installed, shellType, configPath }
|
|
815
|
+
}
|