@yivan-lab/pretty-please 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +390 -1
  2. package/bin/pls.tsx +1255 -123
  3. package/dist/bin/pls.js +1098 -103
  4. package/dist/package.json +4 -4
  5. package/dist/src/alias.d.ts +41 -0
  6. package/dist/src/alias.js +240 -0
  7. package/dist/src/chat-history.js +10 -1
  8. package/dist/src/components/Chat.js +54 -26
  9. package/dist/src/components/CodeColorizer.js +26 -20
  10. package/dist/src/components/CommandBox.js +19 -8
  11. package/dist/src/components/ConfirmationPrompt.js +2 -1
  12. package/dist/src/components/Duration.js +2 -1
  13. package/dist/src/components/InlineRenderer.js +2 -1
  14. package/dist/src/components/MarkdownDisplay.js +2 -1
  15. package/dist/src/components/MultiStepCommandGenerator.d.ts +3 -1
  16. package/dist/src/components/MultiStepCommandGenerator.js +20 -10
  17. package/dist/src/components/TableRenderer.js +2 -1
  18. package/dist/src/config.d.ts +33 -3
  19. package/dist/src/config.js +83 -34
  20. package/dist/src/mastra-agent.d.ts +1 -0
  21. package/dist/src/mastra-agent.js +3 -11
  22. package/dist/src/mastra-chat.d.ts +13 -6
  23. package/dist/src/mastra-chat.js +31 -31
  24. package/dist/src/multi-step.d.ts +23 -7
  25. package/dist/src/multi-step.js +45 -26
  26. package/dist/src/prompts.d.ts +30 -4
  27. package/dist/src/prompts.js +218 -70
  28. package/dist/src/remote-history.d.ts +63 -0
  29. package/dist/src/remote-history.js +315 -0
  30. package/dist/src/remote.d.ts +113 -0
  31. package/dist/src/remote.js +634 -0
  32. package/dist/src/shell-hook.d.ts +58 -0
  33. package/dist/src/shell-hook.js +295 -26
  34. package/dist/src/ui/theme.d.ts +60 -23
  35. package/dist/src/ui/theme.js +544 -22
  36. package/dist/src/upgrade.d.ts +41 -0
  37. package/dist/src/upgrade.js +348 -0
  38. package/dist/src/utils/console.d.ts +4 -0
  39. package/dist/src/utils/console.js +89 -17
  40. package/package.json +4 -4
  41. package/src/alias.ts +301 -0
  42. package/src/chat-history.ts +11 -1
  43. package/src/components/Chat.tsx +71 -26
  44. package/src/components/CodeColorizer.tsx +27 -19
  45. package/src/components/CommandBox.tsx +26 -8
  46. package/src/components/ConfirmationPrompt.tsx +2 -1
  47. package/src/components/Duration.tsx +2 -1
  48. package/src/components/InlineRenderer.tsx +2 -1
  49. package/src/components/MarkdownDisplay.tsx +2 -1
  50. package/src/components/MultiStepCommandGenerator.tsx +25 -11
  51. package/src/components/TableRenderer.tsx +2 -1
  52. package/src/config.ts +126 -35
  53. package/src/mastra-agent.ts +3 -12
  54. package/src/mastra-chat.ts +40 -34
  55. package/src/multi-step.ts +62 -30
  56. package/src/prompts.ts +236 -78
  57. package/src/remote-history.ts +390 -0
  58. package/src/remote.ts +800 -0
  59. package/src/shell-hook.ts +339 -26
  60. package/src/ui/theme.ts +632 -23
  61. package/src/upgrade.ts +397 -0
  62. package/src/utils/console.ts +99 -17
@@ -0,0 +1,390 @@
1
+ /**
2
+ * 远程服务器历史管理模块
3
+ * 管理每个远程服务器的命令历史
4
+ */
5
+
6
+ import fs from 'fs'
7
+ import path from 'path'
8
+ import chalk from 'chalk'
9
+ import { CONFIG_DIR, getConfig } from './config.js'
10
+ import { getCurrentTheme } from './ui/theme.js'
11
+ import { sshExec, getRemote } from './remote.js'
12
+
13
+ // 获取主题颜色
14
+ function getColors() {
15
+ const theme = getCurrentTheme()
16
+ return {
17
+ primary: theme.primary,
18
+ secondary: theme.secondary,
19
+ success: theme.success,
20
+ error: theme.error,
21
+ warning: theme.warning,
22
+ muted: theme.text.muted,
23
+ }
24
+ }
25
+
26
+ // 远程服务器数据目录
27
+ const REMOTES_DIR = path.join(CONFIG_DIR, 'remotes')
28
+
29
+ /**
30
+ * 远程命令历史记录
31
+ */
32
+ export interface RemoteHistoryRecord {
33
+ userPrompt: string
34
+ command: string
35
+ aiGeneratedCommand?: string // AI 原始命令
36
+ userModified?: boolean // 用户是否修改
37
+ executed: boolean
38
+ exitCode: number | null
39
+ output: string
40
+ timestamp: string
41
+ reason?: string // 未执行原因
42
+ }
43
+
44
+ /**
45
+ * Shell 历史记录项
46
+ */
47
+ export interface RemoteShellHistoryItem {
48
+ cmd: string
49
+ exit: number
50
+ time: string
51
+ }
52
+
53
+ // ================== 命令历史管理 ==================
54
+
55
+ /**
56
+ * 获取远程服务器历史文件路径
57
+ */
58
+ function getRemoteHistoryPath(name: string): string {
59
+ return path.join(REMOTES_DIR, name, 'history.json')
60
+ }
61
+
62
+ /**
63
+ * 获取远程服务器命令历史
64
+ */
65
+ export function getRemoteHistory(name: string): RemoteHistoryRecord[] {
66
+ const historyPath = getRemoteHistoryPath(name)
67
+
68
+ if (!fs.existsSync(historyPath)) {
69
+ return []
70
+ }
71
+
72
+ try {
73
+ const content = fs.readFileSync(historyPath, 'utf-8')
74
+ return JSON.parse(content) as RemoteHistoryRecord[]
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 添加远程命令历史记录
82
+ */
83
+ export function addRemoteHistory(name: string, record: Omit<RemoteHistoryRecord, 'timestamp'>): void {
84
+ const config = getConfig()
85
+ const historyPath = getRemoteHistoryPath(name)
86
+
87
+ // 确保目录存在
88
+ const dir = path.dirname(historyPath)
89
+ if (!fs.existsSync(dir)) {
90
+ fs.mkdirSync(dir, { recursive: true })
91
+ }
92
+
93
+ let history = getRemoteHistory(name)
94
+
95
+ // 添加新记录
96
+ history.push({
97
+ ...record,
98
+ timestamp: new Date().toISOString(),
99
+ })
100
+
101
+ // 限制历史数量
102
+ const limit = config.commandHistoryLimit || 10
103
+ if (history.length > limit) {
104
+ history = history.slice(-limit)
105
+ }
106
+
107
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2))
108
+ }
109
+
110
+ /**
111
+ * 清空远程命令历史
112
+ */
113
+ export function clearRemoteHistory(name: string): void {
114
+ const historyPath = getRemoteHistoryPath(name)
115
+ if (fs.existsSync(historyPath)) {
116
+ fs.unlinkSync(historyPath)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 格式化远程命令历史供 AI 使用
122
+ */
123
+ export function formatRemoteHistoryForAI(name: string): string {
124
+ const history = getRemoteHistory(name)
125
+
126
+ if (history.length === 0) {
127
+ return ''
128
+ }
129
+
130
+ const lines = history.map((record, index) => {
131
+ let status = ''
132
+ if (record.reason === 'builtin') {
133
+ status = '(包含 builtin,未执行)'
134
+ } else if (record.executed) {
135
+ status = record.exitCode === 0 ? '✓' : `✗ 退出码:${record.exitCode}`
136
+ } else {
137
+ status = '(用户取消执行)'
138
+ }
139
+
140
+ // 显示用户修改信息
141
+ if (record.userModified && record.aiGeneratedCommand) {
142
+ return `${index + 1}. "${record.userPrompt}" → AI 生成: ${record.aiGeneratedCommand} / 用户修改为: ${record.command} ${status}`
143
+ } else {
144
+ return `${index + 1}. "${record.userPrompt}" → ${record.command} ${status}`
145
+ }
146
+ })
147
+
148
+ return `【该服务器最近通过 pls 执行的命令】\n${lines.join('\n')}`
149
+ }
150
+
151
+ /**
152
+ * 显示远程命令历史
153
+ */
154
+ export function displayRemoteHistory(name: string): void {
155
+ const remote = getRemote(name)
156
+ const history = getRemoteHistory(name)
157
+ const colors = getColors()
158
+
159
+ if (!remote) {
160
+ console.log('')
161
+ console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
162
+ console.log('')
163
+ return
164
+ }
165
+
166
+ console.log('')
167
+
168
+ if (history.length === 0) {
169
+ console.log(chalk.gray(` 服务器 "${name}" 暂无命令历史`))
170
+ console.log('')
171
+ return
172
+ }
173
+
174
+ console.log(chalk.bold(`📜 服务器 "${name}" 命令历史:`))
175
+ console.log(chalk.gray('━'.repeat(50)))
176
+
177
+ history.forEach((item, index) => {
178
+ const status = item.executed
179
+ ? item.exitCode === 0
180
+ ? chalk.hex(colors.success)('✓')
181
+ : chalk.hex(colors.error)(`✗ 退出码:${item.exitCode}`)
182
+ : chalk.gray('(未执行)')
183
+
184
+ console.log(`\n${chalk.gray(`${index + 1}.`)} ${chalk.hex(colors.primary)(item.userPrompt)}`)
185
+
186
+ // 显示用户修改信息
187
+ if (item.userModified && item.aiGeneratedCommand) {
188
+ console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(item.aiGeneratedCommand)}`)
189
+ console.log(` ${chalk.dim('用户修改为:')} ${item.command} ${status} ${chalk.hex(colors.warning)('(已修改)')}`)
190
+ } else {
191
+ console.log(` ${chalk.dim('→')} ${item.command} ${status}`)
192
+ }
193
+
194
+ console.log(` ${chalk.gray(item.timestamp)}`)
195
+ })
196
+
197
+ console.log('')
198
+ console.log(chalk.gray('━'.repeat(50)))
199
+ console.log(chalk.gray(`历史文件: ${getRemoteHistoryPath(name)}`))
200
+ console.log('')
201
+ }
202
+
203
+ // ================== Shell 历史管理 ==================
204
+
205
+ // 远程 shell 历史的本地缓存文件
206
+ function getRemoteShellHistoryPath(name: string): string {
207
+ return path.join(REMOTES_DIR, name, 'shell_history.jsonl')
208
+ }
209
+
210
+ /**
211
+ * 从远程服务器读取 shell 历史
212
+ * 读取远程 ~/.please/shell_history.jsonl
213
+ */
214
+ export async function fetchRemoteShellHistory(name: string): Promise<RemoteShellHistoryItem[]> {
215
+ const config = getConfig()
216
+ const limit = config.shellHistoryLimit || 15
217
+
218
+ try {
219
+ // 读取远程 shell 历史文件
220
+ const result = await sshExec(name, `tail -n ${limit} ~/.please/shell_history.jsonl 2>/dev/null || echo ""`, {
221
+ timeout: 10000,
222
+ })
223
+
224
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
225
+ return []
226
+ }
227
+
228
+ const lines = result.stdout.trim().split('\n').filter(line => line.trim())
229
+ const items: RemoteShellHistoryItem[] = []
230
+
231
+ for (const line of lines) {
232
+ try {
233
+ const item = JSON.parse(line) as RemoteShellHistoryItem
234
+ items.push(item)
235
+ } catch {
236
+ // 跳过无效行
237
+ }
238
+ }
239
+
240
+ // 缓存到本地
241
+ saveRemoteShellHistoryCache(name, items)
242
+
243
+ return items
244
+ } catch {
245
+ // 如果无法连接,尝试返回缓存
246
+ return getRemoteShellHistoryCache(name)
247
+ }
248
+ }
249
+
250
+ /**
251
+ * 保存远程 shell 历史缓存到本地
252
+ */
253
+ function saveRemoteShellHistoryCache(name: string, items: RemoteShellHistoryItem[]): void {
254
+ const cachePath = getRemoteShellHistoryPath(name)
255
+
256
+ // 确保目录存在
257
+ const dir = path.dirname(cachePath)
258
+ if (!fs.existsSync(dir)) {
259
+ fs.mkdirSync(dir, { recursive: true })
260
+ }
261
+
262
+ const content = items.map(item => JSON.stringify(item)).join('\n')
263
+ fs.writeFileSync(cachePath, content)
264
+ }
265
+
266
+ /**
267
+ * 获取本地缓存的远程 shell 历史
268
+ */
269
+ function getRemoteShellHistoryCache(name: string): RemoteShellHistoryItem[] {
270
+ const cachePath = getRemoteShellHistoryPath(name)
271
+
272
+ if (!fs.existsSync(cachePath)) {
273
+ return []
274
+ }
275
+
276
+ try {
277
+ const content = fs.readFileSync(cachePath, 'utf-8')
278
+ const lines = content.trim().split('\n').filter(line => line.trim())
279
+
280
+ return lines.map(line => {
281
+ try {
282
+ return JSON.parse(line) as RemoteShellHistoryItem
283
+ } catch {
284
+ return null
285
+ }
286
+ }).filter((item): item is RemoteShellHistoryItem => item !== null)
287
+ } catch {
288
+ return []
289
+ }
290
+ }
291
+
292
+ /**
293
+ * 格式化远程 shell 历史供 AI 使用
294
+ */
295
+ export function formatRemoteShellHistoryForAI(items: RemoteShellHistoryItem[]): string {
296
+ if (items.length === 0) {
297
+ return ''
298
+ }
299
+
300
+ const lines = items.map((item, index) => {
301
+ const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`
302
+ return `${index + 1}. ${item.cmd} ${status}`
303
+ })
304
+
305
+ return `【该服务器终端最近执行的命令】\n${lines.join('\n')}`
306
+ }
307
+
308
+ /**
309
+ * 显示远程 shell 历史
310
+ */
311
+ export async function displayRemoteShellHistory(name: string): Promise<void> {
312
+ const remote = getRemote(name)
313
+ const colors = getColors()
314
+
315
+ if (!remote) {
316
+ console.log('')
317
+ console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
318
+ console.log('')
319
+ return
320
+ }
321
+
322
+ console.log('')
323
+ console.log(chalk.gray(`正在从 ${name} 读取 shell 历史...`))
324
+
325
+ try {
326
+ const history = await fetchRemoteShellHistory(name)
327
+
328
+ if (history.length === 0) {
329
+ console.log('')
330
+ console.log(chalk.gray(` 服务器 "${name}" 暂无 shell 历史`))
331
+ console.log(chalk.gray(' 请先安装远程 hook: pls remote hook install ' + name))
332
+ console.log('')
333
+ return
334
+ }
335
+
336
+ console.log('')
337
+ console.log(chalk.bold(`终端历史 - ${name}(最近 ${history.length} 条):`))
338
+ console.log(chalk.gray('━'.repeat(50)))
339
+
340
+ history.forEach((item, index) => {
341
+ const num = index + 1
342
+ const status = item.exit === 0 ? chalk.hex(colors.success)('✓') : chalk.hex(colors.error)(`✗ (${item.exit})`)
343
+ console.log(` ${chalk.hex(colors.primary)(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`)
344
+ })
345
+
346
+ console.log(chalk.gray('━'.repeat(50)))
347
+ console.log(chalk.gray(`远程文件: ~/.please/shell_history.jsonl`))
348
+ console.log('')
349
+ } catch (error) {
350
+ const message = error instanceof Error ? error.message : String(error)
351
+ console.log('')
352
+ console.log(chalk.hex(colors.error)(`✗ 无法读取远程 shell 历史: ${message}`))
353
+ console.log('')
354
+ }
355
+ }
356
+
357
+ /**
358
+ * 清空远程 shell 历史
359
+ */
360
+ export async function clearRemoteShellHistory(name: string): Promise<void> {
361
+ const remote = getRemote(name)
362
+ const colors = getColors()
363
+
364
+ if (!remote) {
365
+ console.log('')
366
+ console.log(chalk.hex(colors.error)(`✗ 服务器 "${name}" 不存在`))
367
+ console.log('')
368
+ return
369
+ }
370
+
371
+ try {
372
+ // 清空远程文件
373
+ await sshExec(name, 'rm -f ~/.please/shell_history.jsonl', { timeout: 10000 })
374
+
375
+ // 清空本地缓存
376
+ const cachePath = getRemoteShellHistoryPath(name)
377
+ if (fs.existsSync(cachePath)) {
378
+ fs.unlinkSync(cachePath)
379
+ }
380
+
381
+ console.log('')
382
+ console.log(chalk.hex(colors.success)(`✓ 服务器 "${name}" 的 shell 历史已清空`))
383
+ console.log('')
384
+ } catch (error) {
385
+ const message = error instanceof Error ? error.message : String(error)
386
+ console.log('')
387
+ console.log(chalk.hex(colors.error)(`✗ 无法清空远程 shell 历史: ${message}`))
388
+ console.log('')
389
+ }
390
+ }