@yivan-lab/pretty-please 1.0.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +380 -0
  3. package/bin/pls.js +681 -0
  4. package/bin/pls.tsx +541 -0
  5. package/dist/bin/pls.d.ts +2 -0
  6. package/dist/bin/pls.js +429 -0
  7. package/dist/src/ai.d.ts +48 -0
  8. package/dist/src/ai.js +295 -0
  9. package/dist/src/builtin-detector.d.ts +15 -0
  10. package/dist/src/builtin-detector.js +83 -0
  11. package/dist/src/chat-history.d.ts +26 -0
  12. package/dist/src/chat-history.js +81 -0
  13. package/dist/src/components/Chat.d.ts +13 -0
  14. package/dist/src/components/Chat.js +80 -0
  15. package/dist/src/components/ChatStatus.d.ts +9 -0
  16. package/dist/src/components/ChatStatus.js +34 -0
  17. package/dist/src/components/CodeColorizer.d.ts +12 -0
  18. package/dist/src/components/CodeColorizer.js +82 -0
  19. package/dist/src/components/CommandBox.d.ts +10 -0
  20. package/dist/src/components/CommandBox.js +45 -0
  21. package/dist/src/components/CommandGenerator.d.ts +20 -0
  22. package/dist/src/components/CommandGenerator.js +116 -0
  23. package/dist/src/components/ConfigDisplay.d.ts +9 -0
  24. package/dist/src/components/ConfigDisplay.js +42 -0
  25. package/dist/src/components/ConfigWizard.d.ts +9 -0
  26. package/dist/src/components/ConfigWizard.js +72 -0
  27. package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
  28. package/dist/src/components/ConfirmationPrompt.js +26 -0
  29. package/dist/src/components/Duration.d.ts +9 -0
  30. package/dist/src/components/Duration.js +21 -0
  31. package/dist/src/components/HistoryDisplay.d.ts +9 -0
  32. package/dist/src/components/HistoryDisplay.js +51 -0
  33. package/dist/src/components/HookManager.d.ts +10 -0
  34. package/dist/src/components/HookManager.js +88 -0
  35. package/dist/src/components/InlineRenderer.d.ts +12 -0
  36. package/dist/src/components/InlineRenderer.js +75 -0
  37. package/dist/src/components/MarkdownDisplay.d.ts +13 -0
  38. package/dist/src/components/MarkdownDisplay.js +197 -0
  39. package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
  40. package/dist/src/components/MultiStepCommandGenerator.js +142 -0
  41. package/dist/src/components/TableRenderer.d.ts +12 -0
  42. package/dist/src/components/TableRenderer.js +66 -0
  43. package/dist/src/config.d.ts +29 -0
  44. package/dist/src/config.js +203 -0
  45. package/dist/src/history.d.ts +20 -0
  46. package/dist/src/history.js +113 -0
  47. package/dist/src/mastra-agent.d.ts +7 -0
  48. package/dist/src/mastra-agent.js +31 -0
  49. package/dist/src/multi-step.d.ts +41 -0
  50. package/dist/src/multi-step.js +67 -0
  51. package/dist/src/shell-hook.d.ts +35 -0
  52. package/dist/src/shell-hook.js +348 -0
  53. package/dist/src/sysinfo.d.ts +15 -0
  54. package/dist/src/sysinfo.js +52 -0
  55. package/dist/src/ui/theme.d.ts +26 -0
  56. package/dist/src/ui/theme.js +31 -0
  57. package/dist/src/utils/console.d.ts +44 -0
  58. package/dist/src/utils/console.js +114 -0
  59. package/package.json +78 -0
  60. package/src/ai.js +324 -0
  61. package/src/builtin-detector.js +98 -0
  62. package/src/chat-history.js +94 -0
  63. package/src/components/Chat.tsx +122 -0
  64. package/src/components/ChatStatus.tsx +53 -0
  65. package/src/components/CodeColorizer.tsx +128 -0
  66. package/src/components/CommandBox.tsx +60 -0
  67. package/src/components/CommandGenerator.tsx +184 -0
  68. package/src/components/ConfigDisplay.tsx +64 -0
  69. package/src/components/ConfigWizard.tsx +101 -0
  70. package/src/components/ConfirmationPrompt.tsx +41 -0
  71. package/src/components/Duration.tsx +24 -0
  72. package/src/components/HistoryDisplay.tsx +69 -0
  73. package/src/components/HookManager.tsx +150 -0
  74. package/src/components/InlineRenderer.tsx +123 -0
  75. package/src/components/MarkdownDisplay.tsx +288 -0
  76. package/src/components/MultiStepCommandGenerator.tsx +229 -0
  77. package/src/components/TableRenderer.tsx +110 -0
  78. package/src/config.js +221 -0
  79. package/src/history.js +131 -0
  80. package/src/mastra-agent.ts +35 -0
  81. package/src/multi-step.ts +93 -0
  82. package/src/shell-hook.js +393 -0
  83. package/src/sysinfo.js +57 -0
  84. package/src/ui/theme.ts +37 -0
  85. package/src/utils/console.js +130 -0
  86. package/tsconfig.json +23 -0
@@ -0,0 +1,101 @@
1
+ import React, { useState } from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import TextInput from 'ink-text-input'
4
+ import { getConfig, saveConfig, maskApiKey } from '../config.js'
5
+ import { theme } from '../ui/theme.js'
6
+ import path from 'path'
7
+ import os from 'os'
8
+
9
+ const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json')
10
+
11
+ interface ConfigWizardProps {
12
+ onComplete: () => void
13
+ }
14
+
15
+ type Step = 'apiKey' | 'baseUrl' | 'model' | 'done'
16
+
17
+ /**
18
+ * ConfigWizard 组件 - 交互式配置向导
19
+ */
20
+ export const ConfigWizard: React.FC<ConfigWizardProps> = ({ onComplete }) => {
21
+ const config = getConfig()
22
+ const [step, setStep] = useState<Step>('apiKey')
23
+ const [apiKey, setApiKey] = useState(config.apiKey)
24
+ const [baseUrl, setBaseUrl] = useState(config.baseUrl)
25
+ const [model, setModel] = useState(config.model)
26
+
27
+ const handleApiKeySubmit = (value: string) => {
28
+ if (value.trim()) {
29
+ setApiKey(value.trim())
30
+ }
31
+ setStep('baseUrl')
32
+ }
33
+
34
+ const handleBaseUrlSubmit = (value: string) => {
35
+ if (value.trim()) {
36
+ setBaseUrl(value.trim())
37
+ }
38
+ setStep('model')
39
+ }
40
+
41
+ const handleModelSubmit = (value: string) => {
42
+ if (value.trim()) {
43
+ setModel(value.trim())
44
+ }
45
+
46
+ // 保存配置
47
+ saveConfig({
48
+ ...config,
49
+ apiKey: apiKey || config.apiKey,
50
+ baseUrl: baseUrl || config.baseUrl,
51
+ model: model.trim() || config.model,
52
+ })
53
+
54
+ setStep('done')
55
+ setTimeout(onComplete, 100)
56
+ }
57
+
58
+ return (
59
+ <Box flexDirection="column" marginY={1}>
60
+ <Text bold color={theme.accent}>
61
+ 🔧 Pretty Please 配置向导
62
+ </Text>
63
+ <Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
64
+
65
+ {step === 'apiKey' && (
66
+ <Box marginTop={1}>
67
+ <Text color={theme.primary}>
68
+ 请输入 API Key{config.apiKey ? ` (当前: ${maskApiKey(config.apiKey)})` : ''}:{' '}
69
+ </Text>
70
+ <TextInput value="" onChange={() => {}} onSubmit={handleApiKeySubmit} />
71
+ </Box>
72
+ )}
73
+
74
+ {step === 'baseUrl' && (
75
+ <Box marginTop={1}>
76
+ <Text color={theme.primary}>
77
+ 请输入 API Base URL (回车使用 {baseUrl}):{' '}
78
+ </Text>
79
+ <TextInput value="" onChange={() => {}} onSubmit={handleBaseUrlSubmit} />
80
+ </Box>
81
+ )}
82
+
83
+ {step === 'model' && (
84
+ <Box marginTop={1}>
85
+ <Text color={theme.primary}>
86
+ 请输入模型名称 (回车使用 {model}):{' '}
87
+ </Text>
88
+ <TextInput value="" onChange={() => {}} onSubmit={handleModelSubmit} />
89
+ </Box>
90
+ )}
91
+
92
+ {step === 'done' && (
93
+ <Box flexDirection="column" marginTop={1}>
94
+ <Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
95
+ <Text color={theme.success}>✅ 配置已保存到 </Text>
96
+ <Text color={theme.text.secondary}>{CONFIG_FILE}</Text>
97
+ </Box>
98
+ )}
99
+ </Box>
100
+ )
101
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react'
2
+ import { Text, useInput } from 'ink'
3
+ import { theme } from '../ui/theme.js'
4
+
5
+ interface ConfirmationPromptProps {
6
+ prompt: string
7
+ onConfirm: () => void
8
+ onCancel: () => void
9
+ }
10
+
11
+ /**
12
+ * ConfirmationPrompt 组件 - 单键确认提示
13
+ * 回车 = 确认,Esc = 取消,Ctrl+C = 退出
14
+ */
15
+ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
16
+ prompt,
17
+ onConfirm,
18
+ onCancel,
19
+ }) => {
20
+ useInput((input, key) => {
21
+ if (key.return) {
22
+ // 回车键
23
+ onConfirm()
24
+ } else if (key.escape) {
25
+ // Esc 键
26
+ onCancel()
27
+ } else if (key.ctrl && input === 'c') {
28
+ // Ctrl+C
29
+ process.exit(0)
30
+ }
31
+ })
32
+
33
+ return (
34
+ <Text>
35
+ <Text bold color={theme.warning}>
36
+ {prompt}
37
+ </Text>
38
+ <Text color={theme.text.secondary}> [回车执行 / Esc 取消] </Text>
39
+ </Text>
40
+ )
41
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { theme } from '../ui/theme.js'
4
+
5
+ interface DurationProps {
6
+ ms: number
7
+ }
8
+
9
+ /**
10
+ * 格式化耗时
11
+ */
12
+ function formatDuration(ms: number): string {
13
+ if (ms < 1000) {
14
+ return `${ms}ms`
15
+ }
16
+ return `${(ms / 1000).toFixed(2)}s`
17
+ }
18
+
19
+ /**
20
+ * Duration 组件 - 显示耗时
21
+ */
22
+ export const Duration: React.FC<DurationProps> = ({ ms }) => {
23
+ return <Text color={theme.text.secondary}>({formatDuration(ms)})</Text>
24
+ }
@@ -0,0 +1,69 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import { getHistory, getHistoryFilePath } from '../history.js'
4
+ import { theme } from '../ui/theme.js'
5
+
6
+ interface HistoryDisplayProps {
7
+ onComplete?: () => void
8
+ }
9
+
10
+ /**
11
+ * HistoryDisplay 组件 - 显示历史记录
12
+ */
13
+ export const HistoryDisplay: React.FC<HistoryDisplayProps> = ({ onComplete }) => {
14
+ const history = getHistory()
15
+
16
+ React.useEffect(() => {
17
+ if (onComplete) {
18
+ setTimeout(onComplete, 100)
19
+ }
20
+ }, [onComplete])
21
+
22
+ if (history.length === 0) {
23
+ return (
24
+ <Box marginY={1}>
25
+ <Text color={theme.text.secondary}>暂无历史记录</Text>
26
+ </Box>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <Box flexDirection="column" marginY={1}>
32
+ <Text bold>📜 命令历史:</Text>
33
+ <Text color={theme.text.secondary}>{'━'.repeat(50)}</Text>
34
+
35
+ {history.map((item, index) => {
36
+ const status = item.executed
37
+ ? item.exitCode === 0
38
+ ? '✓'
39
+ : `✗ 退出码:${item.exitCode}`
40
+ : '(未执行)'
41
+
42
+ const statusColor = item.executed
43
+ ? item.exitCode === 0
44
+ ? theme.success
45
+ : theme.error
46
+ : theme.text.secondary
47
+
48
+ return (
49
+ <Box key={index} flexDirection="column" marginY={1}>
50
+ <Box>
51
+ <Text color={theme.text.secondary}>{index + 1}. </Text>
52
+ <Text color={theme.primary}>{item.userPrompt}</Text>
53
+ </Box>
54
+ <Box marginLeft={3}>
55
+ <Text dimColor>→ </Text>
56
+ <Text>{item.command} </Text>
57
+ <Text color={statusColor}>{status}</Text>
58
+ </Box>
59
+ <Box marginLeft={3}>
60
+ <Text color={theme.text.secondary}>{item.timestamp}</Text>
61
+ </Box>
62
+ </Box>
63
+ )
64
+ })}
65
+
66
+ <Text color={theme.text.secondary}>历史文件: {getHistoryFilePath()}</Text>
67
+ </Box>
68
+ )
69
+ }
@@ -0,0 +1,150 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { Box, Text } from 'ink'
3
+ import {
4
+ getHookStatus,
5
+ installShellHook,
6
+ uninstallShellHook,
7
+ detectShell,
8
+ getShellConfigPath,
9
+ } from '../shell-hook.js'
10
+ import { theme } from '../ui/theme.js'
11
+
12
+ interface HookManagerProps {
13
+ action: 'status' | 'install' | 'uninstall'
14
+ onComplete: () => void
15
+ }
16
+
17
+ /**
18
+ * HookManager 组件 - Hook 管理界面
19
+ */
20
+ export const HookManager: React.FC<HookManagerProps> = ({ action, onComplete }) => {
21
+ const [status, setStatus] = useState(getHookStatus())
22
+ const [message, setMessage] = useState<string | null>(null)
23
+ const [isProcessing, setIsProcessing] = useState(false)
24
+
25
+ useEffect(() => {
26
+ const execute = async () => {
27
+ if (action === 'install') {
28
+ setIsProcessing(true)
29
+ const shellType = detectShell()
30
+ const configPath = getShellConfigPath(shellType)
31
+
32
+ if (shellType === 'unknown') {
33
+ setMessage('❌ 不支持的 shell 类型')
34
+ setIsProcessing(false)
35
+ setTimeout(onComplete, 2000)
36
+ return
37
+ }
38
+
39
+ const result = await installShellHook()
40
+ setStatus(getHookStatus())
41
+ setIsProcessing(false)
42
+
43
+ if (result) {
44
+ setMessage(
45
+ `✅ Shell hook 已安装\n⚠️ 请重启终端或执行: source ${configPath}`
46
+ )
47
+ }
48
+
49
+ setTimeout(onComplete, 3000)
50
+ } else if (action === 'uninstall') {
51
+ setIsProcessing(true)
52
+ uninstallShellHook()
53
+ setStatus(getHookStatus())
54
+ setMessage('✅ Shell hook 已卸载\n⚠️ 请重启终端使其生效')
55
+ setIsProcessing(false)
56
+ setTimeout(onComplete, 3000)
57
+ } else {
58
+ // status
59
+ setTimeout(onComplete, 100)
60
+ }
61
+ }
62
+
63
+ execute()
64
+ }, [action, onComplete])
65
+
66
+ if (action === 'install' || action === 'uninstall') {
67
+ return (
68
+ <Box flexDirection="column" marginY={1}>
69
+ <Text bold color={theme.accent}>
70
+ 🔧 Shell Hook {action === 'install' ? '安装' : '卸载'}向导
71
+ </Text>
72
+ <Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
73
+
74
+ {isProcessing && <Text color={theme.info}>处理中...</Text>}
75
+
76
+ {message && (
77
+ <Box flexDirection="column" marginTop={1}>
78
+ {message.split('\n').map((line, i) => (
79
+ <Text
80
+ key={i}
81
+ color={
82
+ line.startsWith('✅')
83
+ ? theme.success
84
+ : line.startsWith('⚠️')
85
+ ? theme.warning
86
+ : line.startsWith('❌')
87
+ ? theme.error
88
+ : theme.text.primary
89
+ }
90
+ >
91
+ {line}
92
+ </Text>
93
+ ))}
94
+ </Box>
95
+ )}
96
+ </Box>
97
+ )
98
+ }
99
+
100
+ // Status display
101
+ return (
102
+ <Box flexDirection="column" marginY={1}>
103
+ <Text bold>📊 Shell Hook 状态</Text>
104
+ <Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
105
+
106
+ <Box marginTop={1}>
107
+ <Text color={theme.primary}> Shell 类型: </Text>
108
+ <Text>{status.shellType}</Text>
109
+ </Box>
110
+
111
+ <Box>
112
+ <Text color={theme.primary}> 配置文件: </Text>
113
+ <Text>{status.configPath || '未知'}</Text>
114
+ </Box>
115
+
116
+ <Box>
117
+ <Text color={theme.primary}> 已安装: </Text>
118
+ {status.installed ? (
119
+ <Text color={theme.success}>是</Text>
120
+ ) : (
121
+ <Text color={theme.text.secondary}>否</Text>
122
+ )}
123
+ </Box>
124
+
125
+ <Box>
126
+ <Text color={theme.primary}> 已启用: </Text>
127
+ {status.enabled ? (
128
+ <Text color={theme.success}>是</Text>
129
+ ) : (
130
+ <Text color={theme.text.secondary}>否</Text>
131
+ )}
132
+ </Box>
133
+
134
+ <Box>
135
+ <Text color={theme.primary}> 历史文件: </Text>
136
+ <Text>{status.historyFile}</Text>
137
+ </Box>
138
+
139
+ <Text color={theme.text.secondary}>{'━'.repeat(40)}</Text>
140
+
141
+ {!status.installed && (
142
+ <Box marginTop={1}>
143
+ <Text color={theme.text.secondary}>
144
+ 提示: 运行 <Text color={theme.primary}>pls hook install</Text> 安装 shell hook
145
+ </Text>
146
+ </Box>
147
+ )}
148
+ </Box>
149
+ )
150
+ }
@@ -0,0 +1,123 @@
1
+ import React from 'react'
2
+ import { Text } from 'ink'
3
+ import { theme } from '../ui/theme.js'
4
+
5
+ interface RenderInlineProps {
6
+ text: string
7
+ defaultColor?: string
8
+ }
9
+
10
+ /**
11
+ * 行内 Markdown 渲染器
12
+ * 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
13
+ */
14
+ function RenderInlineInternal({ text, defaultColor }: RenderInlineProps) {
15
+ const baseColor = defaultColor || theme.text.primary
16
+
17
+ // 快速路径:纯文本无 markdown
18
+ if (!/[*_~`<[https?:]/.test(text)) {
19
+ return <Text color={baseColor}>{text}</Text>
20
+ }
21
+
22
+ const nodes: React.ReactNode[] = []
23
+ let lastIndex = 0
24
+
25
+ // 匹配所有行内 markdown 语法(添加删除线、下划线、链接)
26
+ const inlineRegex = /(~~.+?~~|\*\*[^*]+?\*\*|\*[^*]+?\*|_[^_]+?_|`+[^`]+?`+|\[.*?\]\(.*?\)|<u>.*?<\/u>|https?:\/\/\S+)/g
27
+ let match
28
+
29
+ while ((match = inlineRegex.exec(text)) !== null) {
30
+ // 添加匹配之前的普通文本
31
+ if (match.index > lastIndex) {
32
+ nodes.push(
33
+ <Text key={`t-${lastIndex}`} color={baseColor}>
34
+ {text.slice(lastIndex, match.index)}
35
+ </Text>
36
+ )
37
+ }
38
+
39
+ const fullMatch = match[0]
40
+ let renderedNode: React.ReactNode = null
41
+ const key = `m-${match.index}`
42
+
43
+ // **粗体**
44
+ if (fullMatch.startsWith('**') && fullMatch.endsWith('**') && fullMatch.length > 4) {
45
+ renderedNode = (
46
+ <Text key={key} bold color={baseColor}>
47
+ {fullMatch.slice(2, -2)}
48
+ </Text>
49
+ )
50
+ }
51
+ // ~~删除线~~
52
+ else if (fullMatch.startsWith('~~') && fullMatch.endsWith('~~') && fullMatch.length > 4) {
53
+ renderedNode = (
54
+ <Text key={key} strikethrough color={baseColor}>
55
+ {fullMatch.slice(2, -2)}
56
+ </Text>
57
+ )
58
+ }
59
+ // *斜体*
60
+ else if (fullMatch.startsWith('*') && fullMatch.endsWith('*') && fullMatch.length > 2 && !fullMatch.startsWith('**')) {
61
+ renderedNode = (
62
+ <Text key={key} italic color={baseColor}>
63
+ {fullMatch.slice(1, -1)}
64
+ </Text>
65
+ )
66
+ }
67
+ // `行内代码`
68
+ else if (fullMatch.startsWith('`') && fullMatch.endsWith('`')) {
69
+ const codeText = fullMatch.slice(1, -1)
70
+ renderedNode = (
71
+ <Text key={key} color={theme.primary}>
72
+ {codeText}
73
+ </Text>
74
+ )
75
+ }
76
+ // [链接文本](URL)
77
+ else if (fullMatch.startsWith('[') && fullMatch.includes('](') && fullMatch.endsWith(')')) {
78
+ const linkMatch = fullMatch.match(/\[(.*?)\]\((.*?)\)/)
79
+ if (linkMatch) {
80
+ const linkText = linkMatch[1]
81
+ const url = linkMatch[2]
82
+ renderedNode = (
83
+ <Text key={key} color={baseColor}>
84
+ {linkText}
85
+ <Text color={theme.info}> ({url})</Text>
86
+ </Text>
87
+ )
88
+ }
89
+ }
90
+ // <u>下划线</u>
91
+ else if (fullMatch.startsWith('<u>') && fullMatch.endsWith('</u>') && fullMatch.length > 7) {
92
+ renderedNode = (
93
+ <Text key={key} underline color={baseColor}>
94
+ {fullMatch.slice(3, -4)}
95
+ </Text>
96
+ )
97
+ }
98
+ // 裸 URL
99
+ else if (fullMatch.match(/^https?:\/\//)) {
100
+ renderedNode = (
101
+ <Text key={key} color={theme.info}>
102
+ {fullMatch}
103
+ </Text>
104
+ )
105
+ }
106
+
107
+ nodes.push(renderedNode || <Text key={key} color={baseColor}>{fullMatch}</Text>)
108
+ lastIndex = inlineRegex.lastIndex
109
+ }
110
+
111
+ // 添加剩余的普通文本
112
+ if (lastIndex < text.length) {
113
+ nodes.push(
114
+ <Text key={`t-${lastIndex}`} color={baseColor}>
115
+ {text.slice(lastIndex)}
116
+ </Text>
117
+ )
118
+ }
119
+
120
+ return <>{nodes}</>
121
+ }
122
+
123
+ export const RenderInline = React.memo(RenderInlineInternal)