@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
@@ -0,0 +1,170 @@
1
+ /**
2
+ * 平台检测和通用函数测试
3
+ * 测试基础的平台判断函数和其他通用工具
4
+ */
5
+
6
+ import { describe, it, expect, afterEach } from 'vitest'
7
+ import { isWindows, isMacOS, isLinux, getDefaultShell, getConfigDir, getPowerShellConfigDir } from '../platform'
8
+
9
+ describe('Platform Detection', () => {
10
+ const originalPlatform = process.platform
11
+
12
+ afterEach(() => {
13
+ Object.defineProperty(process, 'platform', {
14
+ value: originalPlatform,
15
+ writable: true,
16
+ })
17
+ })
18
+
19
+ it('isWindows() 在 Windows 平台应该返回 true', () => {
20
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
21
+ expect(isWindows()).toBe(true)
22
+ expect(isMacOS()).toBe(false)
23
+ expect(isLinux()).toBe(false)
24
+ })
25
+
26
+ it('isMacOS() 在 macOS 平台应该返回 true', () => {
27
+ Object.defineProperty(process, 'platform', { value: 'darwin', writable: true })
28
+ expect(isWindows()).toBe(false)
29
+ expect(isMacOS()).toBe(true)
30
+ expect(isLinux()).toBe(false)
31
+ })
32
+
33
+ it('isLinux() 在 Linux 平台应该返回 true', () => {
34
+ Object.defineProperty(process, 'platform', { value: 'linux', writable: true })
35
+ expect(isWindows()).toBe(false)
36
+ expect(isMacOS()).toBe(false)
37
+ expect(isLinux()).toBe(true)
38
+ })
39
+
40
+ it('应该只有一个平台为 true', () => {
41
+ // 无论在哪个平台,三个函数应该只有一个返回 true
42
+ const results = [isWindows(), isMacOS(), isLinux()]
43
+ const trueCount = results.filter(r => r === true).length
44
+ expect(trueCount).toBe(1)
45
+ })
46
+ })
47
+
48
+ describe('getDefaultShell', () => {
49
+ const originalPlatform = process.platform
50
+ const originalEnv = { ...process.env }
51
+
52
+ afterEach(() => {
53
+ Object.defineProperty(process, 'platform', {
54
+ value: originalPlatform,
55
+ })
56
+ process.env = { ...originalEnv }
57
+ })
58
+
59
+ it('Unix 平台应该返回 $SHELL', () => {
60
+ Object.defineProperty(process, 'platform', { value: 'linux', writable: true })
61
+ process.env.SHELL = '/bin/zsh'
62
+
63
+ const shell = getDefaultShell()
64
+ expect(shell).toBe('/bin/zsh')
65
+ })
66
+
67
+ it('Unix 平台无 $SHELL 应该降级到 /bin/bash', () => {
68
+ Object.defineProperty(process, 'platform', { value: 'linux', writable: true })
69
+ delete process.env.SHELL
70
+
71
+ const shell = getDefaultShell()
72
+ expect(shell).toBe('/bin/bash')
73
+ })
74
+
75
+ it('Windows 平台 PowerShell 7 应该返回 pwsh.exe', () => {
76
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
77
+ process.env.PSModulePath = 'C:\\Program Files\\PowerShell\\7\\Modules'
78
+
79
+ const shell = getDefaultShell()
80
+ expect(shell).toBe('pwsh.exe')
81
+ })
82
+
83
+ it('Windows 平台 PowerShell 5 应该返回 powershell.exe', () => {
84
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
85
+ process.env.PSModulePath = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules'
86
+ delete process.env.PROMPT
87
+
88
+ const shell = getDefaultShell()
89
+ expect(shell).toBe('powershell.exe')
90
+ })
91
+
92
+ it('Windows 平台 CMD 应该返回 cmd.exe 或 $COMSPEC', () => {
93
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
94
+ process.env.PROMPT = '$P$G'
95
+ delete process.env.PSModulePath
96
+ delete process.env.COMSPEC
97
+
98
+ const shell = getDefaultShell()
99
+ expect(shell).toBe('cmd.exe')
100
+ })
101
+ })
102
+
103
+ describe('getConfigDir', () => {
104
+ it('应该返回 ~/.please 目录', () => {
105
+ const configDir = getConfigDir()
106
+ expect(configDir).toContain('.please')
107
+ expect(configDir).toMatch(/[\/\\]\.please$/)
108
+ })
109
+
110
+ it('应该使用用户 home 目录', () => {
111
+ const os = require('os')
112
+ const home = os.homedir()
113
+ const configDir = getConfigDir()
114
+
115
+ expect(configDir).toContain(home)
116
+ })
117
+ })
118
+
119
+ describe('getPowerShellConfigDir', () => {
120
+ it('应该返回 PowerShell 变量格式', () => {
121
+ const psDir = getPowerShellConfigDir()
122
+ expect(psDir).toBe('$env:USERPROFILE\\.please')
123
+ })
124
+
125
+ it('返回值应该是 PowerShell 可识别的路径', () => {
126
+ const psDir = getPowerShellConfigDir()
127
+
128
+ // PowerShell 路径应该:
129
+ // 1. 使用 $env:USERPROFILE 而不是硬编码路径
130
+ // 2. 使用反斜杠(Windows 风格)
131
+ expect(psDir).toMatch(/^\$env:USERPROFILE/)
132
+ expect(psDir).toContain('\\')
133
+ })
134
+ })
135
+
136
+ describe('Integration - Platform 和 Shell 检测配合', () => {
137
+ const originalPlatform = process.platform
138
+ const originalEnv = { ...process.env }
139
+
140
+ afterEach(() => {
141
+ Object.defineProperty(process, 'platform', {
142
+ value: originalPlatform,
143
+ })
144
+ process.env = { ...originalEnv }
145
+ })
146
+
147
+ it('macOS + Zsh 应该正常工作', () => {
148
+ Object.defineProperty(process, 'platform', { value: 'darwin', writable: true })
149
+ process.env.SHELL = '/bin/zsh'
150
+
151
+ expect(isMacOS()).toBe(true)
152
+ expect(getDefaultShell()).toBe('/bin/zsh')
153
+ })
154
+
155
+ it('Linux + Bash 应该正常工作', () => {
156
+ Object.defineProperty(process, 'platform', { value: 'linux', writable: true })
157
+ process.env.SHELL = '/bin/bash'
158
+
159
+ expect(isLinux()).toBe(true)
160
+ expect(getDefaultShell()).toBe('/bin/bash')
161
+ })
162
+
163
+ it('Windows + PowerShell 7 应该正常工作', () => {
164
+ Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
165
+ process.env.PSModulePath = 'C:\\Program Files\\PowerShell\\7\\Modules'
166
+
167
+ expect(isWindows()).toBe(true)
168
+ expect(getDefaultShell()).toBe('pwsh.exe')
169
+ })
170
+ })
@@ -0,0 +1,431 @@
1
+ import { execSync } from 'child_process'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ /**
6
+ * 跨平台工具函数
7
+ * 封装所有平台相关的逻辑,优先支持 macOS/Linux,兼容 Windows
8
+ */
9
+
10
+ // ================== 类型定义 ==================
11
+
12
+ /**
13
+ * Shell 类型
14
+ */
15
+ export type ShellType =
16
+ | 'zsh'
17
+ | 'bash'
18
+ | 'fish'
19
+ | 'cmd' // Windows CMD
20
+ | 'powershell5' // Windows PowerShell 5.x
21
+ | 'powershell7' // PowerShell Core 7+ (pwsh)
22
+ | 'unknown'
23
+
24
+ /**
25
+ * Shell 能力
26
+ */
27
+ export interface ShellCapabilities {
28
+ /** 是否支持 Hook(修改配置文件) */
29
+ supportsHook: boolean
30
+ /** 是否支持历史读取 */
31
+ supportsHistory: boolean
32
+ /** 配置文件路径 */
33
+ configPath: string | null
34
+ /** 历史文件路径 */
35
+ historyPath: string | null
36
+ /** 用于执行命令的 Shell 可执行文件 */
37
+ executable: string
38
+ /** Shell 名称(用于显示) */
39
+ displayName: string
40
+ }
41
+
42
+ // ================== 平台检测 ==================
43
+
44
+ /**
45
+ * 是否为 Windows 平台
46
+ */
47
+ export function isWindows(): boolean {
48
+ return process.platform === 'win32'
49
+ }
50
+
51
+ /**
52
+ * 是否为 macOS 平台
53
+ */
54
+ export function isMacOS(): boolean {
55
+ return process.platform === 'darwin'
56
+ }
57
+
58
+ /**
59
+ * 是否为 Linux 平台
60
+ */
61
+ export function isLinux(): boolean {
62
+ return process.platform === 'linux'
63
+ }
64
+
65
+ // ================== Shell 检测 ==================
66
+
67
+ /**
68
+ * 检测 Windows Shell 类型
69
+ */
70
+ function detectWindowsShell(): ShellType {
71
+ // 1. 检查 PSModulePath 判断 PowerShell 版本
72
+ const psModulePath = process.env.PSModulePath || ''
73
+
74
+ if (psModulePath) {
75
+ // PowerShell 7+ 的 PSModulePath 包含 "PowerShell\7" 或 "PowerShell/7"
76
+ if (/PowerShell[\/\\]7/i.test(psModulePath)) {
77
+ return 'powershell7'
78
+ }
79
+ // Windows PowerShell 5.x 的 PSModulePath 包含 "WindowsPowerShell"
80
+ if (/WindowsPowerShell/i.test(psModulePath)) {
81
+ return 'powershell5'
82
+ }
83
+ }
84
+
85
+ // 2. 检查 PROMPT 环境变量(CMD 特有)
86
+ // 注意:在 PowerShell 中运行 CMD 时也可能有 PROMPT
87
+ if (process.env.PROMPT && !psModulePath) {
88
+ return 'cmd'
89
+ }
90
+
91
+ // 3. 尝试检测 pwsh 是否可用
92
+ if (commandExists('pwsh')) {
93
+ return 'powershell7'
94
+ }
95
+
96
+ // 4. 默认 PowerShell 5(Windows 内置)
97
+ return 'powershell5'
98
+ }
99
+
100
+ /**
101
+ * 检测 Unix Shell 类型
102
+ */
103
+ function detectUnixShell(): ShellType {
104
+ const shell = process.env.SHELL || ''
105
+
106
+ if (shell.includes('zsh')) return 'zsh'
107
+ if (shell.includes('bash')) return 'bash'
108
+ if (shell.includes('fish')) return 'fish'
109
+
110
+ return 'unknown'
111
+ }
112
+
113
+ /**
114
+ * 检测当前 Shell 类型
115
+ */
116
+ export function detectShell(): ShellType {
117
+ if (isWindows()) {
118
+ return detectWindowsShell()
119
+ }
120
+ return detectUnixShell()
121
+ }
122
+
123
+ // ================== Shell 能力 ==================
124
+
125
+ /**
126
+ * 获取 Shell 配置文件路径
127
+ */
128
+ function getShellConfigPath(shell: ShellType): string | null {
129
+ const home = os.homedir()
130
+
131
+ switch (shell) {
132
+ case 'zsh':
133
+ return path.join(home, '.zshrc')
134
+
135
+ case 'bash':
136
+ // macOS 使用 .bash_profile,Linux 使用 .bashrc
137
+ return isMacOS()
138
+ ? path.join(home, '.bash_profile')
139
+ : path.join(home, '.bashrc')
140
+
141
+ case 'fish':
142
+ return path.join(home, '.config', 'fish', 'config.fish')
143
+
144
+ case 'powershell5':
145
+ return path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
146
+
147
+ case 'powershell7':
148
+ return path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
149
+
150
+ case 'cmd':
151
+ return null // CMD 不支持配置文件
152
+
153
+ default:
154
+ return null
155
+ }
156
+ }
157
+
158
+ /**
159
+ * 获取 Shell 历史文件路径
160
+ */
161
+ function getShellHistoryPath(shell: ShellType): string | null {
162
+ const home = os.homedir()
163
+
164
+ switch (shell) {
165
+ case 'zsh':
166
+ return process.env.HISTFILE || path.join(home, '.zsh_history')
167
+
168
+ case 'bash':
169
+ return process.env.HISTFILE || path.join(home, '.bash_history')
170
+
171
+ case 'fish':
172
+ return path.join(home, '.local', 'share', 'fish', 'fish_history')
173
+
174
+ case 'powershell5':
175
+ case 'powershell7':
176
+ // PowerShell 历史文件位置(PSReadLine)
177
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming')
178
+ return path.join(appData, 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt')
179
+
180
+ case 'cmd':
181
+ return null // CMD 不持久化历史
182
+
183
+ default:
184
+ return null
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 获取 Shell 可执行文件
190
+ */
191
+ function getShellExecutable(shell: ShellType): string {
192
+ switch (shell) {
193
+ case 'zsh':
194
+ return process.env.SHELL || '/bin/zsh'
195
+
196
+ case 'bash':
197
+ return process.env.SHELL || '/bin/bash'
198
+
199
+ case 'fish':
200
+ return process.env.SHELL || '/usr/bin/fish'
201
+
202
+ case 'powershell5':
203
+ return 'powershell.exe'
204
+
205
+ case 'powershell7':
206
+ return 'pwsh.exe'
207
+
208
+ case 'cmd':
209
+ return process.env.COMSPEC || 'cmd.exe'
210
+
211
+ default:
212
+ return isWindows() ? 'powershell.exe' : '/bin/sh'
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 获取 Shell 显示名称
218
+ */
219
+ function getShellDisplayName(shell: ShellType): string {
220
+ switch (shell) {
221
+ case 'zsh': return 'Zsh'
222
+ case 'bash': return 'Bash'
223
+ case 'fish': return 'Fish'
224
+ case 'cmd': return 'CMD'
225
+ case 'powershell5': return 'PowerShell 5.x'
226
+ case 'powershell7': return 'PowerShell 7+'
227
+ default: return 'Unknown'
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 获取 Shell 能力信息
233
+ */
234
+ export function getShellCapabilities(shell: ShellType): ShellCapabilities {
235
+ return {
236
+ supportsHook: shell !== 'cmd' && shell !== 'unknown',
237
+ supportsHistory: shell !== 'cmd' && shell !== 'unknown',
238
+ configPath: getShellConfigPath(shell),
239
+ historyPath: getShellHistoryPath(shell),
240
+ executable: getShellExecutable(shell),
241
+ displayName: getShellDisplayName(shell),
242
+ }
243
+ }
244
+
245
+ // ================== 命令检测 ==================
246
+
247
+ /**
248
+ * 检测命令是否存在(跨平台)
249
+ */
250
+ export function commandExists(command: string): boolean {
251
+ try {
252
+ if (isWindows()) {
253
+ // Windows: 使用 where 命令
254
+ execSync(`where ${command}`, { stdio: 'ignore' })
255
+ } else {
256
+ // Unix: 使用 command -v(比 which 更可靠)
257
+ execSync(`command -v ${command}`, { stdio: 'ignore', shell: '/bin/sh' })
258
+ }
259
+ return true
260
+ } catch {
261
+ return false
262
+ }
263
+ }
264
+
265
+ /**
266
+ * 批量检测命令是否存在(优化性能)
267
+ * @returns 返回存在的命令列表
268
+ */
269
+ export function batchCommandExists(commands: string[]): string[] {
270
+ if (commands.length === 0) return []
271
+
272
+ const available: string[] = []
273
+
274
+ if (isWindows()) {
275
+ // Windows: 使用 PowerShell 批量检测
276
+ const batchSize = 20
277
+ for (let i = 0; i < commands.length; i += batchSize) {
278
+ const batch = commands.slice(i, i + batchSize)
279
+ try {
280
+ // 使用 Get-Command 批量检测
281
+ const script = batch
282
+ .map(cmd => `if(Get-Command ${cmd} -ErrorAction SilentlyContinue){Write-Output ${cmd}}`)
283
+ .join(';')
284
+ const result = execSync(`powershell -NoProfile -Command "${script}"`, {
285
+ encoding: 'utf-8',
286
+ stdio: ['pipe', 'pipe', 'ignore'],
287
+ timeout: 5000,
288
+ })
289
+ available.push(...result.trim().split(/\r?\n/).filter(Boolean))
290
+ } catch {
291
+ // 这批失败,逐个检测
292
+ for (const cmd of batch) {
293
+ if (commandExists(cmd)) {
294
+ available.push(cmd)
295
+ }
296
+ }
297
+ }
298
+ }
299
+ } else {
300
+ // Unix: 使用 shell 批量检测
301
+ const batchSize = 20
302
+ for (let i = 0; i < commands.length; i += batchSize) {
303
+ const batch = commands.slice(i, i + batchSize)
304
+ const script = `(${batch
305
+ .map(cmd => `command -v ${cmd} >/dev/null 2>&1 && echo ${cmd}`)
306
+ .join('; ')}) 2>/dev/null || true`
307
+
308
+ try {
309
+ const result = execSync(script, {
310
+ encoding: 'utf-8',
311
+ stdio: ['pipe', 'pipe', 'ignore'],
312
+ timeout: 500,
313
+ })
314
+ available.push(...result.trim().split('\n').filter(Boolean))
315
+ } catch {
316
+ // 这批失败,跳过
317
+ }
318
+ }
319
+ }
320
+
321
+ return available
322
+ }
323
+
324
+ // ================== 命令执行 ==================
325
+
326
+ /**
327
+ * 命令执行配置
328
+ */
329
+ export interface ShellExecConfig {
330
+ /** Shell 可执行文件 */
331
+ shell: string
332
+ /** Shell 参数 */
333
+ args: string[]
334
+ /** 完整的命令字符串(已包含错误处理) */
335
+ command: string
336
+ }
337
+
338
+ /**
339
+ * 构建命令执行配置
340
+ * 处理不同 Shell 的语法差异
341
+ */
342
+ export function buildShellExecConfig(command: string, shell?: ShellType): ShellExecConfig {
343
+ const currentShell = shell || detectShell()
344
+ const executable = getShellExecutable(currentShell)
345
+
346
+ switch (currentShell) {
347
+ case 'bash':
348
+ return {
349
+ shell: executable,
350
+ args: ['-c', `set -o pipefail; ${command}`],
351
+ command: `set -o pipefail; ${command}`,
352
+ }
353
+
354
+ case 'zsh':
355
+ return {
356
+ shell: executable,
357
+ args: ['-c', `setopt pipefail; ${command}`],
358
+ command: `setopt pipefail; ${command}`,
359
+ }
360
+
361
+ case 'fish':
362
+ // Fish 不需要特殊处理 pipefail
363
+ return {
364
+ shell: executable,
365
+ args: ['-c', command],
366
+ command,
367
+ }
368
+
369
+ case 'powershell5':
370
+ case 'powershell7':
371
+ // PowerShell: 使用 $ErrorActionPreference 处理错误
372
+ // -NoProfile 加快启动速度
373
+ // -Command 执行命令
374
+ return {
375
+ shell: executable,
376
+ args: ['-NoProfile', '-Command', command],
377
+ command,
378
+ }
379
+
380
+ case 'cmd':
381
+ return {
382
+ shell: executable,
383
+ args: ['/c', command],
384
+ command,
385
+ }
386
+
387
+ default:
388
+ // 默认使用 sh
389
+ return {
390
+ shell: isWindows() ? 'powershell.exe' : '/bin/sh',
391
+ args: [isWindows() ? '-Command' : '-c', command],
392
+ command,
393
+ }
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 获取默认 Shell(用于交互式执行)
399
+ */
400
+ export function getDefaultShell(): string {
401
+ if (isWindows()) {
402
+ const shell = detectWindowsShell()
403
+ return getShellExecutable(shell)
404
+ }
405
+ return process.env.SHELL || '/bin/bash'
406
+ }
407
+
408
+ // ================== 路径处理 ==================
409
+
410
+ /**
411
+ * 获取 pls 配置目录
412
+ * 统一使用 ~/.please
413
+ */
414
+ export function getConfigDir(): string {
415
+ return path.join(os.homedir(), '.please')
416
+ }
417
+
418
+ /**
419
+ * 将路径转换为当前平台格式
420
+ */
421
+ export function normalizePath(p: string): string {
422
+ return path.normalize(p)
423
+ }
424
+
425
+ /**
426
+ * 获取用于 PowerShell 脚本中的路径
427
+ * 使用 $env:USERPROFILE 而不是硬编码路径
428
+ */
429
+ export function getPowerShellConfigDir(): string {
430
+ return '$env:USERPROFILE\\.please'
431
+ }