@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.
Files changed (62) hide show
  1. package/README.md +390 -1
  2. package/bin/pls.tsx +1255 -123
  3. package/dist/bin/pls.js +1098 -103
  4. package/dist/package.json +4 -4
  5. package/dist/src/alias.d.ts +41 -0
  6. package/dist/src/alias.js +240 -0
  7. package/dist/src/chat-history.js +10 -1
  8. package/dist/src/components/Chat.js +54 -26
  9. package/dist/src/components/CodeColorizer.js +26 -20
  10. package/dist/src/components/CommandBox.js +19 -8
  11. package/dist/src/components/ConfirmationPrompt.js +2 -1
  12. package/dist/src/components/Duration.js +2 -1
  13. package/dist/src/components/InlineRenderer.js +2 -1
  14. package/dist/src/components/MarkdownDisplay.js +2 -1
  15. package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
  16. package/dist/src/components/MultiStepCommandGenerator.js +20 -10
  17. package/dist/src/components/TableRenderer.js +2 -1
  18. package/dist/src/config.d.ts +33 -3
  19. package/dist/src/config.js +83 -34
  20. package/dist/src/mastra-agent.d.ts +1 -0
  21. package/dist/src/mastra-agent.js +3 -11
  22. package/dist/src/mastra-chat.d.ts +13 -6
  23. package/dist/src/mastra-chat.js +31 -31
  24. package/dist/src/multi-step.d.ts +23 -7
  25. package/dist/src/multi-step.js +45 -26
  26. package/dist/src/prompts.d.ts +30 -4
  27. package/dist/src/prompts.js +218 -70
  28. package/dist/src/remote-history.d.ts +63 -0
  29. package/dist/src/remote-history.js +315 -0
  30. package/dist/src/remote.d.ts +113 -0
  31. package/dist/src/remote.js +634 -0
  32. package/dist/src/shell-hook.d.ts +58 -0
  33. package/dist/src/shell-hook.js +295 -26
  34. package/dist/src/ui/theme.d.ts +60 -23
  35. package/dist/src/ui/theme.js +544 -22
  36. package/dist/src/upgrade.d.ts +41 -0
  37. package/dist/src/upgrade.js +348 -0
  38. package/dist/src/utils/console.d.ts +4 -0
  39. package/dist/src/utils/console.js +89 -17
  40. package/package.json +4 -4
  41. package/src/alias.ts +301 -0
  42. package/src/chat-history.ts +11 -1
  43. package/src/components/Chat.tsx +71 -26
  44. package/src/components/CodeColorizer.tsx +27 -19
  45. package/src/components/CommandBox.tsx +26 -8
  46. package/src/components/ConfirmationPrompt.tsx +2 -1
  47. package/src/components/Duration.tsx +2 -1
  48. package/src/components/InlineRenderer.tsx +2 -1
  49. package/src/components/MarkdownDisplay.tsx +2 -1
  50. package/src/components/MultiStepCommandGenerator.tsx +25 -11
  51. package/src/components/TableRenderer.tsx +2 -1
  52. package/src/config.ts +126 -35
  53. package/src/mastra-agent.ts +3 -12
  54. package/src/mastra-chat.ts +40 -34
  55. package/src/multi-step.ts +62 -30
  56. package/src/prompts.ts +236 -78
  57. package/src/remote-history.ts +390 -0
  58. package/src/remote.ts +800 -0
  59. package/src/shell-hook.ts +339 -26
  60. package/src/ui/theme.ts +632 -23
  61. package/src/upgrade.ts +397 -0
  62. package/src/utils/console.ts +99 -17
@@ -4,8 +4,19 @@ 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
+ function getColors() {
10
+ const theme = getCurrentTheme();
11
+ return {
12
+ primary: theme.primary,
13
+ success: theme.success,
14
+ error: theme.error,
15
+ warning: theme.warning,
16
+ secondary: theme.secondary,
17
+ };
18
+ }
7
19
  const SHELL_HISTORY_FILE = path.join(CONFIG_DIR, 'shell_history.jsonl');
8
- const MAX_SHELL_HISTORY = 20;
9
20
  // Hook 标记,用于识别我们添加的内容
10
21
  const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>';
11
22
  const HOOK_END_MARKER = '# <<< pretty-please shell hook <<<';
@@ -48,6 +59,8 @@ export function getShellConfigPath(shellType) {
48
59
  * 生成 zsh hook 脚本
49
60
  */
50
61
  function generateZshHook() {
62
+ const config = getConfig();
63
+ const limit = config.shellHistoryLimit || 10; // 从配置读取
51
64
  return `
52
65
  ${HOOK_START_MARKER}
53
66
  # 记录命令到 pretty-please 历史
@@ -64,8 +77,8 @@ __pls_precmd() {
64
77
  # 转义命令中的特殊字符
65
78
  local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
66
79
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
67
- # 保持文件不超过 ${MAX_SHELL_HISTORY}
68
- 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"
80
+ # 保持文件不超过 ${limit} 行(从配置读取)
81
+ 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"
69
82
  unset __PLS_LAST_CMD
70
83
  fi
71
84
  }
@@ -80,6 +93,8 @@ ${HOOK_END_MARKER}
80
93
  * 生成 bash hook 脚本
81
94
  */
82
95
  function generateBashHook() {
96
+ const config = getConfig();
97
+ const limit = config.shellHistoryLimit || 10; // 从配置读取
83
98
  return `
84
99
  ${HOOK_START_MARKER}
85
100
  # 记录命令到 pretty-please 历史
@@ -91,7 +106,7 @@ __pls_prompt_command() {
91
106
  local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
92
107
  local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
93
108
  echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> "${CONFIG_DIR}/shell_history.jsonl"
94
- 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"
109
+ 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"
95
110
  fi
96
111
  }
97
112
 
@@ -105,6 +120,8 @@ ${HOOK_END_MARKER}
105
120
  * 生成 PowerShell hook 脚本
106
121
  */
107
122
  function generatePowerShellHook() {
123
+ const config = getConfig();
124
+ const limit = config.shellHistoryLimit || 10; // 从配置读取
108
125
  return `
109
126
  ${HOOK_START_MARKER}
110
127
  # 记录命令到 pretty-please 历史
@@ -120,8 +137,8 @@ function __Pls_RecordCommand {
120
137
  $escapedCmd = $lastCmd -replace '\\\\', '\\\\\\\\' -replace '"', '\\\\"'
121
138
  $json = "{\`"cmd\`":\`"$escapedCmd\`",\`"exit\`":$exitCode,\`"time\`":\`"$timestamp\`"}"
122
139
  Add-Content -Path "${CONFIG_DIR}/shell_history.jsonl" -Value $json
123
- # 保持文件不超过 ${MAX_SHELL_HISTORY}
124
- $content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${MAX_SHELL_HISTORY}
140
+ # 保持文件不超过 ${limit} 行(从配置读取)
141
+ $content = Get-Content "${CONFIG_DIR}/shell_history.jsonl" -Tail ${limit}
125
142
  $content | Set-Content "${CONFIG_DIR}/shell_history.jsonl"
126
143
  }
127
144
  }
@@ -157,20 +174,21 @@ function generateHookScript(shellType) {
157
174
  export async function installShellHook() {
158
175
  const shellType = detectShell();
159
176
  const configPath = getShellConfigPath(shellType);
177
+ const colors = getColors();
160
178
  if (!configPath) {
161
- console.log(chalk.red(`❌ 不支持的 shell 类型: ${shellType}`));
179
+ console.log(chalk.hex(colors.error)(`❌ 不支持的 shell 类型: ${shellType}`));
162
180
  return false;
163
181
  }
164
182
  const hookScript = generateHookScript(shellType);
165
183
  if (!hookScript) {
166
- console.log(chalk.red(`❌ 无法为 ${shellType} 生成 hook 脚本`));
184
+ console.log(chalk.hex(colors.error)(`❌ 无法为 ${shellType} 生成 hook 脚本`));
167
185
  return false;
168
186
  }
169
187
  // 检查是否已安装
170
188
  if (fs.existsSync(configPath)) {
171
189
  const content = fs.readFileSync(configPath, 'utf-8');
172
190
  if (content.includes(HOOK_START_MARKER)) {
173
- console.log(chalk.yellow('⚠️ Shell hook 已安装,跳过'));
191
+ console.log(chalk.hex(colors.warning)('⚠️ Shell hook 已安装,跳过'));
174
192
  setConfigValue('shellHook', true);
175
193
  return true;
176
194
  }
@@ -189,9 +207,9 @@ export async function installShellHook() {
189
207
  fs.appendFileSync(configPath, hookScript);
190
208
  // 更新配置
191
209
  setConfigValue('shellHook', true);
192
- console.log(chalk.green(`✅ Shell hook 已安装到: ${configPath}`));
193
- console.log(chalk.yellow('⚠️ 请重启终端或执行以下命令使其生效:'));
194
- console.log(chalk.cyan(` source ${configPath}`));
210
+ console.log(chalk.hex(colors.success)(`✅ Shell hook 已安装到: ${configPath}`));
211
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或执行以下命令使其生效:'));
212
+ console.log(chalk.hex(colors.primary)(` source ${configPath}`));
195
213
  return true;
196
214
  }
197
215
  /**
@@ -200,8 +218,9 @@ export async function installShellHook() {
200
218
  export function uninstallShellHook() {
201
219
  const shellType = detectShell();
202
220
  const configPath = getShellConfigPath(shellType);
221
+ const colors = getColors();
203
222
  if (!configPath || !fs.existsSync(configPath)) {
204
- console.log(chalk.yellow('⚠️ 未找到 shell 配置文件'));
223
+ console.log(chalk.hex(colors.warning)('⚠️ 未找到 shell 配置文件'));
205
224
  setConfigValue('shellHook', false);
206
225
  return true;
207
226
  }
@@ -210,7 +229,7 @@ export function uninstallShellHook() {
210
229
  const startIndex = content.indexOf(HOOK_START_MARKER);
211
230
  const endIndex = content.indexOf(HOOK_END_MARKER);
212
231
  if (startIndex === -1 || endIndex === -1) {
213
- console.log(chalk.yellow('⚠️ 未找到已安装的 hook'));
232
+ console.log(chalk.hex(colors.warning)('⚠️ 未找到已安装的 hook'));
214
233
  setConfigValue('shellHook', false);
215
234
  return true;
216
235
  }
@@ -224,8 +243,8 @@ export function uninstallShellHook() {
224
243
  if (fs.existsSync(SHELL_HISTORY_FILE)) {
225
244
  fs.unlinkSync(SHELL_HISTORY_FILE);
226
245
  }
227
- console.log(chalk.green('✅ Shell hook 已卸载'));
228
- console.log(chalk.yellow('⚠️ 请重启终端使其生效'));
246
+ console.log(chalk.hex(colors.success)('✅ Shell hook 已卸载'));
247
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端使其生效'));
229
248
  return true;
230
249
  }
231
250
  /**
@@ -246,7 +265,7 @@ export function getShellHistory() {
246
265
  .trim()
247
266
  .split('\n')
248
267
  .filter((line) => line.trim());
249
- return lines
268
+ const allHistory = lines
250
269
  .map((line) => {
251
270
  try {
252
271
  return JSON.parse(line);
@@ -256,6 +275,9 @@ export function getShellHistory() {
256
275
  }
257
276
  })
258
277
  .filter((item) => item !== null);
278
+ // 应用 shellHistoryLimit 限制:只返回最近的 N 条
279
+ const limit = config.shellHistoryLimit || 15;
280
+ return allHistory.slice(-limit);
259
281
  }
260
282
  catch {
261
283
  return [];
@@ -361,10 +383,11 @@ export function getHookStatus() {
361
383
  export function displayShellHistory() {
362
384
  const config = getConfig();
363
385
  const history = getShellHistory();
386
+ const colors = getColors();
364
387
  if (!config.shellHook) {
365
388
  console.log('');
366
- console.log(chalk.yellow('⚠️ Shell Hook 未启用'));
367
- console.log(chalk.gray('运行 ') + chalk.cyan('pls hook install') + chalk.gray(' 启用 Shell Hook'));
389
+ console.log(chalk.hex(colors.warning)('⚠️ Shell Hook 未启用'));
390
+ console.log(chalk.gray('运行 ') + chalk.hex(colors.primary)('pls hook install') + chalk.gray(' 启用 Shell Hook'));
368
391
  console.log('');
369
392
  return;
370
393
  }
@@ -379,7 +402,7 @@ export function displayShellHistory() {
379
402
  console.log(chalk.gray('━'.repeat(50)));
380
403
  history.forEach((item, index) => {
381
404
  const num = index + 1;
382
- const status = item.exit === 0 ? chalk.green('✓') : chalk.red(`✗ (${item.exit})`);
405
+ const status = item.exit === 0 ? chalk.hex(colors.success)('✓') : chalk.hex(colors.error)(`✗ (${item.exit})`);
383
406
  // 检查是否是 pls 命令
384
407
  const isPls = item.cmd.startsWith('pls ') || item.cmd.startsWith('please ');
385
408
  if (isPls) {
@@ -389,20 +412,20 @@ export function displayShellHistory() {
389
412
  if (plsRecord && plsRecord.executed) {
390
413
  // 检查用户是否修改了命令
391
414
  if (plsRecord.userModified && plsRecord.aiGeneratedCommand) {
392
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}"`);
415
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}"`);
393
416
  console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(plsRecord.aiGeneratedCommand)}`);
394
- console.log(` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.yellow('(已修改)')}`);
417
+ console.log(` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.hex(colors.warning)('(已修改)')}`);
395
418
  }
396
419
  else {
397
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}" → ${plsRecord.command} ${status}`);
420
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} "${args}" → ${plsRecord.command} ${status}`);
398
421
  }
399
422
  }
400
423
  else {
401
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} ${args} ${status}`);
424
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${chalk.hex(colors.secondary)('[pls]')} ${args} ${status}`);
402
425
  }
403
426
  }
404
427
  else {
405
- console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`);
428
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`);
406
429
  }
407
430
  });
408
431
  console.log(chalk.gray('━'.repeat(50)));
@@ -410,6 +433,40 @@ export function displayShellHistory() {
410
433
  console.log(chalk.gray(`文件: ${SHELL_HISTORY_FILE}`));
411
434
  console.log('');
412
435
  }
436
+ /**
437
+ * 当 shellHistoryLimit 变化时,自动重装 Hook
438
+ * 返回是否成功重装
439
+ */
440
+ export async function reinstallHookForLimitChange(oldLimit, newLimit) {
441
+ const config = getConfig();
442
+ // 只有在 hook 已启用时才重装
443
+ if (!config.shellHook) {
444
+ return false;
445
+ }
446
+ // 值没有变化,不需要重装
447
+ if (oldLimit === newLimit) {
448
+ return false;
449
+ }
450
+ const colors = getColors();
451
+ console.log('');
452
+ console.log(chalk.hex(colors.primary)(`检测到 shellHistoryLimit 变化 (${oldLimit} → ${newLimit})`));
453
+ console.log(chalk.hex(colors.primary)('正在更新 Shell Hook...'));
454
+ uninstallShellHook();
455
+ await installShellHook();
456
+ console.log('');
457
+ console.log(chalk.hex(colors.warning)('⚠️ 请重启终端或运行以下命令使新配置生效:'));
458
+ const shellType = detectShell();
459
+ let configFile = '~/.zshrc';
460
+ if (shellType === 'bash') {
461
+ configFile = process.platform === 'darwin' ? '~/.bash_profile' : '~/.bashrc';
462
+ }
463
+ else if (shellType === 'powershell') {
464
+ configFile = '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1';
465
+ }
466
+ console.log(chalk.gray(` source ${configFile}`));
467
+ console.log('');
468
+ return true;
469
+ }
413
470
  /**
414
471
  * 清空 shell 历史
415
472
  */
@@ -417,7 +474,219 @@ export function clearShellHistory() {
417
474
  if (fs.existsSync(SHELL_HISTORY_FILE)) {
418
475
  fs.unlinkSync(SHELL_HISTORY_FILE);
419
476
  }
477
+ const colors = getColors();
420
478
  console.log('');
421
- console.log(chalk.green('✓ Shell 历史已清空'));
479
+ console.log(chalk.hex(colors.success)('✓ Shell 历史已清空'));
422
480
  console.log('');
423
481
  }
482
+ // ================== 远程 Shell Hook ==================
483
+ /**
484
+ * 生成远程 zsh hook 脚本
485
+ */
486
+ function generateRemoteZshHook() {
487
+ const config = getConfig();
488
+ const limit = config.shellHistoryLimit || 10; // 从配置读取
489
+ return `
490
+ ${HOOK_START_MARKER}
491
+ # 记录命令到 pretty-please 历史
492
+ __pls_preexec() {
493
+ __PLS_LAST_CMD="$1"
494
+ __PLS_CMD_START=$(date +%s)
495
+ }
496
+
497
+ __pls_precmd() {
498
+ local exit_code=$?
499
+ if [[ -n "$__PLS_LAST_CMD" ]]; then
500
+ local end_time=$(date +%s)
501
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
502
+ # 确保目录存在
503
+ mkdir -p ~/.please
504
+ # 转义命令中的特殊字符
505
+ local escaped_cmd=$(echo "$__PLS_LAST_CMD" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
506
+ echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
507
+ # 保持文件不超过 ${limit} 行(从配置读取)
508
+ tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
509
+ unset __PLS_LAST_CMD
510
+ fi
511
+ }
512
+
513
+ autoload -Uz add-zsh-hook
514
+ add-zsh-hook preexec __pls_preexec
515
+ add-zsh-hook precmd __pls_precmd
516
+ ${HOOK_END_MARKER}
517
+ `;
518
+ }
519
+ /**
520
+ * 生成远程 bash hook 脚本
521
+ */
522
+ function generateRemoteBashHook() {
523
+ const config = getConfig();
524
+ const limit = config.shellHistoryLimit || 10; // 从配置读取
525
+ return `
526
+ ${HOOK_START_MARKER}
527
+ # 记录命令到 pretty-please 历史
528
+ __pls_prompt_command() {
529
+ local exit_code=$?
530
+ local last_cmd=$(history 1 | sed 's/^ *[0-9]* *//')
531
+ if [[ -n "$last_cmd" && "$last_cmd" != "$__PLS_LAST_CMD" ]]; then
532
+ __PLS_LAST_CMD="$last_cmd"
533
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
534
+ # 确保目录存在
535
+ mkdir -p ~/.please
536
+ local escaped_cmd=$(echo "$last_cmd" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
537
+ echo "{\\"cmd\\":\\"$escaped_cmd\\",\\"exit\\":$exit_code,\\"time\\":\\"$timestamp\\"}" >> ~/.please/shell_history.jsonl
538
+ tail -n ${limit} ~/.please/shell_history.jsonl > ~/.please/shell_history.jsonl.tmp && mv ~/.please/shell_history.jsonl.tmp ~/.please/shell_history.jsonl
539
+ fi
540
+ }
541
+
542
+ if [[ ! "$PROMPT_COMMAND" =~ __pls_prompt_command ]]; then
543
+ PROMPT_COMMAND="__pls_prompt_command;\${PROMPT_COMMAND}"
544
+ fi
545
+ ${HOOK_END_MARKER}
546
+ `;
547
+ }
548
+ /**
549
+ * 检测远程服务器的 shell 类型
550
+ */
551
+ export async function detectRemoteShell(sshExecFn) {
552
+ try {
553
+ const result = await sshExecFn('basename "$SHELL"');
554
+ if (result.exitCode === 0) {
555
+ const shell = result.stdout.trim();
556
+ if (shell === 'zsh')
557
+ return 'zsh';
558
+ if (shell === 'bash')
559
+ return 'bash';
560
+ }
561
+ }
562
+ catch {
563
+ // 忽略错误
564
+ }
565
+ return 'bash'; // 默认 bash
566
+ }
567
+ /**
568
+ * 获取远程 shell 配置文件路径
569
+ */
570
+ export function getRemoteShellConfigPath(shellType) {
571
+ switch (shellType) {
572
+ case 'zsh':
573
+ return '~/.zshrc';
574
+ case 'bash':
575
+ return '~/.bashrc';
576
+ default:
577
+ return '~/.bashrc';
578
+ }
579
+ }
580
+ /**
581
+ * 生成远程 hook 脚本
582
+ */
583
+ export function generateRemoteHookScript(shellType) {
584
+ switch (shellType) {
585
+ case 'zsh':
586
+ return generateRemoteZshHook();
587
+ case 'bash':
588
+ return generateRemoteBashHook();
589
+ default:
590
+ return null;
591
+ }
592
+ }
593
+ /**
594
+ * 检查远程 hook 是否已安装
595
+ */
596
+ export async function checkRemoteHookInstalled(sshExecFn, configPath) {
597
+ try {
598
+ const result = await sshExecFn(`grep -q "${HOOK_START_MARKER}" ${configPath} 2>/dev/null && echo "installed" || echo "not_installed"`);
599
+ return result.stdout.trim() === 'installed';
600
+ }
601
+ catch {
602
+ return false;
603
+ }
604
+ }
605
+ /**
606
+ * 在远程服务器安装 shell hook
607
+ */
608
+ export async function installRemoteShellHook(sshExecFn, shellType) {
609
+ const colors = getColors();
610
+ const configPath = getRemoteShellConfigPath(shellType);
611
+ const hookScript = generateRemoteHookScript(shellType);
612
+ if (!hookScript) {
613
+ return { success: false, message: chalk.hex(colors.error)(`不支持的 shell 类型: ${shellType}`) };
614
+ }
615
+ // 检查是否已安装
616
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath);
617
+ if (installed) {
618
+ return { success: true, message: chalk.hex(colors.warning)('Shell hook 已安装,跳过') };
619
+ }
620
+ // 备份原配置文件
621
+ try {
622
+ await sshExecFn(`cp ${configPath} ${configPath}.pls-backup 2>/dev/null || true`);
623
+ }
624
+ catch {
625
+ // 忽略备份错误
626
+ }
627
+ // 安装 hook
628
+ // 使用 cat 和 heredoc 来追加内容
629
+ const escapedScript = hookScript.replace(/'/g, "'\"'\"'");
630
+ const installCmd = `echo '${escapedScript}' >> ${configPath}`;
631
+ try {
632
+ const result = await sshExecFn(installCmd);
633
+ if (result.exitCode !== 0) {
634
+ return { success: false, message: chalk.hex(colors.error)(`安装失败: ${result.stdout}`) };
635
+ }
636
+ // 确保 ~/.please 目录存在
637
+ await sshExecFn('mkdir -p ~/.please');
638
+ return {
639
+ success: true,
640
+ message: chalk.hex(colors.success)(`Shell hook 已安装到 ${configPath}`),
641
+ };
642
+ }
643
+ catch (error) {
644
+ const message = error instanceof Error ? error.message : String(error);
645
+ return { success: false, message: chalk.hex(colors.error)(`安装失败: ${message}`) };
646
+ }
647
+ }
648
+ /**
649
+ * 从远程服务器卸载 shell hook
650
+ */
651
+ export async function uninstallRemoteShellHook(sshExecFn, shellType) {
652
+ const colors = getColors();
653
+ const configPath = getRemoteShellConfigPath(shellType);
654
+ // 检查是否已安装
655
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath);
656
+ if (!installed) {
657
+ return { success: true, message: chalk.hex(colors.warning)('Shell hook 未安装,跳过') };
658
+ }
659
+ // 使用 sed 删除 hook 代码块
660
+ // 注意:需要处理特殊字符
661
+ const startMarkerEscaped = HOOK_START_MARKER.replace(/[[\]]/g, '\\$&');
662
+ const endMarkerEscaped = HOOK_END_MARKER.replace(/[[\]]/g, '\\$&');
663
+ // 在 macOS 和 Linux 上 sed -i 行为不同,使用 sed + 临时文件
664
+ const uninstallCmd = `
665
+ sed '/${startMarkerEscaped}/,/${endMarkerEscaped}/d' ${configPath} > ${configPath}.tmp && mv ${configPath}.tmp ${configPath}
666
+ `;
667
+ try {
668
+ const result = await sshExecFn(uninstallCmd);
669
+ if (result.exitCode !== 0) {
670
+ return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${result.stdout}`) };
671
+ }
672
+ return {
673
+ success: true,
674
+ message: chalk.hex(colors.success)('Shell hook 已卸载'),
675
+ };
676
+ }
677
+ catch (error) {
678
+ const message = error instanceof Error ? error.message : String(error);
679
+ return { success: false, message: chalk.hex(colors.error)(`卸载失败: ${message}`) };
680
+ }
681
+ }
682
+ /**
683
+ * 获取远程 hook 状态
684
+ */
685
+ export async function getRemoteHookStatus(sshExecFn) {
686
+ // 检测 shell 类型
687
+ const shellType = await detectRemoteShell(sshExecFn);
688
+ const configPath = getRemoteShellConfigPath(shellType);
689
+ // 检查是否已安装
690
+ const installed = await checkRemoteHookInstalled(sshExecFn, configPath);
691
+ return { installed, shellType, configPath };
692
+ }
@@ -1,26 +1,63 @@
1
- export declare const theme: {
2
- readonly primary: "#00D9FF";
3
- readonly secondary: "#A78BFA";
4
- readonly accent: "#F472B6";
5
- readonly success: "#10B981";
6
- readonly error: "#EF4444";
7
- readonly warning: "#F59E0B";
8
- readonly info: "#3B82F6";
9
- readonly text: {
10
- readonly primary: "#E5E7EB";
11
- readonly secondary: "#9CA3AF";
12
- readonly muted: "#6B7280";
13
- readonly dim: "#4B5563";
1
+ export type ThemeName = string;
2
+ export type BuiltinThemeName = 'dark' | 'light' | 'nord' | 'dracula' | 'retro' | 'contrast' | 'monokai';
3
+ export interface Theme {
4
+ primary: string;
5
+ secondary: string;
6
+ accent: string;
7
+ success: string;
8
+ error: string;
9
+ warning: string;
10
+ info: string;
11
+ text: {
12
+ primary: string;
13
+ secondary: string;
14
+ muted: string;
15
+ dim: string;
14
16
  };
15
- readonly border: "#374151";
16
- readonly divider: "#1F2937";
17
- readonly code: {
18
- readonly background: "#1F2937";
19
- readonly text: "#E5E7EB";
20
- readonly keyword: "#C678DD";
21
- readonly string: "#98C379";
22
- readonly function: "#61AFEF";
23
- readonly comment: "#5C6370";
17
+ border: string;
18
+ divider: string;
19
+ code: {
20
+ background: string;
21
+ text: string;
22
+ keyword: string;
23
+ string: string;
24
+ function: string;
25
+ comment: string;
24
26
  };
27
+ }
28
+ export interface ThemeMetadata {
29
+ name: ThemeName;
30
+ displayName: string;
31
+ description: string;
32
+ category: 'dark' | 'light';
33
+ previewColor: string;
34
+ author?: string;
35
+ }
36
+ export interface ThemeDefinition {
37
+ metadata: ThemeMetadata;
38
+ colors: Theme;
39
+ }
40
+ export declare const themeDefinitions: Record<ThemeName, ThemeDefinition>;
41
+ export declare const themes: Record<ThemeName, Theme>;
42
+ export declare function getCurrentTheme(): Theme;
43
+ export declare function getThemeMetadata(name: ThemeName): ThemeMetadata | undefined;
44
+ export declare function getAllThemeMetadata(): ThemeMetadata[];
45
+ export declare function getThemeDefinition(name: ThemeName): ThemeDefinition | undefined;
46
+ export declare function isValidTheme(name: string): name is ThemeName;
47
+ export declare function isBuiltinTheme(name: string): boolean;
48
+ /**
49
+ * 验证主题格式是否正确
50
+ */
51
+ export declare function validateTheme(theme: any): theme is ThemeDefinition;
52
+ /**
53
+ * 获取主题验证的详细错误信息
54
+ */
55
+ export declare function validateThemeWithDetails(theme: any): {
56
+ valid: boolean;
57
+ errors: string[];
25
58
  };
26
- export type Theme = typeof theme;
59
+ /**
60
+ * 创建主题模板
61
+ */
62
+ export declare function createThemeTemplate(name: string, displayName: string, category: 'dark' | 'light'): ThemeDefinition;
63
+ export declare const theme: Theme;