@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.
- package/README.md +250 -620
- package/bin/pls.tsx +178 -40
- package/dist/bin/pls.js +149 -27
- 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 +10 -6
- package/dist/src/multi-step.js +10 -8
- 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 +32 -0
- package/dist/src/shell-hook.js +226 -33
- package/dist/src/sysinfo.d.ts +38 -9
- package/dist/src/sysinfo.js +245 -21
- package/dist/src/system-history.d.ts +18 -0
- package/dist/src/system-history.js +151 -0
- 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 +12 -5
- package/src/multi-step.ts +11 -5
- package/src/project-context.ts +191 -0
- package/src/prompts.ts +26 -5
- package/src/shell-hook.ts +254 -32
- package/src/sysinfo.ts +326 -25
- package/src/system-history.ts +170 -0
- 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
package/dist/src/upgrade.js
CHANGED
|
@@ -323,6 +323,9 @@ pause
|
|
|
323
323
|
}
|
|
324
324
|
console2.muted('━'.repeat(40));
|
|
325
325
|
console2.success(`升级成功: ${currentVersion} → ${latestVersion}`);
|
|
326
|
+
// 升级成功后,重装 shell hook(如果已启用)
|
|
327
|
+
const { reinstallShellHook } = await import('./shell-hook.js');
|
|
328
|
+
await reinstallShellHook({ reason: '版本升级,更新 Shell Hook 脚本' });
|
|
326
329
|
console.log('');
|
|
327
330
|
return true;
|
|
328
331
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户命令偏好统计模块
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 读取和分析 ~/.please/command_stats.txt
|
|
6
|
+
* - 获取用户最常用的命令
|
|
7
|
+
* - 格式化为 AI 可理解的字符串
|
|
8
|
+
* - 智能过滤非偏好命令(Shell 内置、系统通用命令等)
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* 命令统计接口
|
|
12
|
+
*/
|
|
13
|
+
export interface CommandStat {
|
|
14
|
+
command: string;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 获取所有命令统计数据
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCommandStats(): Record<string, number>;
|
|
21
|
+
/**
|
|
22
|
+
* 获取使用频率最高的命令(智能过滤版)
|
|
23
|
+
* @param limit 可选的数量限制,不传则使用配置中的 userPreferencesTopK
|
|
24
|
+
*/
|
|
25
|
+
export declare function getTopCommands(limit?: number): CommandStat[];
|
|
26
|
+
/**
|
|
27
|
+
* 格式化用户偏好为 AI 可理解的字符串
|
|
28
|
+
*
|
|
29
|
+
* 示例输出:
|
|
30
|
+
* "用户偏好: git(234), eza(156), vim(89), docker(67), pnpm(45)"
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatUserPreferences(): string;
|
|
33
|
+
/**
|
|
34
|
+
* 清空统计数据
|
|
35
|
+
*/
|
|
36
|
+
export declare function clearCommandStats(): void;
|
|
37
|
+
/**
|
|
38
|
+
* 获取统计文件路径(用于 CLI 展示)
|
|
39
|
+
*/
|
|
40
|
+
export declare function getStatsFilePath(): string;
|
|
41
|
+
/**
|
|
42
|
+
* 显示统计信息(用于 CLI)
|
|
43
|
+
*/
|
|
44
|
+
export declare function displayCommandStats(): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户命令偏好统计模块
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 读取和分析 ~/.please/command_stats.txt
|
|
6
|
+
* - 获取用户最常用的命令
|
|
7
|
+
* - 格式化为 AI 可理解的字符串
|
|
8
|
+
* - 智能过滤非偏好命令(Shell 内置、系统通用命令等)
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { CONFIG_DIR, getConfig } from './config.js';
|
|
13
|
+
const STATS_FILE = path.join(CONFIG_DIR, 'command_stats.txt');
|
|
14
|
+
/**
|
|
15
|
+
* 命令黑名单:这些命令不算"用户偏好"
|
|
16
|
+
* - Shell 内置命令:cd、export、source 等(必须用的,不是偏好)
|
|
17
|
+
* - 系统基础命令:ls、cat、grep 等(太基础,不反映偏好)
|
|
18
|
+
* - 系统通用命令:clear、exit、history 等(通用命令,不是偏好)
|
|
19
|
+
* - 查询命令:man、which、type 等(查询用途,不是偏好)
|
|
20
|
+
* - 权限命令:sudo、doas 等(权限提升,不是偏好)
|
|
21
|
+
* - pls 自身:pls-dev、pls、please(自引用)
|
|
22
|
+
*/
|
|
23
|
+
const COMMAND_BLACKLIST = new Set([
|
|
24
|
+
// Shell 内置命令
|
|
25
|
+
'cd', 'pushd', 'popd', 'dirs',
|
|
26
|
+
'export', 'set', 'unset', 'declare', 'local', 'readonly',
|
|
27
|
+
'alias', 'unalias',
|
|
28
|
+
'source', '.',
|
|
29
|
+
'history', 'fc',
|
|
30
|
+
'jobs', 'fg', 'bg', 'disown',
|
|
31
|
+
'eval', 'exec', 'builtin', 'command',
|
|
32
|
+
'true', 'false', ':', 'test', '[',
|
|
33
|
+
// 系统基础命令(太基础,不反映偏好)
|
|
34
|
+
'ls', 'cat', 'grep', 'find', 'head', 'tail',
|
|
35
|
+
'cp', 'mv', 'rm', 'mkdir', 'rmdir', 'touch',
|
|
36
|
+
'chmod', 'chown', 'ln',
|
|
37
|
+
'wc', 'sort', 'uniq', 'cut', 'tr', 'sed', 'awk',
|
|
38
|
+
// 系统通用命令(不算偏好)
|
|
39
|
+
'clear', 'reset',
|
|
40
|
+
'exit', 'logout',
|
|
41
|
+
'pwd',
|
|
42
|
+
'echo', 'printf',
|
|
43
|
+
'sleep', 'wait',
|
|
44
|
+
'kill', 'killall', 'pkill',
|
|
45
|
+
// 查询命令
|
|
46
|
+
'man', 'which', 'type', 'whereis', 'whatis', 'apropos',
|
|
47
|
+
'help', 'info',
|
|
48
|
+
// 权限命令
|
|
49
|
+
'sudo', 'doas', 'su',
|
|
50
|
+
// pls 自身
|
|
51
|
+
'pls', 'pls-dev', 'please',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* 确保统计文件存在
|
|
55
|
+
*/
|
|
56
|
+
function ensureStatsFile() {
|
|
57
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
58
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
if (!fs.existsSync(STATS_FILE)) {
|
|
61
|
+
fs.writeFileSync(STATS_FILE, '', 'utf-8');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 获取所有命令统计数据
|
|
66
|
+
*/
|
|
67
|
+
export function getCommandStats() {
|
|
68
|
+
ensureStatsFile();
|
|
69
|
+
const content = fs.readFileSync(STATS_FILE, 'utf-8');
|
|
70
|
+
const stats = {};
|
|
71
|
+
for (const line of content.split('\n')) {
|
|
72
|
+
if (!line.trim())
|
|
73
|
+
continue;
|
|
74
|
+
const [cmd, count] = line.split('=');
|
|
75
|
+
if (cmd && count) {
|
|
76
|
+
stats[cmd] = parseInt(count, 10);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return stats;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 获取使用频率最高的命令(智能过滤版)
|
|
83
|
+
* @param limit 可选的数量限制,不传则使用配置中的 userPreferencesTopK
|
|
84
|
+
*/
|
|
85
|
+
export function getTopCommands(limit) {
|
|
86
|
+
const config = getConfig();
|
|
87
|
+
const topK = limit !== undefined ? limit : config.userPreferencesTopK;
|
|
88
|
+
const stats = getCommandStats();
|
|
89
|
+
return Object.entries(stats)
|
|
90
|
+
.filter(([command]) => !COMMAND_BLACKLIST.has(command)) // 过滤黑名单
|
|
91
|
+
.map(([command, count]) => ({ command, count }))
|
|
92
|
+
.sort((a, b) => b.count - a.count)
|
|
93
|
+
.slice(0, topK);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 格式化用户偏好为 AI 可理解的字符串
|
|
97
|
+
*
|
|
98
|
+
* 示例输出:
|
|
99
|
+
* "用户偏好: git(234), eza(156), vim(89), docker(67), pnpm(45)"
|
|
100
|
+
*/
|
|
101
|
+
export function formatUserPreferences() {
|
|
102
|
+
const top = getTopCommands(); // 使用配置中的 topK
|
|
103
|
+
if (top.length === 0)
|
|
104
|
+
return '';
|
|
105
|
+
const lines = top.map(({ command, count }) => `${command}(${count})`);
|
|
106
|
+
return `用户偏好: ${lines.join(', ')}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 清空统计数据
|
|
110
|
+
*/
|
|
111
|
+
export function clearCommandStats() {
|
|
112
|
+
ensureStatsFile();
|
|
113
|
+
fs.writeFileSync(STATS_FILE, '', 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 获取统计文件路径(用于 CLI 展示)
|
|
117
|
+
*/
|
|
118
|
+
export function getStatsFilePath() {
|
|
119
|
+
return STATS_FILE;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 显示统计信息(用于 CLI)
|
|
123
|
+
*/
|
|
124
|
+
export function displayCommandStats() {
|
|
125
|
+
const config = getConfig();
|
|
126
|
+
const stats = getCommandStats();
|
|
127
|
+
const totalCommands = Object.keys(stats).length;
|
|
128
|
+
const totalExecutions = Object.values(stats).reduce((sum, count) => sum + count, 0);
|
|
129
|
+
if (totalCommands === 0) {
|
|
130
|
+
console.log('\n暂无命令统计数据');
|
|
131
|
+
console.log('提示: 安装并启用 Shell Hook 后会自动开始统计\n');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const displayLimit = config.userPreferencesTopK; // 使用配置项
|
|
135
|
+
const top = getTopCommands(displayLimit);
|
|
136
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
137
|
+
console.log(`📊 命令使用统计`);
|
|
138
|
+
console.log(`总命令数: ${totalCommands}, 总执行次数: ${totalExecutions}`);
|
|
139
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
140
|
+
console.log(`\nTop ${displayLimit} 常用命令(已过滤非偏好命令):\n`);
|
|
141
|
+
top.forEach(({ command, count }, index) => {
|
|
142
|
+
const percentage = ((count / totalExecutions) * 100).toFixed(1);
|
|
143
|
+
const bar = '█'.repeat(Math.floor(count / top[0].count * 20));
|
|
144
|
+
console.log(`${String(index + 1).padStart(2)}. ${command.padEnd(15)} ${bar} ${count} (${percentage}%)`);
|
|
145
|
+
});
|
|
146
|
+
console.log(`\n统计文件: ${STATS_FILE}\n`);
|
|
147
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell 能力专项测试
|
|
3
|
+
* 测试各 Shell 的配置文件、历史文件、能力矩阵等
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { getShellCapabilities } from '../platform';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
describe('Shell Capabilities - 配置文件路径', () => {
|
|
10
|
+
const originalPlatform = process.platform;
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
Object.defineProperty(process, 'platform', {
|
|
14
|
+
value: originalPlatform,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it('Zsh 应该返回 ~/.zshrc', () => {
|
|
18
|
+
const caps = getShellCapabilities('zsh');
|
|
19
|
+
expect(caps.configPath).toBe(path.join(home, '.zshrc'));
|
|
20
|
+
});
|
|
21
|
+
it('Bash 在 macOS 应该返回 ~/.bash_profile', () => {
|
|
22
|
+
Object.defineProperty(process, 'platform', { value: 'darwin', writable: true });
|
|
23
|
+
const caps = getShellCapabilities('bash');
|
|
24
|
+
expect(caps.configPath).toBe(path.join(home, '.bash_profile'));
|
|
25
|
+
});
|
|
26
|
+
it('Bash 在 Linux 应该返回 ~/.bashrc', () => {
|
|
27
|
+
Object.defineProperty(process, 'platform', { value: 'linux', writable: true });
|
|
28
|
+
const caps = getShellCapabilities('bash');
|
|
29
|
+
expect(caps.configPath).toBe(path.join(home, '.bashrc'));
|
|
30
|
+
});
|
|
31
|
+
it('Fish 应该返回 ~/.config/fish/config.fish', () => {
|
|
32
|
+
const caps = getShellCapabilities('fish');
|
|
33
|
+
expect(caps.configPath).toBe(path.join(home, '.config', 'fish', 'config.fish'));
|
|
34
|
+
});
|
|
35
|
+
it('PowerShell 5 应该返回正确的 profile 路径', () => {
|
|
36
|
+
const caps = getShellCapabilities('powershell5');
|
|
37
|
+
expect(caps.configPath).toBe(path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
|
|
38
|
+
});
|
|
39
|
+
it('PowerShell 7 应该返回正确的 profile 路径', () => {
|
|
40
|
+
const caps = getShellCapabilities('powershell7');
|
|
41
|
+
expect(caps.configPath).toBe(path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
|
|
42
|
+
});
|
|
43
|
+
it('CMD 应该返回 null (不支持配置文件)', () => {
|
|
44
|
+
const caps = getShellCapabilities('cmd');
|
|
45
|
+
expect(caps.configPath).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it('Unknown 应该返回 null', () => {
|
|
48
|
+
const caps = getShellCapabilities('unknown');
|
|
49
|
+
expect(caps.configPath).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('Shell Capabilities - 历史文件路径', () => {
|
|
53
|
+
const originalEnv = { ...process.env };
|
|
54
|
+
const home = os.homedir();
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
// 清空 HISTFILE 环境变量
|
|
57
|
+
delete process.env.HISTFILE;
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
process.env = { ...originalEnv };
|
|
61
|
+
});
|
|
62
|
+
it('Zsh 应该返回 ~/.zsh_history (默认)', () => {
|
|
63
|
+
const caps = getShellCapabilities('zsh');
|
|
64
|
+
expect(caps.historyPath).toBe(path.join(home, '.zsh_history'));
|
|
65
|
+
});
|
|
66
|
+
it('Zsh 应该使用 HISTFILE 环境变量覆盖', () => {
|
|
67
|
+
process.env.HISTFILE = '/custom/path/.zsh_history';
|
|
68
|
+
const caps = getShellCapabilities('zsh');
|
|
69
|
+
expect(caps.historyPath).toBe('/custom/path/.zsh_history');
|
|
70
|
+
});
|
|
71
|
+
it('Bash 应该返回 ~/.bash_history (默认)', () => {
|
|
72
|
+
const caps = getShellCapabilities('bash');
|
|
73
|
+
expect(caps.historyPath).toBe(path.join(home, '.bash_history'));
|
|
74
|
+
});
|
|
75
|
+
it('Bash 应该使用 HISTFILE 环境变量覆盖', () => {
|
|
76
|
+
process.env.HISTFILE = '/custom/path/.bash_history';
|
|
77
|
+
const caps = getShellCapabilities('bash');
|
|
78
|
+
expect(caps.historyPath).toBe('/custom/path/.bash_history');
|
|
79
|
+
});
|
|
80
|
+
it('Fish 应该返回 ~/.local/share/fish/fish_history', () => {
|
|
81
|
+
const caps = getShellCapabilities('fish');
|
|
82
|
+
expect(caps.historyPath).toBe(path.join(home, '.local', 'share', 'fish', 'fish_history'));
|
|
83
|
+
});
|
|
84
|
+
it('PowerShell 应该返回 PSReadLine 历史文件路径', () => {
|
|
85
|
+
const caps = getShellCapabilities('powershell5');
|
|
86
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
87
|
+
expect(caps.historyPath).toBe(path.join(appData, 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt'));
|
|
88
|
+
});
|
|
89
|
+
it('PowerShell 5 和 7 应该使用相同的历史文件', () => {
|
|
90
|
+
const caps5 = getShellCapabilities('powershell5');
|
|
91
|
+
const caps7 = getShellCapabilities('powershell7');
|
|
92
|
+
expect(caps5.historyPath).toBe(caps7.historyPath);
|
|
93
|
+
});
|
|
94
|
+
it('CMD 应该返回 null (不持久化历史)', () => {
|
|
95
|
+
const caps = getShellCapabilities('cmd');
|
|
96
|
+
expect(caps.historyPath).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
it('Unknown 应该返回 null', () => {
|
|
99
|
+
const caps = getShellCapabilities('unknown');
|
|
100
|
+
expect(caps.historyPath).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('Shell Capabilities - 能力矩阵', () => {
|
|
104
|
+
it('Zsh 应该支持 Hook 和历史', () => {
|
|
105
|
+
const caps = getShellCapabilities('zsh');
|
|
106
|
+
expect(caps.supportsHook).toBe(true);
|
|
107
|
+
expect(caps.supportsHistory).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('Bash 应该支持 Hook 和历史', () => {
|
|
110
|
+
const caps = getShellCapabilities('bash');
|
|
111
|
+
expect(caps.supportsHook).toBe(true);
|
|
112
|
+
expect(caps.supportsHistory).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it('Fish 应该支持 Hook 和历史', () => {
|
|
115
|
+
const caps = getShellCapabilities('fish');
|
|
116
|
+
expect(caps.supportsHook).toBe(true);
|
|
117
|
+
expect(caps.supportsHistory).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('PowerShell 5 应该支持 Hook 和历史', () => {
|
|
120
|
+
const caps = getShellCapabilities('powershell5');
|
|
121
|
+
expect(caps.supportsHook).toBe(true);
|
|
122
|
+
expect(caps.supportsHistory).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
it('PowerShell 7 应该支持 Hook 和历史', () => {
|
|
125
|
+
const caps = getShellCapabilities('powershell7');
|
|
126
|
+
expect(caps.supportsHook).toBe(true);
|
|
127
|
+
expect(caps.supportsHistory).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('CMD 不应该支持 Hook 和历史', () => {
|
|
130
|
+
const caps = getShellCapabilities('cmd');
|
|
131
|
+
expect(caps.supportsHook).toBe(false);
|
|
132
|
+
expect(caps.supportsHistory).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
it('Unknown 不应该支持 Hook 和历史', () => {
|
|
135
|
+
const caps = getShellCapabilities('unknown');
|
|
136
|
+
expect(caps.supportsHook).toBe(false);
|
|
137
|
+
expect(caps.supportsHistory).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('Shell Capabilities - 可执行文件', () => {
|
|
141
|
+
const originalEnv = { ...process.env };
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
process.env = { ...originalEnv };
|
|
144
|
+
});
|
|
145
|
+
it('Zsh 应该返回 $SHELL 或默认路径', () => {
|
|
146
|
+
delete process.env.SHELL;
|
|
147
|
+
const caps = getShellCapabilities('zsh');
|
|
148
|
+
expect(caps.executable).toBe('/bin/zsh');
|
|
149
|
+
});
|
|
150
|
+
it('Zsh 应该使用 $SHELL 环境变量', () => {
|
|
151
|
+
process.env.SHELL = '/usr/local/bin/zsh';
|
|
152
|
+
const caps = getShellCapabilities('zsh');
|
|
153
|
+
expect(caps.executable).toBe('/usr/local/bin/zsh');
|
|
154
|
+
});
|
|
155
|
+
it('Bash 应该返回 $SHELL 或默认路径', () => {
|
|
156
|
+
delete process.env.SHELL;
|
|
157
|
+
const caps = getShellCapabilities('bash');
|
|
158
|
+
expect(caps.executable).toBe('/bin/bash');
|
|
159
|
+
});
|
|
160
|
+
it('Fish 应该返回 $SHELL 或默认路径', () => {
|
|
161
|
+
delete process.env.SHELL;
|
|
162
|
+
const caps = getShellCapabilities('fish');
|
|
163
|
+
expect(caps.executable).toBe('/usr/bin/fish');
|
|
164
|
+
});
|
|
165
|
+
it('PowerShell 5 应该返回 powershell.exe', () => {
|
|
166
|
+
const caps = getShellCapabilities('powershell5');
|
|
167
|
+
expect(caps.executable).toBe('powershell.exe');
|
|
168
|
+
});
|
|
169
|
+
it('PowerShell 7 应该返回 pwsh.exe', () => {
|
|
170
|
+
const caps = getShellCapabilities('powershell7');
|
|
171
|
+
expect(caps.executable).toBe('pwsh.exe');
|
|
172
|
+
});
|
|
173
|
+
it('CMD 应该返回 $COMSPEC 或 cmd.exe', () => {
|
|
174
|
+
delete process.env.COMSPEC;
|
|
175
|
+
const caps = getShellCapabilities('cmd');
|
|
176
|
+
expect(caps.executable).toBe('cmd.exe');
|
|
177
|
+
});
|
|
178
|
+
it('CMD 应该使用 $COMSPEC 环境变量', () => {
|
|
179
|
+
process.env.COMSPEC = 'C:\\Windows\\System32\\cmd.exe';
|
|
180
|
+
const caps = getShellCapabilities('cmd');
|
|
181
|
+
expect(caps.executable).toBe('C:\\Windows\\System32\\cmd.exe');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe('Shell Capabilities - 显示名称', () => {
|
|
185
|
+
it('应该返回正确的显示名称', () => {
|
|
186
|
+
expect(getShellCapabilities('zsh').displayName).toBe('Zsh');
|
|
187
|
+
expect(getShellCapabilities('bash').displayName).toBe('Bash');
|
|
188
|
+
expect(getShellCapabilities('fish').displayName).toBe('Fish');
|
|
189
|
+
expect(getShellCapabilities('cmd').displayName).toBe('CMD');
|
|
190
|
+
expect(getShellCapabilities('powershell5').displayName).toBe('PowerShell 5.x');
|
|
191
|
+
expect(getShellCapabilities('powershell7').displayName).toBe('PowerShell 7+');
|
|
192
|
+
expect(getShellCapabilities('unknown').displayName).toBe('Unknown');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe('Shell Capabilities - 完整性检查', () => {
|
|
196
|
+
it('每个 Shell 类型都应该返回完整的能力信息', () => {
|
|
197
|
+
const shells = ['zsh', 'bash', 'fish', 'cmd', 'powershell5', 'powershell7', 'unknown'];
|
|
198
|
+
shells.forEach((shell) => {
|
|
199
|
+
const caps = getShellCapabilities(shell);
|
|
200
|
+
expect(caps).toHaveProperty('supportsHook');
|
|
201
|
+
expect(caps).toHaveProperty('supportsHistory');
|
|
202
|
+
expect(caps).toHaveProperty('configPath');
|
|
203
|
+
expect(caps).toHaveProperty('historyPath');
|
|
204
|
+
expect(caps).toHaveProperty('executable');
|
|
205
|
+
expect(caps).toHaveProperty('displayName');
|
|
206
|
+
expect(typeof caps.supportsHook).toBe('boolean');
|
|
207
|
+
expect(typeof caps.supportsHistory).toBe('boolean');
|
|
208
|
+
expect(typeof caps.executable).toBe('string');
|
|
209
|
+
expect(typeof caps.displayName).toBe('string');
|
|
210
|
+
expect(caps.executable).toBeTruthy();
|
|
211
|
+
expect(caps.displayName).toBeTruthy();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令执行配置专项测试
|
|
3
|
+
* 测试不同 Shell 的命令执行配置构建
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { buildShellExecConfig } from '../platform';
|
|
7
|
+
describe('Shell Exec Config - Bash', () => {
|
|
8
|
+
const originalEnv = { ...process.env };
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
delete process.env.SHELL;
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
process.env = { ...originalEnv };
|
|
14
|
+
});
|
|
15
|
+
it('应该为 Bash 添加 pipefail', () => {
|
|
16
|
+
const config = buildShellExecConfig('ls -la', 'bash');
|
|
17
|
+
expect(config.shell).toBe('/bin/bash');
|
|
18
|
+
expect(config.args).toEqual(['-c', 'set -o pipefail; ls -la']);
|
|
19
|
+
expect(config.command).toBe('set -o pipefail; ls -la');
|
|
20
|
+
});
|
|
21
|
+
it('应该使用 $SHELL 环境变量', () => {
|
|
22
|
+
process.env.SHELL = '/usr/local/bin/bash';
|
|
23
|
+
const config = buildShellExecConfig('ls -la', 'bash');
|
|
24
|
+
expect(config.shell).toBe('/usr/local/bin/bash');
|
|
25
|
+
});
|
|
26
|
+
it('应该处理多行命令', () => {
|
|
27
|
+
const command = 'echo "hello"\necho "world"';
|
|
28
|
+
const config = buildShellExecConfig(command, 'bash');
|
|
29
|
+
expect(config.command).toBe(`set -o pipefail; ${command}`);
|
|
30
|
+
});
|
|
31
|
+
it('应该处理包含特殊字符的命令', () => {
|
|
32
|
+
const command = 'echo "test$VAR"; cat file | grep "pattern"';
|
|
33
|
+
const config = buildShellExecConfig(command, 'bash');
|
|
34
|
+
expect(config.command).toBe(`set -o pipefail; ${command}`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('Shell Exec Config - Zsh', () => {
|
|
38
|
+
const originalEnv = { ...process.env };
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
delete process.env.SHELL;
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
process.env = { ...originalEnv };
|
|
44
|
+
});
|
|
45
|
+
it('应该为 Zsh 添加 pipefail', () => {
|
|
46
|
+
const config = buildShellExecConfig('ls -la', 'zsh');
|
|
47
|
+
expect(config.shell).toBe('/bin/zsh');
|
|
48
|
+
expect(config.args).toEqual(['-c', 'setopt pipefail; ls -la']);
|
|
49
|
+
expect(config.command).toBe('setopt pipefail; ls -la');
|
|
50
|
+
});
|
|
51
|
+
it('应该使用 $SHELL 环境变量', () => {
|
|
52
|
+
process.env.SHELL = '/opt/homebrew/bin/zsh';
|
|
53
|
+
const config = buildShellExecConfig('ls -la', 'zsh');
|
|
54
|
+
expect(config.shell).toBe('/opt/homebrew/bin/zsh');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('Shell Exec Config - Fish', () => {
|
|
58
|
+
const originalEnv = { ...process.env };
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
delete process.env.SHELL;
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
process.env = { ...originalEnv };
|
|
64
|
+
});
|
|
65
|
+
it('Fish 不应该添加 pipefail', () => {
|
|
66
|
+
delete process.env.SHELL; // 确保使用默认路径
|
|
67
|
+
const config = buildShellExecConfig('ls -la', 'fish');
|
|
68
|
+
expect(config.shell).toBe('/usr/bin/fish');
|
|
69
|
+
expect(config.args).toEqual(['-c', 'ls -la']);
|
|
70
|
+
expect(config.command).toBe('ls -la'); // 注意:Fish 不需要 pipefail
|
|
71
|
+
});
|
|
72
|
+
it('应该使用 $SHELL 环境变量', () => {
|
|
73
|
+
process.env.SHELL = '/usr/local/bin/fish';
|
|
74
|
+
const config = buildShellExecConfig('ls -la', 'fish');
|
|
75
|
+
expect(config.shell).toBe('/usr/local/bin/fish');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('Shell Exec Config - PowerShell', () => {
|
|
79
|
+
it('PowerShell 5 应该使用 -NoProfile -Command', () => {
|
|
80
|
+
const config = buildShellExecConfig('Get-Process', 'powershell5');
|
|
81
|
+
expect(config.shell).toBe('powershell.exe');
|
|
82
|
+
expect(config.args).toEqual(['-NoProfile', '-Command', 'Get-Process']);
|
|
83
|
+
expect(config.command).toBe('Get-Process');
|
|
84
|
+
});
|
|
85
|
+
it('PowerShell 7 应该使用 -NoProfile -Command', () => {
|
|
86
|
+
const config = buildShellExecConfig('Get-Process', 'powershell7');
|
|
87
|
+
expect(config.shell).toBe('pwsh.exe');
|
|
88
|
+
expect(config.args).toEqual(['-NoProfile', '-Command', 'Get-Process']);
|
|
89
|
+
expect(config.command).toBe('Get-Process');
|
|
90
|
+
});
|
|
91
|
+
it('应该处理多行 PowerShell 脚本', () => {
|
|
92
|
+
const command = 'Get-Process | Where-Object {$_.CPU -gt 10}';
|
|
93
|
+
const config = buildShellExecConfig(command, 'powershell7');
|
|
94
|
+
expect(config.command).toBe(command);
|
|
95
|
+
expect(config.args).toEqual(['-NoProfile', '-Command', command]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Shell Exec Config - CMD', () => {
|
|
99
|
+
const originalEnv = { ...process.env };
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
// 清除 COMSPEC 以确保使用默认值
|
|
102
|
+
delete process.env.COMSPEC;
|
|
103
|
+
});
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
process.env = { ...originalEnv };
|
|
106
|
+
});
|
|
107
|
+
it('CMD 应该使用 /c 参数', () => {
|
|
108
|
+
const config = buildShellExecConfig('dir', 'cmd');
|
|
109
|
+
expect(config.shell).toContain('cmd.exe');
|
|
110
|
+
expect(config.args).toEqual(['/c', 'dir']);
|
|
111
|
+
expect(config.command).toBe('dir');
|
|
112
|
+
});
|
|
113
|
+
it('应该使用 $COMSPEC 环境变量', () => {
|
|
114
|
+
process.env.COMSPEC = 'C:\\Windows\\System32\\cmd.exe';
|
|
115
|
+
const config = buildShellExecConfig('dir', 'cmd');
|
|
116
|
+
expect(config.shell).toBe('C:\\Windows\\System32\\cmd.exe');
|
|
117
|
+
});
|
|
118
|
+
it('应该处理 CMD 批处理命令', () => {
|
|
119
|
+
const command = 'echo hello && dir && cd ..';
|
|
120
|
+
const config = buildShellExecConfig(command, 'cmd');
|
|
121
|
+
expect(config.command).toBe(command);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('Shell Exec Config - Unknown/Default', () => {
|
|
125
|
+
const originalPlatform = process.platform;
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
Object.defineProperty(process, 'platform', {
|
|
128
|
+
value: originalPlatform,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it('Unix 平台未知 Shell 应该降级到 /bin/sh', () => {
|
|
132
|
+
Object.defineProperty(process, 'platform', { value: 'linux', writable: true });
|
|
133
|
+
const config = buildShellExecConfig('ls', 'unknown');
|
|
134
|
+
expect(config.shell).toBe('/bin/sh');
|
|
135
|
+
expect(config.args).toEqual(['-c', 'ls']);
|
|
136
|
+
});
|
|
137
|
+
it('Windows 平台未知 Shell 应该降级到 powershell.exe', () => {
|
|
138
|
+
Object.defineProperty(process, 'platform', { value: 'win32', writable: true });
|
|
139
|
+
const config = buildShellExecConfig('dir', 'unknown');
|
|
140
|
+
expect(config.shell).toBe('powershell.exe');
|
|
141
|
+
expect(config.args).toEqual(['-Command', 'dir']);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('Shell Exec Config - 自动检测 Shell', () => {
|
|
145
|
+
const originalPlatform = process.platform;
|
|
146
|
+
const originalEnv = { ...process.env };
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
vi.stubGlobal('process', {
|
|
149
|
+
...process,
|
|
150
|
+
platform: 'darwin',
|
|
151
|
+
env: {
|
|
152
|
+
SHELL: '/bin/zsh',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
vi.unstubAllGlobals();
|
|
158
|
+
Object.defineProperty(process, 'platform', {
|
|
159
|
+
value: originalPlatform,
|
|
160
|
+
});
|
|
161
|
+
process.env = { ...originalEnv };
|
|
162
|
+
});
|
|
163
|
+
it('不指定 Shell 时应该自动检测', () => {
|
|
164
|
+
vi.stubGlobal('process', {
|
|
165
|
+
...process,
|
|
166
|
+
platform: 'darwin',
|
|
167
|
+
env: {
|
|
168
|
+
SHELL: '/bin/zsh',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const config = buildShellExecConfig('ls -la');
|
|
172
|
+
expect(config.shell).toBe('/bin/zsh');
|
|
173
|
+
expect(config.command).toBe('setopt pipefail; ls -la');
|
|
174
|
+
});
|
|
175
|
+
it('Windows 环境不指定 Shell 时应该自动检测', () => {
|
|
176
|
+
vi.stubGlobal('process', {
|
|
177
|
+
...process,
|
|
178
|
+
platform: 'win32',
|
|
179
|
+
env: {
|
|
180
|
+
PSModulePath: 'C:\\Program Files\\PowerShell\\7\\Modules',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const config = buildShellExecConfig('Get-Process');
|
|
184
|
+
expect(config.shell).toBe('pwsh.exe');
|
|
185
|
+
expect(config.args).toEqual(['-NoProfile', '-Command', 'Get-Process']);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('Shell Exec Config - 边界情况', () => {
|
|
189
|
+
it('应该处理空命令', () => {
|
|
190
|
+
const config = buildShellExecConfig('', 'bash');
|
|
191
|
+
expect(config.command).toBe('set -o pipefail; ');
|
|
192
|
+
});
|
|
193
|
+
it('应该处理只有空格的命令', () => {
|
|
194
|
+
const config = buildShellExecConfig(' ', 'bash');
|
|
195
|
+
expect(config.command).toBe('set -o pipefail; ');
|
|
196
|
+
});
|
|
197
|
+
it('应该处理包含引号的命令', () => {
|
|
198
|
+
const command = 'echo "hello \'world\'"';
|
|
199
|
+
const config = buildShellExecConfig(command, 'bash');
|
|
200
|
+
expect(config.command).toContain(command);
|
|
201
|
+
});
|
|
202
|
+
it('应该处理包含管道的命令', () => {
|
|
203
|
+
const command = 'cat file.txt | grep pattern | wc -l';
|
|
204
|
+
const config = buildShellExecConfig(command, 'bash');
|
|
205
|
+
expect(config.command).toBe(`set -o pipefail; ${command}`);
|
|
206
|
+
});
|
|
207
|
+
it('应该处理包含重定向的命令', () => {
|
|
208
|
+
const command = 'echo "test" > file.txt 2>&1';
|
|
209
|
+
const config = buildShellExecConfig(command, 'bash');
|
|
210
|
+
expect(config.command).toContain(command);
|
|
211
|
+
});
|
|
212
|
+
});
|