@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.
Files changed (94) hide show
  1. package/README.md +32 -4
  2. package/bin/pls.tsx +153 -35
  3. package/dist/bin/pls.js +126 -23
  4. package/dist/package.json +10 -2
  5. package/dist/src/__integration__/command-generation.test.d.ts +5 -0
  6. package/dist/src/__integration__/command-generation.test.js +508 -0
  7. package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
  8. package/dist/src/__integration__/error-recovery.test.js +511 -0
  9. package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
  10. package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
  11. package/dist/src/__tests__/alias.test.d.ts +5 -0
  12. package/dist/src/__tests__/alias.test.js +421 -0
  13. package/dist/src/__tests__/chat-history.test.d.ts +5 -0
  14. package/dist/src/__tests__/chat-history.test.js +372 -0
  15. package/dist/src/__tests__/config.test.d.ts +5 -0
  16. package/dist/src/__tests__/config.test.js +822 -0
  17. package/dist/src/__tests__/history.test.d.ts +5 -0
  18. package/dist/src/__tests__/history.test.js +439 -0
  19. package/dist/src/__tests__/remote-history.test.d.ts +5 -0
  20. package/dist/src/__tests__/remote-history.test.js +641 -0
  21. package/dist/src/__tests__/remote.test.d.ts +5 -0
  22. package/dist/src/__tests__/remote.test.js +689 -0
  23. package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
  24. package/dist/src/__tests__/shell-hook-install.test.js +413 -0
  25. package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
  26. package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
  27. package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
  28. package/dist/src/__tests__/shell-hook.test.js +440 -0
  29. package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
  30. package/dist/src/__tests__/sysinfo.test.js +572 -0
  31. package/dist/src/__tests__/system-history.test.d.ts +5 -0
  32. package/dist/src/__tests__/system-history.test.js +457 -0
  33. package/dist/src/components/Chat.js +9 -28
  34. package/dist/src/config.d.ts +2 -0
  35. package/dist/src/config.js +30 -2
  36. package/dist/src/mastra-chat.js +6 -3
  37. package/dist/src/multi-step.js +6 -3
  38. package/dist/src/project-context.d.ts +22 -0
  39. package/dist/src/project-context.js +168 -0
  40. package/dist/src/prompts.d.ts +4 -4
  41. package/dist/src/prompts.js +23 -6
  42. package/dist/src/shell-hook.d.ts +13 -0
  43. package/dist/src/shell-hook.js +163 -33
  44. package/dist/src/sysinfo.d.ts +38 -9
  45. package/dist/src/sysinfo.js +245 -21
  46. package/dist/src/system-history.d.ts +5 -0
  47. package/dist/src/system-history.js +64 -18
  48. package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
  49. package/dist/src/ui/__tests__/theme.test.js +688 -0
  50. package/dist/src/upgrade.js +3 -0
  51. package/dist/src/user-preferences.d.ts +44 -0
  52. package/dist/src/user-preferences.js +147 -0
  53. package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
  54. package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
  55. package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
  56. package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
  57. package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
  58. package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
  59. package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
  60. package/dist/src/utils/__tests__/platform.test.js +137 -0
  61. package/dist/src/utils/platform.d.ts +88 -0
  62. package/dist/src/utils/platform.js +331 -0
  63. package/package.json +10 -2
  64. package/src/__integration__/command-generation.test.ts +602 -0
  65. package/src/__integration__/error-recovery.test.ts +620 -0
  66. package/src/__integration__/shell-hook-workflow.test.ts +457 -0
  67. package/src/__tests__/alias.test.ts +545 -0
  68. package/src/__tests__/chat-history.test.ts +462 -0
  69. package/src/__tests__/config.test.ts +1043 -0
  70. package/src/__tests__/history.test.ts +538 -0
  71. package/src/__tests__/remote-history.test.ts +791 -0
  72. package/src/__tests__/remote.test.ts +866 -0
  73. package/src/__tests__/shell-hook-install.test.ts +510 -0
  74. package/src/__tests__/shell-hook-remote.test.ts +679 -0
  75. package/src/__tests__/shell-hook.test.ts +564 -0
  76. package/src/__tests__/sysinfo.test.ts +718 -0
  77. package/src/__tests__/system-history.test.ts +608 -0
  78. package/src/components/Chat.tsx +10 -37
  79. package/src/config.ts +29 -2
  80. package/src/mastra-chat.ts +8 -3
  81. package/src/multi-step.ts +7 -2
  82. package/src/project-context.ts +191 -0
  83. package/src/prompts.ts +26 -5
  84. package/src/shell-hook.ts +179 -33
  85. package/src/sysinfo.ts +326 -25
  86. package/src/system-history.ts +67 -14
  87. package/src/ui/__tests__/theme.test.ts +869 -0
  88. package/src/upgrade.ts +5 -0
  89. package/src/user-preferences.ts +178 -0
  90. package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
  91. package/src/utils/__tests__/platform-exec.test.ts +278 -0
  92. package/src/utils/__tests__/platform-shell.test.ts +353 -0
  93. package/src/utils/__tests__/platform.test.ts +170 -0
  94. package/src/utils/platform.ts +431 -0
@@ -0,0 +1,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, getChatHistory } from '../chat-history.js';
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
- // Debug 信息:直接在 useState 初始化时计算(同步)
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: false, onChunk }) // 不需要 AI 返回 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: ",
@@ -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
  /**
@@ -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)));
@@ -1,7 +1,7 @@
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
6
  import { getChatHistory, addChatMessage } from './chat-history.js';
7
7
  /**
@@ -47,13 +47,16 @@ export async function chatWithMastra(prompt, options = {}) {
47
47
  messages.push(msg.content);
48
48
  }
49
49
  // 3. 构建最新消息(动态上下文 + 用户问题)
50
- const sysinfo = formatSystemInfo();
50
+ const sysinfo = formatSystemInfo(await getSystemInfo());
51
51
  const plsHistory = formatHistoryForAI();
52
52
  // 使用统一的历史获取接口(自动降级到系统历史)
53
53
  const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
54
54
  const shellHistory = formatShellHistoryForAIWithFallback();
55
55
  const shellHookEnabled = !!shellHistory; // 如果有 shell 历史就视为启用
56
- const latestUserContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled);
56
+ // 获取用户偏好
57
+ const { formatUserPreferences } = await import('./user-preferences.js');
58
+ const userPreferencesStr = formatUserPreferences();
59
+ const latestUserContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled, userPreferencesStr);
57
60
  messages.push(latestUserContext);
58
61
  // 4. 发送给 AI(流式或非流式)
59
62
  let fullContent = '';