@yivan-lab/pretty-please 1.2.0 → 1.3.1

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 CHANGED
@@ -464,25 +464,126 @@ pls hook uninstall # 卸载 hook
464
464
 
465
465
  ## 🌗 主题系统
466
466
 
467
- 支持深色和浅色两种主题,适配不同终端背景:
467
+ 支持多种内置主题和自定义主题,适配不同终端背景和个人喜好:
468
+
469
+ ### 内置主题
468
470
 
469
471
  ```bash
470
472
  pls theme # 查看当前主题
471
473
  pls theme list # 查看所有可用主题
472
474
  pls theme dark # 切换到深色主题
473
- pls theme light # 切换到浅色主题
475
+ pls theme nord # 切换到 Nord 主题
474
476
  ```
475
477
 
476
- 主题说明:
478
+ **可用的内置主题:**
477
479
  - **dark(深色)** - 明亮的颜色,适合深色终端背景(默认)
478
480
  - **light(浅色)** - 较深的颜色,适合浅色终端背景
481
+ - **nord(北欧冷色)** - 冷色调护眼主题,适合长时间使用
482
+ - **dracula(德古拉暗色)** - 高对比暗色主题,色彩丰富但不刺眼
483
+ - **retro(复古终端绿)** - 经典终端荧光绿,致敬老派 hacker 风格
484
+ - **contrast(高对比度)** - 极高对比度,适合视力辅助和强光环境
485
+ - **monokai(经典编辑器)** - Monokai 经典配色,开发者熟悉的选择
479
486
 
480
487
  也可以通过配置命令切换:
481
488
 
482
489
  ```bash
483
- pls config set theme light
490
+ pls config set theme nord
491
+ ```
492
+
493
+ ### 自定义主题
494
+
495
+ 创建你自己的主题配色方案:
496
+
497
+ #### 1. 创建主题模板
498
+
499
+ ```bash
500
+ # 创建深色主题
501
+ pls theme create my-ocean --display-name "我的海洋主题"
502
+
503
+ # 创建浅色主题
504
+ pls theme create my-light --display-name "我的浅色" --category light
505
+ ```
506
+
507
+ 这会在 `~/.please/themes/` 目录下生成一个 JSON 模板文件。
508
+
509
+ #### 2. 编辑主题文件
510
+
511
+ ```bash
512
+ vim ~/.please/themes/my-ocean.json
484
513
  ```
485
514
 
515
+ 主题文件格式:
516
+
517
+ ```json
518
+ {
519
+ "metadata": {
520
+ "name": "my-ocean",
521
+ "displayName": "我的海洋主题",
522
+ "description": "深邃的海洋蓝配色",
523
+ "category": "dark",
524
+ "author": "Your Name"
525
+ },
526
+ "colors": {
527
+ "primary": "#0077BE",
528
+ "secondary": "#5DADE2",
529
+ "accent": "#48C9B0",
530
+ "success": "#2ECC71",
531
+ "error": "#E74C3C",
532
+ "warning": "#F39C12",
533
+ "info": "#3498DB",
534
+ "text": {
535
+ "primary": "#ECF0F1",
536
+ "secondary": "#BDC3C7",
537
+ "muted": "#95A5A6",
538
+ "dim": "#7F8C8D"
539
+ },
540
+ "border": "#34495E",
541
+ "divider": "#2C3E50",
542
+ "code": {
543
+ "background": "#1C2833",
544
+ "text": "#ECF0F1",
545
+ "keyword": "#C678DD",
546
+ "string": "#98C379",
547
+ "function": "#61AFEF",
548
+ "comment": "#5C6370"
549
+ }
550
+ }
551
+ }
552
+ ```
553
+
554
+ #### 3. 验证主题
555
+
556
+ ```bash
557
+ pls theme validate ~/.please/themes/my-ocean.json
558
+ ```
559
+
560
+ 验证通过后会显示主题信息,失败会给出详细的错误提示。
561
+
562
+ #### 4. 应用自定义主题
563
+
564
+ ```bash
565
+ pls theme my-ocean
566
+ ```
567
+
568
+ #### 5. 管理自定义主题
569
+
570
+ ```bash
571
+ # 查看所有主题(内置 + 自定义)
572
+ pls theme list
573
+
574
+ # 只查看自定义主题
575
+ pls theme list --custom
576
+
577
+ # 只查看内置主题
578
+ pls theme list --builtin
579
+ ```
580
+
581
+ 自定义主题在列表中会显示 ✨ 标记。
582
+
583
+ **主题文件位置:** `~/.please/themes/`
584
+
585
+ **颜色格式:** 所有颜色必须使用十六进制格式(如 `#00D9FF`)
586
+
486
587
  ## 🏷️ 命令别名
487
588
 
488
589
  将常用的操作保存为别名,一键触发:
@@ -594,7 +695,13 @@ pls hook status
594
695
  # 主题
595
696
  pls theme # 查看当前主题
596
697
  pls theme list # 查看所有主题
597
- pls theme <dark|light> # 切换主题
698
+ pls theme <name> # 切换主题(dark/light/nord/dracula/retro/contrast/monokai)
699
+
700
+ # 自定义主题
701
+ pls theme create <name> --display-name "名称" # 创建主题模板
702
+ pls theme validate <file> # 验证主题文件
703
+ pls theme list --custom # 只显示自定义主题
704
+ pls theme list --builtin # 只显示内置主题
598
705
 
599
706
  # 别名
600
707
  pls alias # 查看所有别名
package/bin/pls.tsx CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { dirname, join } from 'path'
5
+ import path from 'path'
5
6
  import { exec } from 'child_process'
6
7
  import fs from 'fs'
7
8
  import os from 'os'
@@ -126,10 +127,19 @@ function executeCommand(command: string): Promise<{ exitCode: number; output: st
126
127
 
127
128
  console.log('') // 空行
128
129
 
129
- // 计算命令框宽度,让分隔线长度一致
130
+ // 计算命令框宽度,让分隔线长度一致(限制终端宽度)
131
+ const termWidth = process.stdout.columns || 80
132
+ const maxContentWidth = termWidth - 6
130
133
  const lines = command.split('\n')
131
- const maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)))
132
- const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20)
134
+ const wrappedLines: string[] = []
135
+ for (const line of lines) {
136
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth))
137
+ }
138
+ const actualMaxWidth = Math.max(
139
+ ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
140
+ console2.getDisplayWidth('生成命令')
141
+ )
142
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
133
143
  console2.printSeparator('输出', boxWidth)
134
144
 
135
145
  // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
@@ -217,11 +227,21 @@ configCmd
217
227
  configCmd
218
228
  .command('set <key> <value>')
219
229
  .description('设置配置项 (apiKey, baseUrl, provider, model, shellHook, chatHistoryLimit)')
220
- .action((key, value) => {
230
+ .action(async (key, value) => {
221
231
  try {
232
+ const oldConfig = getConfig()
233
+ const oldShellHistoryLimit = oldConfig.shellHistoryLimit
234
+
222
235
  setConfigValue(key, value)
223
236
  console.log('')
224
237
  console2.success(`已设置 ${key}`)
238
+
239
+ // 如果修改了 shellHistoryLimit,自动重装 hook
240
+ if (key === 'shellHistoryLimit') {
241
+ const { reinstallHookForLimitChange } = await import('../src/shell-hook.js')
242
+ await reinstallHookForLimitChange(oldShellHistoryLimit, Number(value))
243
+ }
244
+
225
245
  console.log('')
226
246
  } catch (error: any) {
227
247
  console.log('')
@@ -243,8 +263,10 @@ const themeCmd = program.command('theme').description('管理主题')
243
263
  themeCmd
244
264
  .command('list')
245
265
  .description('查看所有可用主题')
246
- .action(async () => {
247
- const { themes } = await import('../src/ui/theme.js')
266
+ .option('--custom', '只显示自定义主题')
267
+ .option('--builtin', '只显示内置主题')
268
+ .action(async (options: { custom?: boolean; builtin?: boolean }) => {
269
+ const { getAllThemeMetadata, isBuiltinTheme } = await import('../src/ui/theme.js')
248
270
  const config = getConfig()
249
271
  const currentTheme = config.theme || 'dark'
250
272
 
@@ -252,37 +274,82 @@ themeCmd
252
274
  console2.title('🎨 可用主题:')
253
275
  console2.muted('━'.repeat(50))
254
276
 
255
- Object.keys(themes).forEach((themeName) => {
256
- const isCurrent = themeName === currentTheme
257
- const prefix = isCurrent ? '●' : '○'
258
- const label = themeName === 'dark' ? 'dark (深色)' : 'light (浅色)'
259
- const color = themeName === 'dark' ? '#00D9FF' : '#0284C7'
277
+ // 动态获取所有主题元数据
278
+ const allThemes = getAllThemeMetadata()
260
279
 
261
- if (isCurrent) {
262
- console.log(` ${chalk.hex(color)(prefix)} ${chalk.hex(color).bold(label)} ${chalk.gray('(当前)')}`)
263
- } else {
264
- console.log(` ${chalk.gray(prefix)} ${label}`)
280
+ // 根据选项过滤主题
281
+ const builtinThemes = allThemes.filter((meta) => isBuiltinTheme(meta.name))
282
+ const customThemes = allThemes.filter((meta) => !isBuiltinTheme(meta.name))
283
+
284
+ // 显示内置主题
285
+ if (!options.custom) {
286
+ if (builtinThemes.length > 0) {
287
+ console.log('')
288
+ console2.info('内置主题:')
289
+ builtinThemes.forEach((meta) => {
290
+ const isCurrent = meta.name === currentTheme
291
+ const prefix = isCurrent ? '●' : '○'
292
+ const label = `${meta.name} (${meta.displayName})`
293
+
294
+ if (isCurrent) {
295
+ console.log(` ${chalk.hex(meta.previewColor)(prefix)} ${chalk.hex(meta.previewColor).bold(label)} ${chalk.gray('(当前)')}`)
296
+ } else {
297
+ console.log(` ${chalk.gray(prefix)} ${label}`)
298
+ }
299
+ })
265
300
  }
266
- })
301
+ }
267
302
 
303
+ // 显示自定义主题
304
+ if (!options.builtin) {
305
+ if (customThemes.length > 0) {
306
+ console.log('')
307
+ console2.info('自定义主题:')
308
+ customThemes.forEach((meta) => {
309
+ const isCurrent = meta.name === currentTheme
310
+ const prefix = isCurrent ? '●' : '○'
311
+ const label = `${meta.name} (${meta.displayName})`
312
+ const emoji = ' ✨'
313
+
314
+ if (isCurrent) {
315
+ console.log(` ${chalk.hex(meta.previewColor)(prefix)} ${chalk.hex(meta.previewColor).bold(label)}${emoji} ${chalk.gray('(当前)')}`)
316
+ } else {
317
+ console.log(` ${chalk.gray(prefix)} ${label}${emoji}`)
318
+ }
319
+ })
320
+ } else if (options.custom) {
321
+ console.log('')
322
+ console2.muted(' 还没有自定义主题')
323
+ console2.muted(' 使用 pls theme create <name> 创建')
324
+ }
325
+ }
326
+
327
+ console.log('')
268
328
  console2.muted('━'.repeat(50))
269
329
  console.log('')
270
330
  })
271
331
 
272
332
  themeCmd
273
- .argument('[name]', '主题名称 (dark, light)')
333
+ .argument('[name]', '主题名称')
274
334
  .description('切换主题')
275
- .action((name?: string) => {
335
+ .action(async (name?: string) => {
336
+ const { getThemeMetadata, getAllThemeMetadata, isValidTheme } = await import('../src/ui/theme.js')
337
+
276
338
  if (!name) {
277
339
  // 显示当前主题
278
340
  const config = getConfig()
279
341
  const currentTheme = config.theme || 'dark'
280
- const label = currentTheme === 'dark' ? 'dark (深色)' : 'light (浅色)'
281
- const color = currentTheme === 'dark' ? '#00D9FF' : '#0284C7'
342
+ const meta = getThemeMetadata(currentTheme as any)
343
+
344
+ if (meta) {
345
+ console.log('')
346
+ console.log(`当前主题: ${chalk.hex(meta.previewColor).bold(`${meta.name} (${meta.displayName})`)}`)
347
+ if (meta.description) {
348
+ console2.muted(` ${meta.description}`)
349
+ }
350
+ console.log('')
351
+ }
282
352
 
283
- console.log('')
284
- console.log(`当前主题: ${chalk.hex(color).bold(label)}`)
285
- console.log('')
286
353
  console2.muted('使用 pls theme list 查看所有主题')
287
354
  console2.muted('使用 pls theme <name> 切换主题')
288
355
  console.log('')
@@ -291,12 +358,91 @@ themeCmd
291
358
 
292
359
  // 切换主题
293
360
  try {
361
+ // 验证主题是否存在
362
+ if (!isValidTheme(name)) {
363
+ const allThemes = getAllThemeMetadata()
364
+ const themeNames = allThemes.map((m) => m.name).join(', ')
365
+ throw new Error(`未知主题 "${name}",可用主题: ${themeNames}`)
366
+ }
367
+
294
368
  setConfigValue('theme', name)
295
- const label = name === 'dark' ? 'dark (深色)' : 'light (浅色)'
296
- const color = name === 'dark' ? '#00D9FF' : '#0284C7'
369
+ const meta = getThemeMetadata(name)
297
370
 
371
+ if (meta) {
372
+ console.log('')
373
+ console2.success(`已切换到 ${chalk.hex(meta.previewColor).bold(`${meta.name} (${meta.displayName})`)} 主题`)
374
+ if (meta.description) {
375
+ console2.muted(` ${meta.description}`)
376
+ }
377
+ console.log('')
378
+ }
379
+ } catch (error: any) {
380
+ console.log('')
381
+ console2.error(error.message)
298
382
  console.log('')
299
- console2.success(`已切换到 ${chalk.hex(color).bold(label)} 主题`)
383
+ process.exit(1)
384
+ }
385
+ })
386
+
387
+ // theme create - 创建主题模板
388
+ themeCmd
389
+ .command('create <name>')
390
+ .description('创建自定义主题模板')
391
+ .option('-d, --display-name <name>', '显示名称')
392
+ .option('-c, --category <type>', '主题类别 (dark 或 light)', 'dark')
393
+ .action(async (name: string, options: { displayName?: string; category?: string }) => {
394
+ const { createThemeTemplate } = await import('../src/ui/theme.js')
395
+
396
+ try {
397
+ // 验证主题名称格式
398
+ if (!/^[a-z0-9-]+$/.test(name)) {
399
+ throw new Error('主题名称只能包含小写字母、数字和连字符')
400
+ }
401
+
402
+ // 验证类别
403
+ const category = options.category as 'dark' | 'light'
404
+ if (category !== 'dark' && category !== 'light') {
405
+ throw new Error('主题类别必须是 dark 或 light')
406
+ }
407
+
408
+ // 创建主题目录
409
+ const themesDir = path.join(os.homedir(), '.please', 'themes')
410
+ if (!fs.existsSync(themesDir)) {
411
+ fs.mkdirSync(themesDir, { recursive: true })
412
+ }
413
+
414
+ // 检查主题文件是否已存在
415
+ const themePath = path.join(themesDir, `${name}.json`)
416
+ if (fs.existsSync(themePath)) {
417
+ throw new Error(`主题 "${name}" 已存在`)
418
+ }
419
+
420
+ // 创建主题模板
421
+ const displayName = options.displayName || name
422
+ const template = createThemeTemplate(name, displayName, category)
423
+
424
+ // 保存到文件
425
+ fs.writeFileSync(themePath, JSON.stringify(template, null, 2), 'utf-8')
426
+
427
+ // 显示成功信息
428
+ console.log('')
429
+ console2.success(`已创建主题模板: ${themePath}`)
430
+ console.log('')
431
+
432
+ console2.info('📝 下一步:')
433
+ console.log(` 1. 编辑主题文件修改颜色配置`)
434
+ console2.muted(` vim ${themePath}`)
435
+ console.log('')
436
+ console.log(` 2. 验证主题格式`)
437
+ console2.muted(` pls theme validate ${themePath}`)
438
+ console.log('')
439
+ console.log(` 3. 应用主题查看效果`)
440
+ console2.muted(` pls theme ${name}`)
441
+ console.log('')
442
+
443
+ console2.info('💡 提示:')
444
+ console2.muted(' - 使用在线工具选择颜色: https://colorhunt.co')
445
+ console2.muted(' - 参考内置主题: pls theme list')
300
446
  console.log('')
301
447
  } catch (error: any) {
302
448
  console.log('')
@@ -306,6 +452,75 @@ themeCmd
306
452
  }
307
453
  })
308
454
 
455
+ // theme validate - 验证主题文件
456
+ themeCmd
457
+ .command('validate <file>')
458
+ .description('验证主题文件格式')
459
+ .action(async (file: string) => {
460
+ const { validateThemeWithDetails } = await import('../src/ui/theme.js')
461
+
462
+ try {
463
+ // 读取主题文件
464
+ const themePath = path.isAbsolute(file) ? file : path.join(process.cwd(), file)
465
+
466
+ if (!fs.existsSync(themePath)) {
467
+ throw new Error(`文件不存在: ${themePath}`)
468
+ }
469
+
470
+ const content = fs.readFileSync(themePath, 'utf-8')
471
+ const theme = JSON.parse(content)
472
+
473
+ // 验证主题
474
+ const result = validateThemeWithDetails(theme)
475
+
476
+ console.log('')
477
+
478
+ if (result.valid) {
479
+ console2.success('✓ 主题验证通过')
480
+ console.log('')
481
+
482
+ if (theme.metadata) {
483
+ console2.info('主题信息:')
484
+ console.log(` 名称: ${theme.metadata.name} (${theme.metadata.displayName})`)
485
+ console.log(` 类别: ${theme.metadata.category}`)
486
+ if (theme.metadata.description) {
487
+ console.log(` 描述: ${theme.metadata.description}`)
488
+ }
489
+ if (theme.metadata.author) {
490
+ console.log(` 作者: ${theme.metadata.author}`)
491
+ }
492
+ }
493
+
494
+ console.log('')
495
+ } else {
496
+ console2.error('✗ 主题验证失败')
497
+ console.log('')
498
+ console2.info('错误列表:')
499
+ result.errors.forEach((err, idx) => {
500
+ console.log(` ${idx + 1}. ${err}`)
501
+ })
502
+ console.log('')
503
+
504
+ console2.info('修复建议:')
505
+ console2.muted(` 1. 编辑主题文件: vim ${themePath}`)
506
+ console2.muted(' 2. 参考内置主题格式')
507
+ console2.muted(' 3. 确保所有颜色使用 #RRGGBB 格式')
508
+ console.log('')
509
+
510
+ process.exit(1)
511
+ }
512
+ } catch (error: any) {
513
+ console.log('')
514
+ if (error.message.includes('Unexpected token')) {
515
+ console2.error('JSON 格式错误,请检查文件语法')
516
+ } else {
517
+ console2.error(error.message)
518
+ }
519
+ console.log('')
520
+ process.exit(1)
521
+ }
522
+ })
523
+
309
524
  // history 子命令
310
525
  const historyCmd = program.command('history').description('查看或管理命令历史')
311
526
 
@@ -962,24 +1177,17 @@ remoteCmd.action(() => {
962
1177
  displayRemotes()
963
1178
  })
964
1179
 
965
- // chat 子命令
966
- const chatCmd = program.command('chat').description('AI 对话模式,问答、讲解命令')
967
-
968
- chatCmd
969
- .command('clear')
970
- .description('清空对话历史')
971
- .action(() => {
972
- clearChatHistory()
973
- console.log('')
974
- console2.success('对话历史已清空')
975
- console.log('')
976
- })
977
-
978
- // 默认 chat 命令(进行对话)
979
- chatCmd
980
- .argument('[prompt...]', '你的问题')
1180
+ // chat 命令(AI 对话)
1181
+ program
1182
+ .command('chat')
1183
+ .description('AI 对话模式,问答、讲解命令')
1184
+ .argument('[prompt...]', '你的问题(不提供则显示状态)')
981
1185
  .option('-d, --debug', '显示调试信息')
982
1186
  .action((promptArgs, options) => {
1187
+ // Workaround: Commander.js 14.x 的子命令 option 解析有 bug
1188
+ // 直接从 process.argv 检查 --debug
1189
+ const debug = process.argv.includes('--debug') || process.argv.includes('-d')
1190
+
983
1191
  const prompt = promptArgs.join(' ')
984
1192
 
985
1193
  if (!prompt.trim()) {
@@ -995,8 +1203,8 @@ chatCmd
995
1203
  console2.muted('━'.repeat(40))
996
1204
  console.log('')
997
1205
  console2.muted('用法:')
998
- console2.info(' pls chat <问题> 与 AI 对话')
999
- console2.info(' pls chat clear 清空对话历史')
1206
+ console2.info(' pls chat <问题> 与 AI 对话')
1207
+ console2.info(' pls history chat clear 清空对话历史')
1000
1208
  console.log('')
1001
1209
  return
1002
1210
  }
@@ -1019,7 +1227,7 @@ chatCmd
1019
1227
  render(
1020
1228
  React.createElement(Chat, {
1021
1229
  prompt,
1022
- debug: options.debug,
1230
+ debug: debug, // 使用 debug 变量
1023
1231
  showRoundCount: true,
1024
1232
  onComplete: () => process.exit(0),
1025
1233
  })
@@ -1297,9 +1505,6 @@ program
1297
1505
 
1298
1506
  // 处理步骤结果
1299
1507
  if (!stepResult || stepResult.cancelled) {
1300
- console.log('')
1301
- console2.muted('已取消执行')
1302
- console.log('')
1303
1508
  process.exit(0)
1304
1509
  }
1305
1510
 
@@ -1495,10 +1700,19 @@ async function executeRemoteCommand(
1495
1700
 
1496
1701
  console.log('') // 空行
1497
1702
 
1498
- // 计算命令框宽度,让分隔线长度一致
1703
+ // 计算命令框宽度,让分隔线长度一致(限制终端宽度)
1704
+ const termWidth = process.stdout.columns || 80
1705
+ const maxContentWidth = termWidth - 6
1499
1706
  const lines = command.split('\n')
1500
- const maxContentWidth = Math.max(...lines.map(l => console2.getDisplayWidth(l)))
1501
- const boxWidth = Math.max(maxContentWidth + 4, console2.getDisplayWidth('生成命令') + 6, 20)
1707
+ const wrappedLines: string[] = []
1708
+ for (const line of lines) {
1709
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth))
1710
+ }
1711
+ const actualMaxWidth = Math.max(
1712
+ ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
1713
+ console2.getDisplayWidth('生成命令')
1714
+ )
1715
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
1502
1716
  console2.printSeparator(`远程输出 (${remoteName})`, boxWidth)
1503
1717
 
1504
1718
  try {