@yivan-lab/pretty-please 1.4.0 → 1.5.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 +32 -4
- package/bin/pls.tsx +153 -35
- package/dist/bin/pls.js +126 -23
- package/dist/package.json +10 -2
- package/dist/src/__integration__/command-generation.test.d.ts +5 -0
- package/dist/src/__integration__/command-generation.test.js +508 -0
- package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
- package/dist/src/__integration__/error-recovery.test.js +511 -0
- package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
- package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
- package/dist/src/__tests__/alias.test.d.ts +5 -0
- package/dist/src/__tests__/alias.test.js +421 -0
- package/dist/src/__tests__/chat-history.test.d.ts +5 -0
- package/dist/src/__tests__/chat-history.test.js +372 -0
- package/dist/src/__tests__/config.test.d.ts +5 -0
- package/dist/src/__tests__/config.test.js +822 -0
- package/dist/src/__tests__/history.test.d.ts +5 -0
- package/dist/src/__tests__/history.test.js +439 -0
- package/dist/src/__tests__/remote-history.test.d.ts +5 -0
- package/dist/src/__tests__/remote-history.test.js +641 -0
- package/dist/src/__tests__/remote.test.d.ts +5 -0
- package/dist/src/__tests__/remote.test.js +689 -0
- package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook-install.test.js +413 -0
- package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
- package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook.test.js +440 -0
- package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
- package/dist/src/__tests__/sysinfo.test.js +572 -0
- package/dist/src/__tests__/system-history.test.d.ts +5 -0
- package/dist/src/__tests__/system-history.test.js +457 -0
- package/dist/src/components/Chat.js +9 -28
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +30 -2
- package/dist/src/mastra-chat.js +6 -3
- package/dist/src/multi-step.js +6 -3
- package/dist/src/project-context.d.ts +22 -0
- package/dist/src/project-context.js +168 -0
- package/dist/src/prompts.d.ts +4 -4
- package/dist/src/prompts.js +23 -6
- package/dist/src/shell-hook.d.ts +13 -0
- package/dist/src/shell-hook.js +163 -33
- package/dist/src/sysinfo.d.ts +38 -9
- package/dist/src/sysinfo.js +245 -21
- package/dist/src/system-history.d.ts +5 -0
- package/dist/src/system-history.js +64 -18
- package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
- package/dist/src/ui/__tests__/theme.test.js +688 -0
- package/dist/src/upgrade.js +3 -0
- package/dist/src/user-preferences.d.ts +44 -0
- package/dist/src/user-preferences.js +147 -0
- package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
- package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
- package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
- package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform.test.js +137 -0
- package/dist/src/utils/platform.d.ts +88 -0
- package/dist/src/utils/platform.js +331 -0
- package/package.json +10 -2
- package/src/__integration__/command-generation.test.ts +602 -0
- package/src/__integration__/error-recovery.test.ts +620 -0
- package/src/__integration__/shell-hook-workflow.test.ts +457 -0
- package/src/__tests__/alias.test.ts +545 -0
- package/src/__tests__/chat-history.test.ts +462 -0
- package/src/__tests__/config.test.ts +1043 -0
- package/src/__tests__/history.test.ts +538 -0
- package/src/__tests__/remote-history.test.ts +791 -0
- package/src/__tests__/remote.test.ts +866 -0
- package/src/__tests__/shell-hook-install.test.ts +510 -0
- package/src/__tests__/shell-hook-remote.test.ts +679 -0
- package/src/__tests__/shell-hook.test.ts +564 -0
- package/src/__tests__/sysinfo.test.ts +718 -0
- package/src/__tests__/system-history.test.ts +608 -0
- package/src/components/Chat.tsx +10 -37
- package/src/config.ts +29 -2
- package/src/mastra-chat.ts +8 -3
- package/src/multi-step.ts +7 -2
- package/src/project-context.ts +191 -0
- package/src/prompts.ts +26 -5
- package/src/shell-hook.ts +179 -33
- package/src/sysinfo.ts +326 -25
- package/src/system-history.ts +67 -14
- package/src/ui/__tests__/theme.test.ts +869 -0
- package/src/upgrade.ts +5 -0
- package/src/user-preferences.ts +178 -0
- package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
- package/src/utils/__tests__/platform-exec.test.ts +278 -0
- package/src/utils/__tests__/platform-shell.test.ts +353 -0
- package/src/utils/__tests__/platform.test.ts +170 -0
- 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
|
+
}
|