@yivan-lab/pretty-please 1.3.1 → 1.5.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 (94) hide show
  1. package/README.md +250 -620
  2. package/bin/pls.tsx +178 -40
  3. package/dist/bin/pls.js +149 -27
  4. package/dist/package.json +10 -2
  5. package/dist/src/__integration__/command-generation.test.d.ts +5 -0
  6. package/dist/src/__integration__/command-generation.test.js +508 -0
  7. package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
  8. package/dist/src/__integration__/error-recovery.test.js +511 -0
  9. package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
  10. package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
  11. package/dist/src/__tests__/alias.test.d.ts +5 -0
  12. package/dist/src/__tests__/alias.test.js +421 -0
  13. package/dist/src/__tests__/chat-history.test.d.ts +5 -0
  14. package/dist/src/__tests__/chat-history.test.js +372 -0
  15. package/dist/src/__tests__/config.test.d.ts +5 -0
  16. package/dist/src/__tests__/config.test.js +822 -0
  17. package/dist/src/__tests__/history.test.d.ts +5 -0
  18. package/dist/src/__tests__/history.test.js +439 -0
  19. package/dist/src/__tests__/remote-history.test.d.ts +5 -0
  20. package/dist/src/__tests__/remote-history.test.js +641 -0
  21. package/dist/src/__tests__/remote.test.d.ts +5 -0
  22. package/dist/src/__tests__/remote.test.js +689 -0
  23. package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
  24. package/dist/src/__tests__/shell-hook-install.test.js +413 -0
  25. package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
  26. package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
  27. package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
  28. package/dist/src/__tests__/shell-hook.test.js +440 -0
  29. package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
  30. package/dist/src/__tests__/sysinfo.test.js +572 -0
  31. package/dist/src/__tests__/system-history.test.d.ts +5 -0
  32. package/dist/src/__tests__/system-history.test.js +457 -0
  33. package/dist/src/components/Chat.js +9 -28
  34. package/dist/src/config.d.ts +2 -0
  35. package/dist/src/config.js +30 -2
  36. package/dist/src/mastra-chat.js +10 -6
  37. package/dist/src/multi-step.js +10 -8
  38. package/dist/src/project-context.d.ts +22 -0
  39. package/dist/src/project-context.js +168 -0
  40. package/dist/src/prompts.d.ts +4 -4
  41. package/dist/src/prompts.js +23 -6
  42. package/dist/src/shell-hook.d.ts +32 -0
  43. package/dist/src/shell-hook.js +226 -33
  44. package/dist/src/sysinfo.d.ts +38 -9
  45. package/dist/src/sysinfo.js +245 -21
  46. package/dist/src/system-history.d.ts +18 -0
  47. package/dist/src/system-history.js +151 -0
  48. package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
  49. package/dist/src/ui/__tests__/theme.test.js +688 -0
  50. package/dist/src/upgrade.js +3 -0
  51. package/dist/src/user-preferences.d.ts +44 -0
  52. package/dist/src/user-preferences.js +147 -0
  53. package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
  54. package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
  55. package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
  56. package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
  57. package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
  58. package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
  59. package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
  60. package/dist/src/utils/__tests__/platform.test.js +137 -0
  61. package/dist/src/utils/platform.d.ts +88 -0
  62. package/dist/src/utils/platform.js +331 -0
  63. package/package.json +10 -2
  64. package/src/__integration__/command-generation.test.ts +602 -0
  65. package/src/__integration__/error-recovery.test.ts +620 -0
  66. package/src/__integration__/shell-hook-workflow.test.ts +457 -0
  67. package/src/__tests__/alias.test.ts +545 -0
  68. package/src/__tests__/chat-history.test.ts +462 -0
  69. package/src/__tests__/config.test.ts +1043 -0
  70. package/src/__tests__/history.test.ts +538 -0
  71. package/src/__tests__/remote-history.test.ts +791 -0
  72. package/src/__tests__/remote.test.ts +866 -0
  73. package/src/__tests__/shell-hook-install.test.ts +510 -0
  74. package/src/__tests__/shell-hook-remote.test.ts +679 -0
  75. package/src/__tests__/shell-hook.test.ts +564 -0
  76. package/src/__tests__/sysinfo.test.ts +718 -0
  77. package/src/__tests__/system-history.test.ts +608 -0
  78. package/src/components/Chat.tsx +10 -37
  79. package/src/config.ts +29 -2
  80. package/src/mastra-chat.ts +12 -5
  81. package/src/multi-step.ts +11 -5
  82. package/src/project-context.ts +191 -0
  83. package/src/prompts.ts +26 -5
  84. package/src/shell-hook.ts +254 -32
  85. package/src/sysinfo.ts +326 -25
  86. package/src/system-history.ts +170 -0
  87. package/src/ui/__tests__/theme.test.ts +869 -0
  88. package/src/upgrade.ts +5 -0
  89. package/src/user-preferences.ts +178 -0
  90. package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
  91. package/src/utils/__tests__/platform-exec.test.ts +278 -0
  92. package/src/utils/__tests__/platform-shell.test.ts +353 -0
  93. package/src/utils/__tests__/platform.test.ts +170 -0
  94. package/src/utils/platform.ts +431 -0
package/bin/pls.tsx CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { dirname, join } from 'path'
5
5
  import path from 'path'
6
- import { exec } from 'child_process'
6
+ import { exec, spawn } from 'child_process'
7
7
  import fs from 'fs'
8
8
  import os from 'os'
9
9
  import chalk from 'chalk'
@@ -12,7 +12,7 @@ import chalk from 'chalk'
12
12
  // import { render } from 'ink'
13
13
  // import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
14
14
  // import { Chat } from '../src/components/Chat.js'
15
- import { isConfigValid, setConfigValue, getConfig, maskApiKey } from '../src/config.js'
15
+ import { isConfigValid, setConfigValue, getConfig, maskApiKey, displayConfig } from '../src/config.js'
16
16
  import { clearHistory, addHistory, getHistory, getHistoryFilePath } from '../src/history.js'
17
17
  import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath, displayChatHistory } from '../src/chat-history.js'
18
18
  import { type ExecutedStep } from '../src/multi-step.js'
@@ -50,6 +50,12 @@ import {
50
50
  generateBatchRemoteCommands,
51
51
  executeBatchRemoteCommands,
52
52
  } from '../src/remote.js'
53
+ import { getSystemInfo, formatSystemInfo, refreshSystemCache, displaySystemInfo } from '../src/sysinfo.js'
54
+ import {
55
+ displayCommandStats,
56
+ clearCommandStats,
57
+ getStatsFilePath,
58
+ } from '../src/user-preferences.js'
53
59
  import {
54
60
  addRemoteHistory,
55
61
  displayRemoteHistory,
@@ -65,6 +71,11 @@ import {
65
71
  uninstallRemoteShellHook,
66
72
  getRemoteHookStatus,
67
73
  } from '../src/shell-hook.js'
74
+ import {
75
+ buildShellExecConfig,
76
+ getDefaultShell,
77
+ isWindows,
78
+ } from '../src/utils/platform.js'
68
79
 
69
80
  // 获取主题颜色的辅助函数
70
81
  function getThemeColors() {
@@ -116,10 +127,92 @@ process.on('beforeExit', () => {
116
127
  }
117
128
  })
118
129
 
130
+ /**
131
+ * 需要 TTY 的工具白名单
132
+ * 这些工具在 pipe 模式下可能会卡住或无输出
133
+ */
134
+ const TTY_REQUIRED_COMMANDS = new Set([
135
+ // ls 替代品(带图标/颜色)
136
+ 'eza', 'exa', 'lsd',
137
+ // cat 替代品(带语法高亮)
138
+ 'bat', 'batcat',
139
+ // diff 替代品
140
+ 'delta', 'diff-so-fancy',
141
+ // 系统监控
142
+ 'htop', 'btop', 'top', 'glances', 'gtop', 'bpytop',
143
+ // 编辑器
144
+ 'vim', 'nvim', 'nano', 'emacs', 'micro', 'helix', 'hx',
145
+ // 分页器
146
+ 'less', 'more', 'most',
147
+ // 模糊搜索
148
+ 'fzf', 'skim', 'sk',
149
+ // 终端复用器
150
+ 'tmux', 'screen', 'zellij',
151
+ // TUI 工具
152
+ 'lazygit', 'lazydocker', 'lazysql', 'k9s', 'tig',
153
+ // 文件管理器
154
+ 'nnn', 'ranger', 'lf', 'yazi', 'mc', 'vifm',
155
+ // 数据查看
156
+ 'visidata', 'vd',
157
+ ])
158
+
159
+ /**
160
+ * 使用 inherit 模式执行命令(用于需要 TTY 的工具)
161
+ * 特点:命令能正常执行,但无法捕获输出
162
+ */
163
+ function executeWithInherit(command: string): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
164
+ return new Promise((resolve) => {
165
+ console.log('') // 空行
166
+
167
+ // 计算命令框宽度
168
+ const termWidth = process.stdout.columns || 80
169
+ const maxContentWidth = termWidth - 6
170
+ const lines = command.split('\n')
171
+ const wrappedLines: string[] = []
172
+ for (const line of lines) {
173
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth))
174
+ }
175
+ const actualMaxWidth = Math.max(
176
+ ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
177
+ console2.getDisplayWidth('生成命令')
178
+ )
179
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
180
+ console2.printSeparator('输出', boxWidth)
181
+
182
+ // 使用 platform 模块构建跨平台命令执行配置
183
+ const execConfig = buildShellExecConfig(command)
184
+
185
+ // 使用 spawn + inherit(输出直接到终端)
186
+ const child = spawn(execConfig.shell, execConfig.args, {
187
+ stdio: 'inherit',
188
+ env: process.env,
189
+ })
190
+
191
+ child.on('close', (code) => {
192
+ console2.printSeparator('', boxWidth)
193
+ resolve({ exitCode: code || 0, output: '', stdout: '', stderr: '' })
194
+ })
195
+
196
+ child.on('error', (err) => {
197
+ console2.printSeparator('', boxWidth)
198
+ console2.error(err.message)
199
+ resolve({ exitCode: 1, output: err.message, stdout: '', stderr: err.message })
200
+ })
201
+ })
202
+ }
203
+
119
204
  /**
120
205
  * 执行命令(原生版本)
121
206
  */
122
207
  function executeCommand(command: string): Promise<{ exitCode: number; output: string; stdout: string; stderr: string }> {
208
+ // 检测是否是需要 TTY 的工具
209
+ const firstCmd = command.trim().split(/[\s|&;]/)[0]
210
+ if (TTY_REQUIRED_COMMANDS.has(firstCmd)) {
211
+ // 使用 inherit 模式执行(无法捕获输出,但能正常运行)
212
+ return executeWithInherit(command)
213
+ }
214
+
215
+ // 普通命令:使用 pipe 模式(捕获输出)
123
216
  return new Promise((resolve) => {
124
217
  let stdout = ''
125
218
  let stderr = ''
@@ -142,8 +235,10 @@ function executeCommand(command: string): Promise<{ exitCode: number; output: st
142
235
  const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
143
236
  console2.printSeparator('输出', boxWidth)
144
237
 
145
- // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
146
- const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' })
238
+ // 使用 platform 模块构建跨平台命令执行配置
239
+ const execConfig = buildShellExecConfig(command)
240
+
241
+ const child = exec(execConfig.command, { shell: execConfig.shell })
147
242
 
148
243
  child.stdout?.on('data', (data) => {
149
244
  stdout += data
@@ -191,37 +286,7 @@ configCmd
191
286
  .alias('show')
192
287
  .description('查看当前配置')
193
288
  .action(() => {
194
- const config = getConfig()
195
- const CONFIG_FILE = join(os.homedir(), '.please', 'config.json')
196
-
197
- console.log('')
198
- console2.title('当前配置:')
199
- console2.muted('━'.repeat(50))
200
- console.log(` ${chalk.hex(getThemeColors().primary)('apiKey')}: ${maskApiKey(config.apiKey)}`)
201
- console.log(` ${chalk.hex(getThemeColors().primary)('baseUrl')}: ${config.baseUrl}`)
202
- console.log(` ${chalk.hex(getThemeColors().primary)('provider')}: ${config.provider}`)
203
- console.log(` ${chalk.hex(getThemeColors().primary)('model')}: ${config.model}`)
204
- console.log(
205
- ` ${chalk.hex(getThemeColors().primary)('shellHook')}: ${
206
- config.shellHook ? chalk.hex(getThemeColors().success)('已启用') : chalk.gray('未启用')
207
- }`
208
- )
209
- console.log(
210
- ` ${chalk.hex(getThemeColors().primary)('editMode')}: ${
211
- config.editMode === 'auto' ? chalk.hex(getThemeColors().primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
212
- }`
213
- )
214
- console.log(` ${chalk.hex(getThemeColors().primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
215
- console.log(` ${chalk.hex(getThemeColors().primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
216
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
217
- console.log(
218
- ` ${chalk.hex(getThemeColors().primary)('theme')}: ${
219
- config.theme === 'dark' ? chalk.hex(getThemeColors().primary)('dark (深色)') : chalk.hex(getThemeColors().primary)('light (浅色)')
220
- }`
221
- )
222
- console2.muted('━'.repeat(50))
223
- console2.muted(`配置文件: ${CONFIG_FILE}`)
224
- console.log('')
289
+ displayConfig()
225
290
  })
226
291
 
227
292
  configCmd
@@ -816,6 +881,59 @@ aliasCmd.action(() => {
816
881
  displayAliases()
817
882
  })
818
883
 
884
+ // sysinfo 子命令
885
+ const sysinfoCmd = program.command('sysinfo').description('管理系统信息')
886
+
887
+ sysinfoCmd
888
+ .command('show')
889
+ .description('查看系统信息')
890
+ .action(async () => {
891
+ const info = await getSystemInfo()
892
+ displaySystemInfo(info)
893
+ })
894
+
895
+ sysinfoCmd
896
+ .command('refresh')
897
+ .description('刷新系统信息缓存')
898
+ .action(() => {
899
+ console.log('')
900
+ refreshSystemCache()
901
+ console.log('')
902
+ })
903
+
904
+ // 默认 sysinfo 命令(显示信息)
905
+ sysinfoCmd.action(async () => {
906
+ const info = await getSystemInfo()
907
+ displaySystemInfo(info)
908
+ })
909
+
910
+ // prefs 子命令
911
+ const prefsCmd = program.command('prefs').description('管理命令偏好统计')
912
+
913
+ prefsCmd
914
+ .command('show')
915
+ .description('查看命令偏好统计')
916
+ .action(() => {
917
+ displayCommandStats()
918
+ })
919
+
920
+ prefsCmd
921
+ .command('clear')
922
+ .description('清空偏好统计')
923
+ .action(() => {
924
+ const colors = getThemeColors()
925
+ clearCommandStats()
926
+ console.log('')
927
+ console.log(chalk.hex(colors.success)('✓ 已清空命令偏好统计'))
928
+ console.log(chalk.gray(` 统计文件: ${getStatsFilePath()}`))
929
+ console.log('')
930
+ })
931
+
932
+ // 默认 prefs 命令(显示统计)
933
+ prefsCmd.action(() => {
934
+ displayCommandStats()
935
+ })
936
+
819
937
  // remote 子命令
820
938
  const remoteCmd = program.command('remote').description('管理远程服务器')
821
939
 
@@ -1240,7 +1358,7 @@ program
1240
1358
  .argument('[prompt...]', '自然语言描述你想执行的操作')
1241
1359
  .option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
1242
1360
  .option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
1243
- .action((promptArgs, options) => {
1361
+ .action(async (promptArgs, options) => {
1244
1362
  // 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
1245
1363
  if (typeof options.remote === 'string' && !getRemote(options.remote)) {
1246
1364
  // "查看当前目录" 不是服务器名,放回 prompt
@@ -1248,12 +1366,32 @@ program
1248
1366
  options.remote = true // 改为使用默认服务器
1249
1367
  }
1250
1368
 
1369
+ let prompt = ''
1370
+
1251
1371
  if (promptArgs.length === 0) {
1252
- program.help()
1253
- return
1254
- }
1372
+ // 无参数时:尝试自动检测上一条失败的命令
1373
+ const { getLastNonPlsCommand } = await import('../src/shell-hook.js')
1374
+ const lastCmd = getLastNonPlsCommand()
1375
+
1376
+ if (lastCmd && lastCmd.exit !== 0) {
1377
+ // 找到了失败的命令,自动生成 prompt
1378
+ prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`
1379
+
1380
+ if (options.debug) {
1381
+ console.log('')
1382
+ console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`)
1383
+ console2.muted(`生成 prompt: ${prompt}`)
1384
+ }
1255
1385
 
1256
- let prompt = promptArgs.join(' ')
1386
+ // 继续执行命令生成流程(不 return)
1387
+ } else {
1388
+ // 没有失败的命令,显示帮助
1389
+ program.help()
1390
+ return
1391
+ }
1392
+ } else {
1393
+ prompt = promptArgs.join(' ')
1394
+ }
1257
1395
 
1258
1396
  if (!prompt.trim()) {
1259
1397
  console.log('')
package/dist/bin/pls.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { fileURLToPath } from 'url';
4
- import { dirname, join } from 'path';
4
+ import { dirname } from 'path';
5
5
  import path from 'path';
6
- import { exec } from 'child_process';
6
+ import { exec, spawn } from 'child_process';
7
7
  import fs from 'fs';
8
8
  import os from 'os';
9
9
  import chalk from 'chalk';
@@ -12,7 +12,7 @@ import chalk from 'chalk';
12
12
  // import { render } from 'ink'
13
13
  // import { MultiStepCommandGenerator } from '../src/components/MultiStepCommandGenerator.js'
14
14
  // import { Chat } from '../src/components/Chat.js'
15
- import { isConfigValid, setConfigValue, getConfig, maskApiKey } from '../src/config.js';
15
+ import { isConfigValid, setConfigValue, getConfig, displayConfig } from '../src/config.js';
16
16
  import { clearHistory, addHistory, getHistory, getHistoryFilePath } from '../src/history.js';
17
17
  import { clearChatHistory, getChatRoundCount, getChatHistoryFilePath, displayChatHistory } from '../src/chat-history.js';
18
18
  import { installShellHook, uninstallShellHook, getHookStatus, detectShell, getShellConfigPath, displayShellHistory, clearShellHistory, } from '../src/shell-hook.js';
@@ -20,8 +20,11 @@ import { checkForUpdates, showUpdateNotice, performUpgrade, } from '../src/upgra
20
20
  import { getCurrentTheme } from '../src/ui/theme.js';
21
21
  import { addAlias, removeAlias, displayAliases, resolveAlias, } from '../src/alias.js';
22
22
  import { addRemote, removeRemote, displayRemotes, getRemote, testRemoteConnection, sshExec, collectRemoteSysInfo, setRemoteWorkDir, getRemoteWorkDir, generateBatchRemoteCommands, executeBatchRemoteCommands, } from '../src/remote.js';
23
+ import { getSystemInfo, refreshSystemCache, displaySystemInfo } from '../src/sysinfo.js';
24
+ import { displayCommandStats, clearCommandStats, getStatsFilePath, } from '../src/user-preferences.js';
23
25
  import { addRemoteHistory, displayRemoteHistory, clearRemoteHistory, fetchRemoteShellHistory, displayRemoteShellHistory, clearRemoteShellHistory, } from '../src/remote-history.js';
24
26
  import { detectRemoteShell, getRemoteShellConfigPath, installRemoteShellHook, uninstallRemoteShellHook, getRemoteHookStatus, } from '../src/shell-hook.js';
27
+ import { buildShellExecConfig, } from '../src/utils/platform.js';
25
28
  // 获取主题颜色的辅助函数
26
29
  function getThemeColors() {
27
30
  const theme = getCurrentTheme();
@@ -65,10 +68,81 @@ process.on('beforeExit', () => {
65
68
  showUpdateNotice(packageJson.version, updateCheckResult.latestVersion);
66
69
  }
67
70
  });
71
+ /**
72
+ * 需要 TTY 的工具白名单
73
+ * 这些工具在 pipe 模式下可能会卡住或无输出
74
+ */
75
+ const TTY_REQUIRED_COMMANDS = new Set([
76
+ // ls 替代品(带图标/颜色)
77
+ 'eza', 'exa', 'lsd',
78
+ // cat 替代品(带语法高亮)
79
+ 'bat', 'batcat',
80
+ // diff 替代品
81
+ 'delta', 'diff-so-fancy',
82
+ // 系统监控
83
+ 'htop', 'btop', 'top', 'glances', 'gtop', 'bpytop',
84
+ // 编辑器
85
+ 'vim', 'nvim', 'nano', 'emacs', 'micro', 'helix', 'hx',
86
+ // 分页器
87
+ 'less', 'more', 'most',
88
+ // 模糊搜索
89
+ 'fzf', 'skim', 'sk',
90
+ // 终端复用器
91
+ 'tmux', 'screen', 'zellij',
92
+ // TUI 工具
93
+ 'lazygit', 'lazydocker', 'lazysql', 'k9s', 'tig',
94
+ // 文件管理器
95
+ 'nnn', 'ranger', 'lf', 'yazi', 'mc', 'vifm',
96
+ // 数据查看
97
+ 'visidata', 'vd',
98
+ ]);
99
+ /**
100
+ * 使用 inherit 模式执行命令(用于需要 TTY 的工具)
101
+ * 特点:命令能正常执行,但无法捕获输出
102
+ */
103
+ function executeWithInherit(command) {
104
+ return new Promise((resolve) => {
105
+ console.log(''); // 空行
106
+ // 计算命令框宽度
107
+ const termWidth = process.stdout.columns || 80;
108
+ const maxContentWidth = termWidth - 6;
109
+ const lines = command.split('\n');
110
+ const wrappedLines = [];
111
+ for (const line of lines) {
112
+ wrappedLines.push(...console2.wrapText(line, maxContentWidth));
113
+ }
114
+ const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
115
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
116
+ console2.printSeparator('输出', boxWidth);
117
+ // 使用 platform 模块构建跨平台命令执行配置
118
+ const execConfig = buildShellExecConfig(command);
119
+ // 使用 spawn + inherit(输出直接到终端)
120
+ const child = spawn(execConfig.shell, execConfig.args, {
121
+ stdio: 'inherit',
122
+ env: process.env,
123
+ });
124
+ child.on('close', (code) => {
125
+ console2.printSeparator('', boxWidth);
126
+ resolve({ exitCode: code || 0, output: '', stdout: '', stderr: '' });
127
+ });
128
+ child.on('error', (err) => {
129
+ console2.printSeparator('', boxWidth);
130
+ console2.error(err.message);
131
+ resolve({ exitCode: 1, output: err.message, stdout: '', stderr: err.message });
132
+ });
133
+ });
134
+ }
68
135
  /**
69
136
  * 执行命令(原生版本)
70
137
  */
71
138
  function executeCommand(command) {
139
+ // 检测是否是需要 TTY 的工具
140
+ const firstCmd = command.trim().split(/[\s|&;]/)[0];
141
+ if (TTY_REQUIRED_COMMANDS.has(firstCmd)) {
142
+ // 使用 inherit 模式执行(无法捕获输出,但能正常运行)
143
+ return executeWithInherit(command);
144
+ }
145
+ // 普通命令:使用 pipe 模式(捕获输出)
72
146
  return new Promise((resolve) => {
73
147
  let stdout = '';
74
148
  let stderr = '';
@@ -85,8 +159,9 @@ function executeCommand(command) {
85
159
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
86
160
  const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
87
161
  console2.printSeparator('输出', boxWidth);
88
- // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
89
- const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' });
162
+ // 使用 platform 模块构建跨平台命令执行配置
163
+ const execConfig = buildShellExecConfig(command);
164
+ const child = exec(execConfig.command, { shell: execConfig.shell });
90
165
  child.stdout?.on('data', (data) => {
91
166
  stdout += data;
92
167
  hasOutput = true;
@@ -127,24 +202,7 @@ configCmd
127
202
  .alias('show')
128
203
  .description('查看当前配置')
129
204
  .action(() => {
130
- const config = getConfig();
131
- const CONFIG_FILE = join(os.homedir(), '.please', 'config.json');
132
- console.log('');
133
- console2.title('当前配置:');
134
- console2.muted('━'.repeat(50));
135
- console.log(` ${chalk.hex(getThemeColors().primary)('apiKey')}: ${maskApiKey(config.apiKey)}`);
136
- console.log(` ${chalk.hex(getThemeColors().primary)('baseUrl')}: ${config.baseUrl}`);
137
- console.log(` ${chalk.hex(getThemeColors().primary)('provider')}: ${config.provider}`);
138
- console.log(` ${chalk.hex(getThemeColors().primary)('model')}: ${config.model}`);
139
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHook')}: ${config.shellHook ? chalk.hex(getThemeColors().success)('已启用') : chalk.gray('未启用')}`);
140
- console.log(` ${chalk.hex(getThemeColors().primary)('editMode')}: ${config.editMode === 'auto' ? chalk.hex(getThemeColors().primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')}`);
141
- console.log(` ${chalk.hex(getThemeColors().primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
142
- console.log(` ${chalk.hex(getThemeColors().primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
143
- console.log(` ${chalk.hex(getThemeColors().primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
144
- console.log(` ${chalk.hex(getThemeColors().primary)('theme')}: ${config.theme === 'dark' ? chalk.hex(getThemeColors().primary)('dark (深色)') : chalk.hex(getThemeColors().primary)('light (浅色)')}`);
145
- console2.muted('━'.repeat(50));
146
- console2.muted(`配置文件: ${CONFIG_FILE}`);
147
- console.log('');
205
+ displayConfig();
148
206
  });
149
207
  configCmd
150
208
  .command('set <key> <value>')
@@ -648,6 +706,51 @@ aliasCmd
648
706
  aliasCmd.action(() => {
649
707
  displayAliases();
650
708
  });
709
+ // sysinfo 子命令
710
+ const sysinfoCmd = program.command('sysinfo').description('管理系统信息');
711
+ sysinfoCmd
712
+ .command('show')
713
+ .description('查看系统信息')
714
+ .action(async () => {
715
+ const info = await getSystemInfo();
716
+ displaySystemInfo(info);
717
+ });
718
+ sysinfoCmd
719
+ .command('refresh')
720
+ .description('刷新系统信息缓存')
721
+ .action(() => {
722
+ console.log('');
723
+ refreshSystemCache();
724
+ console.log('');
725
+ });
726
+ // 默认 sysinfo 命令(显示信息)
727
+ sysinfoCmd.action(async () => {
728
+ const info = await getSystemInfo();
729
+ displaySystemInfo(info);
730
+ });
731
+ // prefs 子命令
732
+ const prefsCmd = program.command('prefs').description('管理命令偏好统计');
733
+ prefsCmd
734
+ .command('show')
735
+ .description('查看命令偏好统计')
736
+ .action(() => {
737
+ displayCommandStats();
738
+ });
739
+ prefsCmd
740
+ .command('clear')
741
+ .description('清空偏好统计')
742
+ .action(() => {
743
+ const colors = getThemeColors();
744
+ clearCommandStats();
745
+ console.log('');
746
+ console.log(chalk.hex(colors.success)('✓ 已清空命令偏好统计'));
747
+ console.log(chalk.gray(` 统计文件: ${getStatsFilePath()}`));
748
+ console.log('');
749
+ });
750
+ // 默认 prefs 命令(显示统计)
751
+ prefsCmd.action(() => {
752
+ displayCommandStats();
753
+ });
651
754
  // remote 子命令
652
755
  const remoteCmd = program.command('remote').description('管理远程服务器');
653
756
  remoteCmd
@@ -1030,18 +1133,37 @@ program
1030
1133
  .argument('[prompt...]', '自然语言描述你想执行的操作')
1031
1134
  .option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
1032
1135
  .option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
1033
- .action((promptArgs, options) => {
1136
+ .action(async (promptArgs, options) => {
1034
1137
  // 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
1035
1138
  if (typeof options.remote === 'string' && !getRemote(options.remote)) {
1036
1139
  // "查看当前目录" 不是服务器名,放回 prompt
1037
1140
  promptArgs.unshift(options.remote);
1038
1141
  options.remote = true; // 改为使用默认服务器
1039
1142
  }
1143
+ let prompt = '';
1040
1144
  if (promptArgs.length === 0) {
1041
- program.help();
1042
- return;
1145
+ // 无参数时:尝试自动检测上一条失败的命令
1146
+ const { getLastNonPlsCommand } = await import('../src/shell-hook.js');
1147
+ const lastCmd = getLastNonPlsCommand();
1148
+ if (lastCmd && lastCmd.exit !== 0) {
1149
+ // 找到了失败的命令,自动生成 prompt
1150
+ prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`;
1151
+ if (options.debug) {
1152
+ console.log('');
1153
+ console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`);
1154
+ console2.muted(`生成 prompt: ${prompt}`);
1155
+ }
1156
+ // 继续执行命令生成流程(不 return)
1157
+ }
1158
+ else {
1159
+ // 没有失败的命令,显示帮助
1160
+ program.help();
1161
+ return;
1162
+ }
1163
+ }
1164
+ else {
1165
+ prompt = promptArgs.join(' ');
1043
1166
  }
1044
- let prompt = promptArgs.join(' ');
1045
1167
  if (!prompt.trim()) {
1046
1168
  console.log('');
1047
1169
  console2.error('请提供你想执行的操作描述');
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,10 @@
11
11
  "dev": "tsx bin/pls.tsx",
12
12
  "build": "tsc && node scripts/postbuild.js",
13
13
  "start": "node dist/bin/pls.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:ui": "vitest --ui",
17
+ "test:coverage": "vitest run --coverage",
14
18
  "link:dev": "mkdir -p ~/.local/bin && ln -sf \"$(pwd)/bin/pls.tsx\" ~/.local/bin/pls-dev && echo '✅ pls-dev 已链接到 ~/.local/bin/pls-dev'",
15
19
  "unlink:dev": "rm -f ~/.local/bin/pls-dev && echo '✅ pls-dev 已移除'",
16
20
  "prepublishOnly": "pnpm build"
@@ -55,6 +59,7 @@
55
59
  "chalk": "^5.6.2",
56
60
  "cli-highlight": "^2.1.11",
57
61
  "commander": "^14.0.2",
62
+ "detect-package-manager": "^3.0.2",
58
63
  "ink": "^6.5.1",
59
64
  "ink-box": "^2.0.0",
60
65
  "ink-markdown": "^1.0.4",
@@ -73,8 +78,11 @@
73
78
  "@types/hast": "^3.0.4",
74
79
  "@types/node": "^25.0.2",
75
80
  "@types/react": "^19.2.7",
81
+ "@vitest/coverage-v8": "^4.0.16",
82
+ "@vitest/ui": "^4.0.16",
76
83
  "react-devtools-core": "^7.0.1",
77
84
  "tsx": "^4.21.0",
78
- "typescript": "^5.9.3"
85
+ "typescript": "^5.9.3",
86
+ "vitest": "^4.0.16"
79
87
  }
80
88
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 命令生成工作流集成测试
3
+ * 测试用户输入 → AI生成 → 确认 → 执行 → 成功/失败 的完整流程
4
+ */
5
+ export {};