@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
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统 Shell 历史读取模块测试
|
|
3
|
+
* 测试各种 Shell 历史格式解析和系统历史读取功能
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { zshExtendedHistory, zshSimpleHistory, bashHistory, powerShellHistory, } from '../../tests/fixtures/shell-history';
|
|
7
|
+
// Mock fs 模块
|
|
8
|
+
vi.mock('fs', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
existsSync: vi.fn(),
|
|
11
|
+
readFileSync: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
// Mock config 模块
|
|
15
|
+
vi.mock('../config.js', () => ({
|
|
16
|
+
getConfig: vi.fn(() => ({
|
|
17
|
+
shellHistoryLimit: 10,
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
// Mock platform 模块
|
|
21
|
+
vi.mock('../utils/platform.js', () => ({
|
|
22
|
+
detectShell: vi.fn(),
|
|
23
|
+
getShellCapabilities: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import { getConfig } from '../config.js';
|
|
27
|
+
import { detectShell, getShellCapabilities } from '../utils/platform.js';
|
|
28
|
+
// 获取 mock 函数引用
|
|
29
|
+
const mockFs = vi.mocked(fs);
|
|
30
|
+
const mockDetectShell = vi.mocked(detectShell);
|
|
31
|
+
const mockGetShellCapabilities = vi.mocked(getShellCapabilities);
|
|
32
|
+
const mockGetConfig = vi.mocked(getConfig);
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
// 默认配置
|
|
36
|
+
mockGetConfig.mockReturnValue({
|
|
37
|
+
shellHistoryLimit: 10,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Zsh 历史解析测试
|
|
45
|
+
// ============================================================================
|
|
46
|
+
describe('Zsh 历史解析', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
mockDetectShell.mockReturnValue('zsh');
|
|
49
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
50
|
+
supportsHistory: true,
|
|
51
|
+
historyPath: '/home/user/.zsh_history',
|
|
52
|
+
supportsHook: true,
|
|
53
|
+
hookType: 'zsh',
|
|
54
|
+
});
|
|
55
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
56
|
+
});
|
|
57
|
+
it('应该解析扩展格式(: timestamp:duration;command)', async () => {
|
|
58
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:0;ls -la');
|
|
59
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
60
|
+
const history = getSystemShellHistory();
|
|
61
|
+
expect(history).toHaveLength(1);
|
|
62
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
63
|
+
expect(history[0].exit).toBe(0);
|
|
64
|
+
// 时间戳 1700000000 = 2023-11-14T22:13:20.000Z
|
|
65
|
+
expect(history[0].time).toContain('2023-11-14');
|
|
66
|
+
});
|
|
67
|
+
it('应该解析简单格式(纯命令)', async () => {
|
|
68
|
+
mockFs.readFileSync.mockReturnValue('git status');
|
|
69
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
70
|
+
const history = getSystemShellHistory();
|
|
71
|
+
expect(history).toHaveLength(1);
|
|
72
|
+
expect(history[0].cmd).toBe('git status');
|
|
73
|
+
expect(history[0].exit).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
it('应该处理多行扩展格式历史', async () => {
|
|
76
|
+
mockFs.readFileSync.mockReturnValue(zshExtendedHistory);
|
|
77
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
78
|
+
const history = getSystemShellHistory();
|
|
79
|
+
expect(history).toHaveLength(5);
|
|
80
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
81
|
+
expect(history[1].cmd).toBe('git status');
|
|
82
|
+
expect(history[4].cmd).toBe('cd ~/projects');
|
|
83
|
+
});
|
|
84
|
+
it('应该处理多行简单格式历史', async () => {
|
|
85
|
+
mockFs.readFileSync.mockReturnValue(zshSimpleHistory);
|
|
86
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
87
|
+
const history = getSystemShellHistory();
|
|
88
|
+
expect(history).toHaveLength(5);
|
|
89
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
90
|
+
expect(history[4].cmd).toBe('cd ~/projects');
|
|
91
|
+
});
|
|
92
|
+
it('退出码应该默认为 0(系统历史无退出码)', async () => {
|
|
93
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:0;failed-command');
|
|
94
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
95
|
+
const history = getSystemShellHistory();
|
|
96
|
+
expect(history[0].exit).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it('空行应该被过滤', async () => {
|
|
99
|
+
mockFs.readFileSync.mockReturnValue('ls -la\n\n\ngit status\n\n');
|
|
100
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
101
|
+
const history = getSystemShellHistory();
|
|
102
|
+
expect(history).toHaveLength(2);
|
|
103
|
+
});
|
|
104
|
+
it('应该正确转换时间戳为 ISO 8601 格式', async () => {
|
|
105
|
+
// 时间戳 1700000000 对应 2023-11-14T22:13:20.000Z
|
|
106
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:0;test');
|
|
107
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
108
|
+
const history = getSystemShellHistory();
|
|
109
|
+
expect(history[0].time).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
110
|
+
});
|
|
111
|
+
it('duration 字段应该被忽略', async () => {
|
|
112
|
+
// duration 值不同,但结果应该一样
|
|
113
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:999;test');
|
|
114
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
115
|
+
const history = getSystemShellHistory();
|
|
116
|
+
expect(history[0].cmd).toBe('test');
|
|
117
|
+
});
|
|
118
|
+
it('应该保留命令中的特殊字符', async () => {
|
|
119
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:0;echo "hello $USER" && ls');
|
|
120
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
121
|
+
const history = getSystemShellHistory();
|
|
122
|
+
expect(history[0].cmd).toBe('echo "hello $USER" && ls');
|
|
123
|
+
});
|
|
124
|
+
it('简单格式的时间应该使用当前时间', async () => {
|
|
125
|
+
const beforeTime = new Date().toISOString();
|
|
126
|
+
mockFs.readFileSync.mockReturnValue('simple-command');
|
|
127
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
128
|
+
const history = getSystemShellHistory();
|
|
129
|
+
const afterTime = new Date().toISOString();
|
|
130
|
+
// 时间应该在测试执行期间
|
|
131
|
+
expect(history[0].time >= beforeTime).toBe(true);
|
|
132
|
+
expect(history[0].time <= afterTime).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Bash 历史解析测试
|
|
137
|
+
// ============================================================================
|
|
138
|
+
describe('Bash 历史解析', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
mockDetectShell.mockReturnValue('bash');
|
|
141
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
142
|
+
supportsHistory: true,
|
|
143
|
+
historyPath: '/home/user/.bash_history',
|
|
144
|
+
supportsHook: true,
|
|
145
|
+
hookType: 'bash',
|
|
146
|
+
});
|
|
147
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
148
|
+
});
|
|
149
|
+
it('应该解析纯文本命令', async () => {
|
|
150
|
+
mockFs.readFileSync.mockReturnValue('ls -la');
|
|
151
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
152
|
+
const history = getSystemShellHistory();
|
|
153
|
+
expect(history).toHaveLength(1);
|
|
154
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
155
|
+
});
|
|
156
|
+
it('应该处理多行历史', async () => {
|
|
157
|
+
mockFs.readFileSync.mockReturnValue(bashHistory);
|
|
158
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
159
|
+
const history = getSystemShellHistory();
|
|
160
|
+
expect(history).toHaveLength(5);
|
|
161
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
162
|
+
expect(history[2].cmd).toBe('npm install');
|
|
163
|
+
});
|
|
164
|
+
it('退出码应该默认为 0', async () => {
|
|
165
|
+
mockFs.readFileSync.mockReturnValue('any-command');
|
|
166
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
167
|
+
const history = getSystemShellHistory();
|
|
168
|
+
expect(history[0].exit).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
it('时间应该使用当前时间', async () => {
|
|
171
|
+
const beforeTime = new Date().toISOString();
|
|
172
|
+
mockFs.readFileSync.mockReturnValue('test-command');
|
|
173
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
174
|
+
const history = getSystemShellHistory();
|
|
175
|
+
expect(history[0].time >= beforeTime).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
it('空行应该被过滤', async () => {
|
|
178
|
+
mockFs.readFileSync.mockReturnValue('cmd1\n\ncmd2\n\n\ncmd3');
|
|
179
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
180
|
+
const history = getSystemShellHistory();
|
|
181
|
+
expect(history).toHaveLength(3);
|
|
182
|
+
});
|
|
183
|
+
it('应该去除首尾空格', async () => {
|
|
184
|
+
mockFs.readFileSync.mockReturnValue(' ls -la ');
|
|
185
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
186
|
+
const history = getSystemShellHistory();
|
|
187
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
188
|
+
});
|
|
189
|
+
it('应该保留命令中的特殊字符', async () => {
|
|
190
|
+
mockFs.readFileSync.mockReturnValue('grep "pattern" file | awk \'{print $1}\'');
|
|
191
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
192
|
+
const history = getSystemShellHistory();
|
|
193
|
+
expect(history[0].cmd).toBe('grep "pattern" file | awk \'{print $1}\'');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Fish 历史解析测试
|
|
198
|
+
// ============================================================================
|
|
199
|
+
describe('Fish 历史解析', () => {
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
mockDetectShell.mockReturnValue('fish');
|
|
202
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
203
|
+
supportsHistory: true,
|
|
204
|
+
historyPath: '/home/user/.local/share/fish/fish_history',
|
|
205
|
+
supportsHook: false,
|
|
206
|
+
hookType: null,
|
|
207
|
+
});
|
|
208
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
209
|
+
});
|
|
210
|
+
it('应该解析 YAML-like 格式(- cmd: ...)', async () => {
|
|
211
|
+
mockFs.readFileSync.mockReturnValue('- cmd: ls -la');
|
|
212
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
213
|
+
const history = getSystemShellHistory();
|
|
214
|
+
expect(history).toHaveLength(1);
|
|
215
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
216
|
+
});
|
|
217
|
+
it('非 cmd 行应该被过滤', async () => {
|
|
218
|
+
mockFs.readFileSync.mockReturnValue('- cmd: ls -la\n when: 1700000000\n- cmd: git status');
|
|
219
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
220
|
+
const history = getSystemShellHistory();
|
|
221
|
+
// 只有 cmd 行被解析
|
|
222
|
+
expect(history).toHaveLength(2);
|
|
223
|
+
expect(history[0].cmd).toBe('ls -la');
|
|
224
|
+
expect(history[1].cmd).toBe('git status');
|
|
225
|
+
});
|
|
226
|
+
it('退出码应该默认为 0', async () => {
|
|
227
|
+
mockFs.readFileSync.mockReturnValue('- cmd: failed-cmd');
|
|
228
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
229
|
+
const history = getSystemShellHistory();
|
|
230
|
+
expect(history[0].exit).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
it('时间应该使用当前时间', async () => {
|
|
233
|
+
const beforeTime = new Date().toISOString();
|
|
234
|
+
mockFs.readFileSync.mockReturnValue('- cmd: test');
|
|
235
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
236
|
+
const history = getSystemShellHistory();
|
|
237
|
+
expect(history[0].time >= beforeTime).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it('格式错误的行应该被过滤', async () => {
|
|
240
|
+
mockFs.readFileSync.mockReturnValue('- cmd: valid\ninvalid line\n- cmd: also-valid');
|
|
241
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
242
|
+
const history = getSystemShellHistory();
|
|
243
|
+
expect(history).toHaveLength(2);
|
|
244
|
+
});
|
|
245
|
+
it('应该保留命令中的特殊字符', async () => {
|
|
246
|
+
mockFs.readFileSync.mockReturnValue('- cmd: echo "hello $USER"');
|
|
247
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
248
|
+
const history = getSystemShellHistory();
|
|
249
|
+
expect(history[0].cmd).toBe('echo "hello $USER"');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// PowerShell 历史解析测试
|
|
254
|
+
// ============================================================================
|
|
255
|
+
describe('PowerShell 历史解析', () => {
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
mockDetectShell.mockReturnValue('powershell7');
|
|
258
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
259
|
+
supportsHistory: true,
|
|
260
|
+
historyPath: 'C:\\Users\\test\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt',
|
|
261
|
+
supportsHook: true,
|
|
262
|
+
hookType: 'powershell',
|
|
263
|
+
});
|
|
264
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
265
|
+
});
|
|
266
|
+
it('应该解析纯文本命令', async () => {
|
|
267
|
+
mockFs.readFileSync.mockReturnValue('Get-ChildItem');
|
|
268
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
269
|
+
const history = getSystemShellHistory();
|
|
270
|
+
expect(history).toHaveLength(1);
|
|
271
|
+
expect(history[0].cmd).toBe('Get-ChildItem');
|
|
272
|
+
});
|
|
273
|
+
it('应该处理多行历史', async () => {
|
|
274
|
+
mockFs.readFileSync.mockReturnValue(powerShellHistory);
|
|
275
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
276
|
+
const history = getSystemShellHistory();
|
|
277
|
+
expect(history).toHaveLength(5);
|
|
278
|
+
expect(history[0].cmd).toBe('Get-ChildItem');
|
|
279
|
+
expect(history[1].cmd).toBe('Get-Process');
|
|
280
|
+
});
|
|
281
|
+
it('退出码应该默认为 0', async () => {
|
|
282
|
+
mockFs.readFileSync.mockReturnValue('Get-Process');
|
|
283
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
284
|
+
const history = getSystemShellHistory();
|
|
285
|
+
expect(history[0].exit).toBe(0);
|
|
286
|
+
});
|
|
287
|
+
it('时间应该使用当前时间', async () => {
|
|
288
|
+
const beforeTime = new Date().toISOString();
|
|
289
|
+
mockFs.readFileSync.mockReturnValue('Write-Host "test"');
|
|
290
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
291
|
+
const history = getSystemShellHistory();
|
|
292
|
+
expect(history[0].time >= beforeTime).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
it('空行应该被过滤', async () => {
|
|
295
|
+
mockFs.readFileSync.mockReturnValue('cmd1\n\ncmd2');
|
|
296
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
297
|
+
const history = getSystemShellHistory();
|
|
298
|
+
expect(history).toHaveLength(2);
|
|
299
|
+
});
|
|
300
|
+
it('PowerShell 5 也应该正常工作', async () => {
|
|
301
|
+
mockDetectShell.mockReturnValue('powershell5');
|
|
302
|
+
mockFs.readFileSync.mockReturnValue('Get-Service');
|
|
303
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
304
|
+
const history = getSystemShellHistory();
|
|
305
|
+
expect(history).toHaveLength(1);
|
|
306
|
+
expect(history[0].cmd).toBe('Get-Service');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// 系统历史读取测试
|
|
311
|
+
// ============================================================================
|
|
312
|
+
describe('getSystemShellHistory', () => {
|
|
313
|
+
it('应该根据 Shell 类型选择正确的解析器', async () => {
|
|
314
|
+
// Zsh
|
|
315
|
+
mockDetectShell.mockReturnValue('zsh');
|
|
316
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
317
|
+
supportsHistory: true,
|
|
318
|
+
historyPath: '/home/user/.zsh_history',
|
|
319
|
+
});
|
|
320
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
321
|
+
mockFs.readFileSync.mockReturnValue(': 1700000000:0;zsh-cmd');
|
|
322
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
323
|
+
let history = getSystemShellHistory();
|
|
324
|
+
expect(history[0].cmd).toBe('zsh-cmd');
|
|
325
|
+
// Bash
|
|
326
|
+
mockDetectShell.mockReturnValue('bash');
|
|
327
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
328
|
+
supportsHistory: true,
|
|
329
|
+
historyPath: '/home/user/.bash_history',
|
|
330
|
+
});
|
|
331
|
+
mockFs.readFileSync.mockReturnValue('bash-cmd');
|
|
332
|
+
history = getSystemShellHistory();
|
|
333
|
+
expect(history[0].cmd).toBe('bash-cmd');
|
|
334
|
+
});
|
|
335
|
+
it('文件不存在时应该返回空数组', async () => {
|
|
336
|
+
mockDetectShell.mockReturnValue('zsh');
|
|
337
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
338
|
+
supportsHistory: true,
|
|
339
|
+
historyPath: '/nonexistent/.zsh_history',
|
|
340
|
+
});
|
|
341
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
342
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
343
|
+
const history = getSystemShellHistory();
|
|
344
|
+
expect(history).toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
it('Shell 不支持历史时应该返回空数组', async () => {
|
|
347
|
+
mockDetectShell.mockReturnValue('cmd');
|
|
348
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
349
|
+
supportsHistory: false,
|
|
350
|
+
historyPath: null,
|
|
351
|
+
});
|
|
352
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
353
|
+
const history = getSystemShellHistory();
|
|
354
|
+
expect(history).toEqual([]);
|
|
355
|
+
});
|
|
356
|
+
it('应该只返回最后 N 条(shellHistoryLimit)', async () => {
|
|
357
|
+
mockGetConfig.mockReturnValue({ shellHistoryLimit: 3 });
|
|
358
|
+
mockDetectShell.mockReturnValue('bash');
|
|
359
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
360
|
+
supportsHistory: true,
|
|
361
|
+
historyPath: '/home/user/.bash_history',
|
|
362
|
+
});
|
|
363
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
364
|
+
mockFs.readFileSync.mockReturnValue('cmd1\ncmd2\ncmd3\ncmd4\ncmd5');
|
|
365
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
366
|
+
const history = getSystemShellHistory();
|
|
367
|
+
expect(history).toHaveLength(3);
|
|
368
|
+
expect(history[0].cmd).toBe('cmd3');
|
|
369
|
+
expect(history[2].cmd).toBe('cmd5');
|
|
370
|
+
});
|
|
371
|
+
it('读取失败时应该返回空数组', async () => {
|
|
372
|
+
mockDetectShell.mockReturnValue('zsh');
|
|
373
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
374
|
+
supportsHistory: true,
|
|
375
|
+
historyPath: '/home/user/.zsh_history',
|
|
376
|
+
});
|
|
377
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
378
|
+
mockFs.readFileSync.mockImplementation(() => {
|
|
379
|
+
throw new Error('EACCES: permission denied');
|
|
380
|
+
});
|
|
381
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
382
|
+
const history = getSystemShellHistory();
|
|
383
|
+
expect(history).toEqual([]);
|
|
384
|
+
});
|
|
385
|
+
it('unknown Shell 应该返回空数组', async () => {
|
|
386
|
+
mockDetectShell.mockReturnValue('unknown');
|
|
387
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
388
|
+
supportsHistory: false,
|
|
389
|
+
historyPath: null,
|
|
390
|
+
});
|
|
391
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
392
|
+
const history = getSystemShellHistory();
|
|
393
|
+
expect(history).toEqual([]);
|
|
394
|
+
});
|
|
395
|
+
it('空文件应该返回空数组', async () => {
|
|
396
|
+
mockDetectShell.mockReturnValue('bash');
|
|
397
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
398
|
+
supportsHistory: true,
|
|
399
|
+
historyPath: '/home/user/.bash_history',
|
|
400
|
+
});
|
|
401
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
402
|
+
mockFs.readFileSync.mockReturnValue('');
|
|
403
|
+
const { getSystemShellHistory } = await import('../system-history.js');
|
|
404
|
+
const history = getSystemShellHistory();
|
|
405
|
+
expect(history).toEqual([]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// getLastCommandFromSystem 测试
|
|
410
|
+
// ============================================================================
|
|
411
|
+
describe('getLastCommandFromSystem', () => {
|
|
412
|
+
beforeEach(() => {
|
|
413
|
+
mockDetectShell.mockReturnValue('bash');
|
|
414
|
+
mockGetShellCapabilities.mockReturnValue({
|
|
415
|
+
supportsHistory: true,
|
|
416
|
+
historyPath: '/home/user/.bash_history',
|
|
417
|
+
});
|
|
418
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
419
|
+
});
|
|
420
|
+
it('应该返回最近一条非 pls 命令', async () => {
|
|
421
|
+
mockFs.readFileSync.mockReturnValue('git status\npls install git\nls -la');
|
|
422
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
423
|
+
const lastCmd = getLastCommandFromSystem();
|
|
424
|
+
expect(lastCmd).not.toBeNull();
|
|
425
|
+
expect(lastCmd.cmd).toBe('ls -la');
|
|
426
|
+
});
|
|
427
|
+
it('应该排除 pls 命令', async () => {
|
|
428
|
+
mockFs.readFileSync.mockReturnValue('git status\npls fix\npls install');
|
|
429
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
430
|
+
const lastCmd = getLastCommandFromSystem();
|
|
431
|
+
expect(lastCmd.cmd).toBe('git status');
|
|
432
|
+
});
|
|
433
|
+
it('应该排除 please 命令', async () => {
|
|
434
|
+
mockFs.readFileSync.mockReturnValue('npm install\nplease help\nplease run');
|
|
435
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
436
|
+
const lastCmd = getLastCommandFromSystem();
|
|
437
|
+
expect(lastCmd.cmd).toBe('npm install');
|
|
438
|
+
});
|
|
439
|
+
it('所有命令都是 pls/please 时应该返回 null', async () => {
|
|
440
|
+
mockFs.readFileSync.mockReturnValue('pls help\npls config\nplease install');
|
|
441
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
442
|
+
const lastCmd = getLastCommandFromSystem();
|
|
443
|
+
expect(lastCmd).toBeNull();
|
|
444
|
+
});
|
|
445
|
+
it('历史为空时应该返回 null', async () => {
|
|
446
|
+
mockFs.readFileSync.mockReturnValue('');
|
|
447
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
448
|
+
const lastCmd = getLastCommandFromSystem();
|
|
449
|
+
expect(lastCmd).toBeNull();
|
|
450
|
+
});
|
|
451
|
+
it('文件不存在时应该返回 null', async () => {
|
|
452
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
453
|
+
const { getLastCommandFromSystem } = await import('../system-history.js');
|
|
454
|
+
const lastCmd = getLastCommandFromSystem();
|
|
455
|
+
expect(lastCmd).toBeNull();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -3,13 +3,8 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { MarkdownDisplay } from './MarkdownDisplay.js';
|
|
5
5
|
import { chatWithMastra } from '../mastra-chat.js';
|
|
6
|
-
import { getChatRoundCount
|
|
6
|
+
import { getChatRoundCount } from '../chat-history.js';
|
|
7
7
|
import { getCurrentTheme } from '../ui/theme.js';
|
|
8
|
-
import { formatSystemInfo } from '../sysinfo.js';
|
|
9
|
-
import { formatHistoryForAI } from '../history.js';
|
|
10
|
-
import { formatShellHistoryForAI, getShellHistory } from '../shell-hook.js';
|
|
11
|
-
import { getConfig } from '../config.js';
|
|
12
|
-
import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from '../prompts.js';
|
|
13
8
|
/**
|
|
14
9
|
* Chat 组件 - AI 对话模式
|
|
15
10
|
* 使用正常渲染,完成后保持最后一帧在终端
|
|
@@ -20,25 +15,7 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
20
15
|
const [content, setContent] = useState('');
|
|
21
16
|
const [duration, setDuration] = useState(0);
|
|
22
17
|
const [roundCount] = useState(getChatRoundCount());
|
|
23
|
-
|
|
24
|
-
const [debugInfo] = useState(() => {
|
|
25
|
-
if (!debug)
|
|
26
|
-
return null;
|
|
27
|
-
const config = getConfig();
|
|
28
|
-
const sysinfo = formatSystemInfo();
|
|
29
|
-
const plsHistory = formatHistoryForAI();
|
|
30
|
-
const shellHistory = formatShellHistoryForAI();
|
|
31
|
-
const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
|
|
32
|
-
const chatHistory = getChatHistory();
|
|
33
|
-
const userContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled);
|
|
34
|
-
return {
|
|
35
|
-
sysinfo,
|
|
36
|
-
model: config.model,
|
|
37
|
-
systemPrompt: CHAT_SYSTEM_PROMPT,
|
|
38
|
-
userContext,
|
|
39
|
-
chatHistory,
|
|
40
|
-
};
|
|
41
|
-
});
|
|
18
|
+
const [debugInfo, setDebugInfo] = useState(null);
|
|
42
19
|
useEffect(() => {
|
|
43
20
|
const startTime = Date.now();
|
|
44
21
|
// 流式输出回调
|
|
@@ -46,12 +23,16 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
46
23
|
setStatus('streaming');
|
|
47
24
|
setContent((prev) => prev + chunk);
|
|
48
25
|
};
|
|
49
|
-
// 调用 AI
|
|
50
|
-
chatWithMastra(prompt, { debug
|
|
26
|
+
// 调用 AI(如果需要 debug 信息,则开启)
|
|
27
|
+
chatWithMastra(prompt, { debug, onChunk })
|
|
51
28
|
.then((result) => {
|
|
52
29
|
const endTime = Date.now();
|
|
53
30
|
setDuration(endTime - startTime);
|
|
54
31
|
setStatus('done');
|
|
32
|
+
// 如果有 debug 信息,保存它
|
|
33
|
+
if (result.debug) {
|
|
34
|
+
setDebugInfo(result.debug);
|
|
35
|
+
}
|
|
55
36
|
setTimeout(onComplete, debug ? 500 : 100);
|
|
56
37
|
})
|
|
57
38
|
.catch((error) => {
|
|
@@ -61,7 +42,7 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }) {
|
|
|
61
42
|
});
|
|
62
43
|
}, [prompt, debug, onComplete]);
|
|
63
44
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
64
|
-
debugInfo && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
45
|
+
debug && debugInfo && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
65
46
|
React.createElement(Text, { color: theme.accent, bold: true }, "\u2501\u2501\u2501 \u8C03\u8BD5\u4FE1\u606F \u2501\u2501\u2501"),
|
|
66
47
|
React.createElement(Text, { color: theme.text.secondary },
|
|
67
48
|
"\u6A21\u578B: ",
|
package/dist/src/config.d.ts
CHANGED
|
@@ -44,11 +44,13 @@ export interface Config {
|
|
|
44
44
|
chatHistoryLimit: number;
|
|
45
45
|
commandHistoryLimit: number;
|
|
46
46
|
shellHistoryLimit: number;
|
|
47
|
+
userPreferencesTopK: number;
|
|
47
48
|
editMode: EditMode;
|
|
48
49
|
theme: ThemeName;
|
|
49
50
|
aliases: Record<string, AliasConfig>;
|
|
50
51
|
remotes: Record<string, RemoteConfig>;
|
|
51
52
|
defaultRemote?: string;
|
|
53
|
+
systemCacheExpireDays?: number;
|
|
52
54
|
}
|
|
53
55
|
export declare function getConfig(): Config;
|
|
54
56
|
/**
|
package/dist/src/config.js
CHANGED
|
@@ -11,7 +11,8 @@ function getColors() {
|
|
|
11
11
|
primary: theme.primary,
|
|
12
12
|
secondary: theme.secondary,
|
|
13
13
|
success: theme.success,
|
|
14
|
-
error: theme.error
|
|
14
|
+
error: theme.error,
|
|
15
|
+
warning: theme.warning,
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
// 配置文件路径
|
|
@@ -43,11 +44,13 @@ const DEFAULT_CONFIG = {
|
|
|
43
44
|
chatHistoryLimit: 5,
|
|
44
45
|
commandHistoryLimit: 5,
|
|
45
46
|
shellHistoryLimit: 10,
|
|
47
|
+
userPreferencesTopK: 20, // 默认显示 Top 20
|
|
46
48
|
editMode: 'manual',
|
|
47
49
|
theme: 'dark',
|
|
48
50
|
aliases: {},
|
|
49
51
|
remotes: {},
|
|
50
52
|
defaultRemote: '',
|
|
53
|
+
systemCacheExpireDays: 7,
|
|
51
54
|
};
|
|
52
55
|
/**
|
|
53
56
|
* 确保配置目录存在
|
|
@@ -103,7 +106,7 @@ export function setConfigValue(key, value) {
|
|
|
103
106
|
if (key === 'shellHook') {
|
|
104
107
|
config.shellHook = value === 'true' || value === true;
|
|
105
108
|
}
|
|
106
|
-
else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
|
|
109
|
+
else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit' || key === 'userPreferencesTopK' || key === 'systemCacheExpireDays') {
|
|
107
110
|
const num = typeof value === 'number' ? value : parseInt(String(value), 10);
|
|
108
111
|
if (isNaN(num) || num < 1) {
|
|
109
112
|
throw new Error(`${key} 必须是大于 0 的整数`);
|
|
@@ -173,6 +176,10 @@ export function displayConfig() {
|
|
|
173
176
|
console.log(` ${chalk.hex(colors.primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
|
|
174
177
|
console.log(` ${chalk.hex(colors.primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
|
|
175
178
|
console.log(` ${chalk.hex(colors.primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
|
|
179
|
+
console.log(` ${chalk.hex(colors.primary)('userPreferencesTopK')}: ${config.userPreferencesTopK} 个`);
|
|
180
|
+
if (config.systemCacheExpireDays !== undefined) {
|
|
181
|
+
console.log(` ${chalk.hex(colors.primary)('systemCacheExpireDays')}: ${config.systemCacheExpireDays} 天`);
|
|
182
|
+
}
|
|
176
183
|
// 动态显示主题信息
|
|
177
184
|
const themeMetadata = getAllThemeMetadata().find((m) => m.name === config.theme);
|
|
178
185
|
const themeLabel = themeMetadata ? `${themeMetadata.name} (${themeMetadata.displayName})` : config.theme;
|
|
@@ -269,6 +276,9 @@ export async function runConfigWizard() {
|
|
|
269
276
|
if (!isNaN(num) && num > 0) {
|
|
270
277
|
config.chatHistoryLimit = num;
|
|
271
278
|
}
|
|
279
|
+
else {
|
|
280
|
+
console.log(chalk.hex(colors.warning)(' ⚠️ 输入无效,保持原值'));
|
|
281
|
+
}
|
|
272
282
|
}
|
|
273
283
|
// 8. Command History Limit
|
|
274
284
|
const commandHistoryPrompt = `${chalk.hex(colors.primary)('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.commandHistoryLimit)} ${chalk.gray('→')} `;
|
|
@@ -278,6 +288,9 @@ export async function runConfigWizard() {
|
|
|
278
288
|
if (!isNaN(num) && num > 0) {
|
|
279
289
|
config.commandHistoryLimit = num;
|
|
280
290
|
}
|
|
291
|
+
else {
|
|
292
|
+
console.log(chalk.hex(colors.warning)(' ⚠️ 输入无效,保持原值'));
|
|
293
|
+
}
|
|
281
294
|
}
|
|
282
295
|
// 9. Shell History Limit
|
|
283
296
|
const oldShellHistoryLimit = config.shellHistoryLimit; // 保存旧值
|
|
@@ -288,6 +301,21 @@ export async function runConfigWizard() {
|
|
|
288
301
|
if (!isNaN(num) && num > 0) {
|
|
289
302
|
config.shellHistoryLimit = num;
|
|
290
303
|
}
|
|
304
|
+
else {
|
|
305
|
+
console.log(chalk.hex(colors.warning)(' ⚠️ 输入无效,保持原值'));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// 10. User Preferences Top K
|
|
309
|
+
const userPrefsPrompt = `${chalk.hex(colors.primary)('用户偏好显示命令数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.userPreferencesTopK)} ${chalk.gray('→')} `;
|
|
310
|
+
const userPrefsTopK = await question(rl, userPrefsPrompt);
|
|
311
|
+
if (userPrefsTopK.trim()) {
|
|
312
|
+
const num = parseInt(userPrefsTopK.trim(), 10);
|
|
313
|
+
if (!isNaN(num) && num > 0) {
|
|
314
|
+
config.userPreferencesTopK = num;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
console.log(chalk.hex(colors.warning)(' ⚠️ 输入无效,保持原值'));
|
|
318
|
+
}
|
|
291
319
|
}
|
|
292
320
|
saveConfig(config);
|
|
293
321
|
console.log('\n' + chalk.gray('━'.repeat(50)));
|
package/dist/src/mastra-chat.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Agent } from '@mastra/core';
|
|
2
2
|
import { getConfig } from './config.js';
|
|
3
3
|
import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from './prompts.js';
|
|
4
|
-
import { formatSystemInfo } from './sysinfo.js';
|
|
4
|
+
import { formatSystemInfo, getSystemInfo } from './sysinfo.js';
|
|
5
5
|
import { formatHistoryForAI } from './history.js';
|
|
6
|
-
import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
|
|
7
6
|
import { getChatHistory, addChatMessage } from './chat-history.js';
|
|
8
7
|
/**
|
|
9
8
|
* 创建 Mastra Chat Agent(使用静态系统提示词)
|
|
@@ -48,11 +47,16 @@ export async function chatWithMastra(prompt, options = {}) {
|
|
|
48
47
|
messages.push(msg.content);
|
|
49
48
|
}
|
|
50
49
|
// 3. 构建最新消息(动态上下文 + 用户问题)
|
|
51
|
-
const sysinfo = formatSystemInfo();
|
|
50
|
+
const sysinfo = formatSystemInfo(await getSystemInfo());
|
|
52
51
|
const plsHistory = formatHistoryForAI();
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
52
|
+
// 使用统一的历史获取接口(自动降级到系统历史)
|
|
53
|
+
const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
|
|
54
|
+
const shellHistory = formatShellHistoryForAIWithFallback();
|
|
55
|
+
const shellHookEnabled = !!shellHistory; // 如果有 shell 历史就视为启用
|
|
56
|
+
// 获取用户偏好
|
|
57
|
+
const { formatUserPreferences } = await import('./user-preferences.js');
|
|
58
|
+
const userPreferencesStr = formatUserPreferences();
|
|
59
|
+
const latestUserContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled, userPreferencesStr);
|
|
56
60
|
messages.push(latestUserContext);
|
|
57
61
|
// 4. 发送给 AI(流式或非流式)
|
|
58
62
|
let fullContent = '';
|