@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.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/bin/pls.js +681 -0
- package/bin/pls.tsx +541 -0
- package/dist/bin/pls.d.ts +2 -0
- package/dist/bin/pls.js +429 -0
- package/dist/src/ai.d.ts +48 -0
- package/dist/src/ai.js +295 -0
- package/dist/src/builtin-detector.d.ts +15 -0
- package/dist/src/builtin-detector.js +83 -0
- package/dist/src/chat-history.d.ts +26 -0
- package/dist/src/chat-history.js +81 -0
- package/dist/src/components/Chat.d.ts +13 -0
- package/dist/src/components/Chat.js +80 -0
- package/dist/src/components/ChatStatus.d.ts +9 -0
- package/dist/src/components/ChatStatus.js +34 -0
- package/dist/src/components/CodeColorizer.d.ts +12 -0
- package/dist/src/components/CodeColorizer.js +82 -0
- package/dist/src/components/CommandBox.d.ts +10 -0
- package/dist/src/components/CommandBox.js +45 -0
- package/dist/src/components/CommandGenerator.d.ts +20 -0
- package/dist/src/components/CommandGenerator.js +116 -0
- package/dist/src/components/ConfigDisplay.d.ts +9 -0
- package/dist/src/components/ConfigDisplay.js +42 -0
- package/dist/src/components/ConfigWizard.d.ts +9 -0
- package/dist/src/components/ConfigWizard.js +72 -0
- package/dist/src/components/ConfirmationPrompt.d.ts +12 -0
- package/dist/src/components/ConfirmationPrompt.js +26 -0
- package/dist/src/components/Duration.d.ts +9 -0
- package/dist/src/components/Duration.js +21 -0
- package/dist/src/components/HistoryDisplay.d.ts +9 -0
- package/dist/src/components/HistoryDisplay.js +51 -0
- package/dist/src/components/HookManager.d.ts +10 -0
- package/dist/src/components/HookManager.js +88 -0
- package/dist/src/components/InlineRenderer.d.ts +12 -0
- package/dist/src/components/InlineRenderer.js +75 -0
- package/dist/src/components/MarkdownDisplay.d.ts +13 -0
- package/dist/src/components/MarkdownDisplay.js +197 -0
- package/dist/src/components/MultiStepCommandGenerator.d.ts +25 -0
- package/dist/src/components/MultiStepCommandGenerator.js +142 -0
- package/dist/src/components/TableRenderer.d.ts +12 -0
- package/dist/src/components/TableRenderer.js +66 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +203 -0
- package/dist/src/history.d.ts +20 -0
- package/dist/src/history.js +113 -0
- package/dist/src/mastra-agent.d.ts +7 -0
- package/dist/src/mastra-agent.js +31 -0
- package/dist/src/multi-step.d.ts +41 -0
- package/dist/src/multi-step.js +67 -0
- package/dist/src/shell-hook.d.ts +35 -0
- package/dist/src/shell-hook.js +348 -0
- package/dist/src/sysinfo.d.ts +15 -0
- package/dist/src/sysinfo.js +52 -0
- package/dist/src/ui/theme.d.ts +26 -0
- package/dist/src/ui/theme.js +31 -0
- package/dist/src/utils/console.d.ts +44 -0
- package/dist/src/utils/console.js +114 -0
- package/package.json +78 -0
- package/src/ai.js +324 -0
- package/src/builtin-detector.js +98 -0
- package/src/chat-history.js +94 -0
- package/src/components/Chat.tsx +122 -0
- package/src/components/ChatStatus.tsx +53 -0
- package/src/components/CodeColorizer.tsx +128 -0
- package/src/components/CommandBox.tsx +60 -0
- package/src/components/CommandGenerator.tsx +184 -0
- package/src/components/ConfigDisplay.tsx +64 -0
- package/src/components/ConfigWizard.tsx +101 -0
- package/src/components/ConfirmationPrompt.tsx +41 -0
- package/src/components/Duration.tsx +24 -0
- package/src/components/HistoryDisplay.tsx +69 -0
- package/src/components/HookManager.tsx +150 -0
- package/src/components/InlineRenderer.tsx +123 -0
- package/src/components/MarkdownDisplay.tsx +288 -0
- package/src/components/MultiStepCommandGenerator.tsx +229 -0
- package/src/components/TableRenderer.tsx +110 -0
- package/src/config.js +221 -0
- package/src/history.js +131 -0
- package/src/mastra-agent.ts +35 -0
- package/src/multi-step.ts +93 -0
- package/src/shell-hook.js +393 -0
- package/src/sysinfo.js +57 -0
- package/src/ui/theme.ts +37 -0
- package/src/utils/console.js +130 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { common, createLowlight } from 'lowlight';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
// 创建 lowlight 实例
|
|
6
|
+
const lowlight = createLowlight(common);
|
|
7
|
+
// 语法高亮颜色映射
|
|
8
|
+
const syntaxColors = {
|
|
9
|
+
'hljs-keyword': theme.code.keyword,
|
|
10
|
+
'hljs-string': theme.code.string,
|
|
11
|
+
'hljs-function': theme.code.function,
|
|
12
|
+
'hljs-comment': theme.code.comment,
|
|
13
|
+
'hljs-number': theme.primary,
|
|
14
|
+
'hljs-built_in': theme.secondary,
|
|
15
|
+
'hljs-title': theme.accent,
|
|
16
|
+
'hljs-variable': theme.text.primary,
|
|
17
|
+
'hljs-type': theme.info,
|
|
18
|
+
'hljs-operator': theme.text.secondary,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* 渲染 HAST 语法树节点
|
|
22
|
+
*/
|
|
23
|
+
function renderHastNode(node, inheritedColor) {
|
|
24
|
+
if (node.type === 'text') {
|
|
25
|
+
const color = inheritedColor || theme.code.text;
|
|
26
|
+
return React.createElement(Text, { color: color }, node.value);
|
|
27
|
+
}
|
|
28
|
+
if (node.type === 'element') {
|
|
29
|
+
const nodeClasses = node.properties?.['className'] || [];
|
|
30
|
+
let elementColor = undefined;
|
|
31
|
+
// 查找颜色
|
|
32
|
+
for (let i = nodeClasses.length - 1; i >= 0; i--) {
|
|
33
|
+
const className = nodeClasses[i];
|
|
34
|
+
if (syntaxColors[className]) {
|
|
35
|
+
elementColor = syntaxColors[className];
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const colorToPassDown = elementColor || inheritedColor;
|
|
40
|
+
// 递归渲染子节点
|
|
41
|
+
const children = node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, colorToPassDown))));
|
|
42
|
+
return React.createElement(React.Fragment, null, children);
|
|
43
|
+
}
|
|
44
|
+
if (node.type === 'root') {
|
|
45
|
+
if (!node.children || node.children.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return node.children?.map((child, index) => (React.createElement(React.Fragment, { key: index }, renderHastNode(child, inheritedColor))));
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 高亮并渲染一行代码
|
|
54
|
+
*/
|
|
55
|
+
function highlightLine(line, language) {
|
|
56
|
+
try {
|
|
57
|
+
const highlighted = !language || !lowlight.registered(language)
|
|
58
|
+
? lowlight.highlightAuto(line)
|
|
59
|
+
: lowlight.highlight(language, line);
|
|
60
|
+
const rendered = renderHastNode(highlighted, undefined);
|
|
61
|
+
return rendered !== null ? rendered : line;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return line;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 代码高亮组件
|
|
69
|
+
*/
|
|
70
|
+
function ColorizeCodeInternal({ code, language = null, showLineNumbers = false }) {
|
|
71
|
+
const codeToHighlight = code.replace(/\n$/, '');
|
|
72
|
+
const lines = codeToHighlight.split('\n');
|
|
73
|
+
const padWidth = String(lines.length).length;
|
|
74
|
+
const renderedLines = lines.map((line, index) => {
|
|
75
|
+
const contentToRender = highlightLine(line, language);
|
|
76
|
+
return (React.createElement(Box, { key: index, minHeight: 1 },
|
|
77
|
+
showLineNumbers && (React.createElement(Text, { color: theme.text.dim }, `${String(index + 1).padStart(padWidth, ' ')} `)),
|
|
78
|
+
React.createElement(Text, { color: theme.code.text }, contentToRender)));
|
|
79
|
+
});
|
|
80
|
+
return (React.createElement(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, paddingY: 1, borderStyle: "round", borderColor: theme.border }, renderedLines));
|
|
81
|
+
}
|
|
82
|
+
export const ColorizeCode = React.memo(ColorizeCodeInternal);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
/**
|
|
5
|
+
* 计算字符串的显示宽度(中文占2个宽度)
|
|
6
|
+
*/
|
|
7
|
+
function getDisplayWidth(str) {
|
|
8
|
+
let width = 0;
|
|
9
|
+
for (const char of str) {
|
|
10
|
+
// 中文、日文、韩文等宽字符占 2 个宽度
|
|
11
|
+
if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
|
|
12
|
+
width += 2;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
width += 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return width;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* CommandBox 组件 - 显示带边框和标题的命令框
|
|
22
|
+
*/
|
|
23
|
+
export const CommandBox = ({ command, title = '生成命令' }) => {
|
|
24
|
+
const lines = command.split('\n');
|
|
25
|
+
const titleWidth = getDisplayWidth(title);
|
|
26
|
+
const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
|
|
27
|
+
const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20);
|
|
28
|
+
// 顶部边框:┌─ 生成命令 ─────┐
|
|
29
|
+
const topPadding = boxWidth - titleWidth - 5;
|
|
30
|
+
const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐';
|
|
31
|
+
// 底部边框
|
|
32
|
+
const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
|
|
33
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
34
|
+
React.createElement(Text, { color: theme.warning }, topBorder),
|
|
35
|
+
lines.map((line, index) => {
|
|
36
|
+
const lineWidth = getDisplayWidth(line);
|
|
37
|
+
const padding = ' '.repeat(boxWidth - lineWidth - 4);
|
|
38
|
+
return (React.createElement(Text, { key: index },
|
|
39
|
+
React.createElement(Text, { color: theme.warning }, "\u2502 "),
|
|
40
|
+
React.createElement(Text, { color: theme.primary }, line),
|
|
41
|
+
React.createElement(Text, null, padding),
|
|
42
|
+
React.createElement(Text, { color: theme.warning }, " \u2502")));
|
|
43
|
+
}),
|
|
44
|
+
React.createElement(Text, { color: theme.warning }, bottomBorder)));
|
|
45
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface CommandGeneratorProps {
|
|
3
|
+
prompt: string;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
onComplete: (result: {
|
|
6
|
+
command?: string;
|
|
7
|
+
confirmed?: boolean;
|
|
8
|
+
cancelled?: boolean;
|
|
9
|
+
hasBuiltin?: boolean;
|
|
10
|
+
builtins?: string[];
|
|
11
|
+
debugInfo?: any;
|
|
12
|
+
error?: string;
|
|
13
|
+
}) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* CommandGenerator 组件 - 命令生成和确认(仅用于交互)
|
|
17
|
+
* 不执行命令,执行交给调用方用原生方式处理
|
|
18
|
+
*/
|
|
19
|
+
export declare const CommandGenerator: React.FC<CommandGeneratorProps>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { generateCommand } from '../ai.js';
|
|
5
|
+
import { detectBuiltin, formatBuiltins } from '../builtin-detector.js';
|
|
6
|
+
import { CommandBox } from './CommandBox.js';
|
|
7
|
+
import { ConfirmationPrompt } from './ConfirmationPrompt.js';
|
|
8
|
+
import { Duration } from './Duration.js';
|
|
9
|
+
import { theme } from '../ui/theme.js';
|
|
10
|
+
/**
|
|
11
|
+
* CommandGenerator 组件 - 命令生成和确认(仅用于交互)
|
|
12
|
+
* 不执行命令,执行交给调用方用原生方式处理
|
|
13
|
+
*/
|
|
14
|
+
export const CommandGenerator = ({ prompt, debug, onComplete }) => {
|
|
15
|
+
const [state, setState] = useState({ type: 'thinking' });
|
|
16
|
+
const [thinkDuration, setThinkDuration] = useState(0);
|
|
17
|
+
const [debugInfo, setDebugInfo] = useState(null);
|
|
18
|
+
// 初始化:调用 AI 生成命令
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const thinkStart = Date.now();
|
|
21
|
+
generateCommand(prompt, { debug: debug || false })
|
|
22
|
+
.then((result) => {
|
|
23
|
+
const command = debug && typeof result === 'object' ? result.command : result;
|
|
24
|
+
const thinkEnd = Date.now();
|
|
25
|
+
setThinkDuration(thinkEnd - thinkStart);
|
|
26
|
+
if (debug && typeof result === 'object' && 'debug' in result) {
|
|
27
|
+
setDebugInfo(result.debug);
|
|
28
|
+
}
|
|
29
|
+
// 检测 builtin
|
|
30
|
+
const { hasBuiltin, builtins } = detectBuiltin(command);
|
|
31
|
+
setState({
|
|
32
|
+
type: 'showing_command',
|
|
33
|
+
command,
|
|
34
|
+
hasBuiltin,
|
|
35
|
+
builtins,
|
|
36
|
+
});
|
|
37
|
+
// 如果是 builtin,直接完成(不执行)
|
|
38
|
+
if (hasBuiltin) {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
onComplete({
|
|
41
|
+
command,
|
|
42
|
+
confirmed: false,
|
|
43
|
+
hasBuiltin: true,
|
|
44
|
+
builtins,
|
|
45
|
+
debugInfo: debugInfo || undefined,
|
|
46
|
+
});
|
|
47
|
+
}, 100);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.catch((error) => {
|
|
51
|
+
setState({ type: 'error', error: error.message });
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
onComplete({ error: error.message });
|
|
54
|
+
}, 100);
|
|
55
|
+
});
|
|
56
|
+
}, [prompt, debug]);
|
|
57
|
+
// 处理确认
|
|
58
|
+
const handleConfirm = () => {
|
|
59
|
+
if (state.type === 'showing_command') {
|
|
60
|
+
// 返回命令和确认状态,让调用方执行
|
|
61
|
+
onComplete({
|
|
62
|
+
command: state.command,
|
|
63
|
+
confirmed: true,
|
|
64
|
+
debugInfo: debugInfo || undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// 处理取消
|
|
69
|
+
const handleCancel = () => {
|
|
70
|
+
if (state.type === 'showing_command') {
|
|
71
|
+
setState({ type: 'cancelled', command: state.command });
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
onComplete({
|
|
74
|
+
command: state.command,
|
|
75
|
+
cancelled: true,
|
|
76
|
+
});
|
|
77
|
+
}, 100);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
81
|
+
state.type === 'thinking' && (React.createElement(Box, null,
|
|
82
|
+
React.createElement(Text, { color: theme.info },
|
|
83
|
+
React.createElement(Spinner, { type: "dots" }),
|
|
84
|
+
" \u6B63\u5728\u601D\u8003..."))),
|
|
85
|
+
state.type !== 'thinking' && thinkDuration > 0 && (React.createElement(Box, null,
|
|
86
|
+
React.createElement(Text, { color: theme.success }, "\u2713 \u601D\u8003\u5B8C\u6210 "),
|
|
87
|
+
React.createElement(Duration, { ms: thinkDuration }))),
|
|
88
|
+
debugInfo && (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
89
|
+
React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501 \u8C03\u8BD5\u4FE1\u606F \u2501\u2501\u2501"),
|
|
90
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
91
|
+
"\u7CFB\u7EDF\u4FE1\u606F: ",
|
|
92
|
+
debugInfo.sysinfo),
|
|
93
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
94
|
+
"\u6A21\u578B: ",
|
|
95
|
+
debugInfo.model),
|
|
96
|
+
React.createElement(Text, { color: theme.text.secondary }, "System Prompt:"),
|
|
97
|
+
React.createElement(Text, { dimColor: true }, debugInfo.systemPrompt),
|
|
98
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
99
|
+
"User Prompt: ",
|
|
100
|
+
debugInfo.userPrompt),
|
|
101
|
+
React.createElement(Text, { color: theme.accent }, "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"))),
|
|
102
|
+
(state.type === 'showing_command' || state.type === 'cancelled') && (React.createElement(CommandBox, { command: state.command })),
|
|
103
|
+
state.type === 'showing_command' && state.hasBuiltin && (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
104
|
+
React.createElement(Text, { color: theme.error },
|
|
105
|
+
"\u26A0\uFE0F \u6B64\u547D\u4EE4\u5305\u542B shell \u5185\u7F6E\u547D\u4EE4\uFF08",
|
|
106
|
+
formatBuiltins(state.builtins),
|
|
107
|
+
"\uFF09\uFF0C\u65E0\u6CD5\u5728\u5B50\u8FDB\u7A0B\u4E2D\u751F\u6548"),
|
|
108
|
+
React.createElement(Text, { color: theme.warning }, "\uD83D\uDCA1 \u8BF7\u624B\u52A8\u590D\u5236\u5230\u7EC8\u7AEF\u6267\u884C"))),
|
|
109
|
+
state.type === 'showing_command' && !state.hasBuiltin && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel })),
|
|
110
|
+
state.type === 'cancelled' && (React.createElement(Box, { marginTop: 1 },
|
|
111
|
+
React.createElement(Text, { color: theme.text.secondary }, "\u5DF2\u53D6\u6D88\u6267\u884C"))),
|
|
112
|
+
state.type === 'error' && (React.createElement(Box, { marginTop: 1 },
|
|
113
|
+
React.createElement(Text, { color: theme.error },
|
|
114
|
+
"\u274C \u9519\u8BEF: ",
|
|
115
|
+
state.error)))));
|
|
116
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { getConfig, maskApiKey } from '../config.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json');
|
|
8
|
+
/**
|
|
9
|
+
* ConfigDisplay 组件 - 显示当前配置
|
|
10
|
+
*/
|
|
11
|
+
export const ConfigDisplay = ({ onComplete }) => {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (onComplete) {
|
|
15
|
+
setTimeout(onComplete, 100);
|
|
16
|
+
}
|
|
17
|
+
}, [onComplete]);
|
|
18
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
19
|
+
React.createElement(Text, { bold: true }, "\u5F53\u524D\u914D\u7F6E:"),
|
|
20
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
21
|
+
React.createElement(Box, null,
|
|
22
|
+
React.createElement(Text, { color: theme.primary }, " apiKey: "),
|
|
23
|
+
React.createElement(Text, null, maskApiKey(config.apiKey))),
|
|
24
|
+
React.createElement(Box, null,
|
|
25
|
+
React.createElement(Text, { color: theme.primary }, " baseUrl: "),
|
|
26
|
+
React.createElement(Text, null, config.baseUrl)),
|
|
27
|
+
React.createElement(Box, null,
|
|
28
|
+
React.createElement(Text, { color: theme.primary }, " model: "),
|
|
29
|
+
React.createElement(Text, null, config.model)),
|
|
30
|
+
React.createElement(Box, null,
|
|
31
|
+
React.createElement(Text, { color: theme.primary }, " shellHook: "),
|
|
32
|
+
config.shellHook ? (React.createElement(Text, { color: theme.success }, "\u5DF2\u542F\u7528")) : (React.createElement(Text, { color: theme.text.secondary }, "\u672A\u542F\u7528"))),
|
|
33
|
+
React.createElement(Box, null,
|
|
34
|
+
React.createElement(Text, { color: theme.primary }, " chatHistoryLimit: "),
|
|
35
|
+
React.createElement(Text, null,
|
|
36
|
+
config.chatHistoryLimit,
|
|
37
|
+
" \u8F6E")),
|
|
38
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
39
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
40
|
+
"\u914D\u7F6E\u6587\u4EF6: ",
|
|
41
|
+
CONFIG_FILE)));
|
|
42
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
const CONFIG_FILE = path.join(os.homedir(), '.please', 'config.json');
|
|
9
|
+
/**
|
|
10
|
+
* ConfigWizard 组件 - 交互式配置向导
|
|
11
|
+
*/
|
|
12
|
+
export const ConfigWizard = ({ onComplete }) => {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
const [step, setStep] = useState('apiKey');
|
|
15
|
+
const [apiKey, setApiKey] = useState(config.apiKey);
|
|
16
|
+
const [baseUrl, setBaseUrl] = useState(config.baseUrl);
|
|
17
|
+
const [model, setModel] = useState(config.model);
|
|
18
|
+
const handleApiKeySubmit = (value) => {
|
|
19
|
+
if (value.trim()) {
|
|
20
|
+
setApiKey(value.trim());
|
|
21
|
+
}
|
|
22
|
+
setStep('baseUrl');
|
|
23
|
+
};
|
|
24
|
+
const handleBaseUrlSubmit = (value) => {
|
|
25
|
+
if (value.trim()) {
|
|
26
|
+
setBaseUrl(value.trim());
|
|
27
|
+
}
|
|
28
|
+
setStep('model');
|
|
29
|
+
};
|
|
30
|
+
const handleModelSubmit = (value) => {
|
|
31
|
+
if (value.trim()) {
|
|
32
|
+
setModel(value.trim());
|
|
33
|
+
}
|
|
34
|
+
// 保存配置
|
|
35
|
+
saveConfig({
|
|
36
|
+
...config,
|
|
37
|
+
apiKey: apiKey || config.apiKey,
|
|
38
|
+
baseUrl: baseUrl || config.baseUrl,
|
|
39
|
+
model: model.trim() || config.model,
|
|
40
|
+
});
|
|
41
|
+
setStep('done');
|
|
42
|
+
setTimeout(onComplete, 100);
|
|
43
|
+
};
|
|
44
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
45
|
+
React.createElement(Text, { bold: true, color: theme.accent }, "\uD83D\uDD27 Pretty Please \u914D\u7F6E\u5411\u5BFC"),
|
|
46
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
47
|
+
step === 'apiKey' && (React.createElement(Box, { marginTop: 1 },
|
|
48
|
+
React.createElement(Text, { color: theme.primary },
|
|
49
|
+
"\u8BF7\u8F93\u5165 API Key",
|
|
50
|
+
config.apiKey ? ` (当前: ${maskApiKey(config.apiKey)})` : '',
|
|
51
|
+
":",
|
|
52
|
+
' '),
|
|
53
|
+
React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleApiKeySubmit }))),
|
|
54
|
+
step === 'baseUrl' && (React.createElement(Box, { marginTop: 1 },
|
|
55
|
+
React.createElement(Text, { color: theme.primary },
|
|
56
|
+
"\u8BF7\u8F93\u5165 API Base URL (\u56DE\u8F66\u4F7F\u7528 ",
|
|
57
|
+
baseUrl,
|
|
58
|
+
"):",
|
|
59
|
+
' '),
|
|
60
|
+
React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleBaseUrlSubmit }))),
|
|
61
|
+
step === 'model' && (React.createElement(Box, { marginTop: 1 },
|
|
62
|
+
React.createElement(Text, { color: theme.primary },
|
|
63
|
+
"\u8BF7\u8F93\u5165\u6A21\u578B\u540D\u79F0 (\u56DE\u8F66\u4F7F\u7528 ",
|
|
64
|
+
model,
|
|
65
|
+
"):",
|
|
66
|
+
' '),
|
|
67
|
+
React.createElement(TextInput, { value: "", onChange: () => { }, onSubmit: handleModelSubmit }))),
|
|
68
|
+
step === 'done' && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
69
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
70
|
+
React.createElement(Text, { color: theme.success }, "\u2705 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 "),
|
|
71
|
+
React.createElement(Text, { color: theme.text.secondary }, CONFIG_FILE)))));
|
|
72
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ConfirmationPromptProps {
|
|
3
|
+
prompt: string;
|
|
4
|
+
onConfirm: () => void;
|
|
5
|
+
onCancel: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* ConfirmationPrompt 组件 - 单键确认提示
|
|
9
|
+
* 回车 = 确认,Esc = 取消,Ctrl+C = 退出
|
|
10
|
+
*/
|
|
11
|
+
export declare const ConfirmationPrompt: React.FC<ConfirmationPromptProps>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
/**
|
|
5
|
+
* ConfirmationPrompt 组件 - 单键确认提示
|
|
6
|
+
* 回车 = 确认,Esc = 取消,Ctrl+C = 退出
|
|
7
|
+
*/
|
|
8
|
+
export const ConfirmationPrompt = ({ prompt, onConfirm, onCancel, }) => {
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (key.return) {
|
|
11
|
+
// 回车键
|
|
12
|
+
onConfirm();
|
|
13
|
+
}
|
|
14
|
+
else if (key.escape) {
|
|
15
|
+
// Esc 键
|
|
16
|
+
onCancel();
|
|
17
|
+
}
|
|
18
|
+
else if (key.ctrl && input === 'c') {
|
|
19
|
+
// Ctrl+C
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return (React.createElement(Text, null,
|
|
24
|
+
React.createElement(Text, { bold: true, color: theme.warning }, prompt),
|
|
25
|
+
React.createElement(Text, { color: theme.text.secondary }, " [\u56DE\u8F66\u6267\u884C / Esc \u53D6\u6D88] ")));
|
|
26
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { theme } from '../ui/theme.js';
|
|
4
|
+
/**
|
|
5
|
+
* 格式化耗时
|
|
6
|
+
*/
|
|
7
|
+
function formatDuration(ms) {
|
|
8
|
+
if (ms < 1000) {
|
|
9
|
+
return `${ms}ms`;
|
|
10
|
+
}
|
|
11
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Duration 组件 - 显示耗时
|
|
15
|
+
*/
|
|
16
|
+
export const Duration = ({ ms }) => {
|
|
17
|
+
return React.createElement(Text, { color: theme.text.secondary },
|
|
18
|
+
"(",
|
|
19
|
+
formatDuration(ms),
|
|
20
|
+
")");
|
|
21
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
* HistoryDisplay 组件 - 显示历史记录
|
|
7
|
+
*/
|
|
8
|
+
export const HistoryDisplay = ({ onComplete }) => {
|
|
9
|
+
const history = getHistory();
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (onComplete) {
|
|
12
|
+
setTimeout(onComplete, 100);
|
|
13
|
+
}
|
|
14
|
+
}, [onComplete]);
|
|
15
|
+
if (history.length === 0) {
|
|
16
|
+
return (React.createElement(Box, { marginY: 1 },
|
|
17
|
+
React.createElement(Text, { color: theme.text.secondary }, "\u6682\u65E0\u5386\u53F2\u8BB0\u5F55")));
|
|
18
|
+
}
|
|
19
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
20
|
+
React.createElement(Text, { bold: true }, "\uD83D\uDCDC \u547D\u4EE4\u5386\u53F2:"),
|
|
21
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(50)),
|
|
22
|
+
history.map((item, index) => {
|
|
23
|
+
const status = item.executed
|
|
24
|
+
? item.exitCode === 0
|
|
25
|
+
? '✓'
|
|
26
|
+
: `✗ 退出码:${item.exitCode}`
|
|
27
|
+
: '(未执行)';
|
|
28
|
+
const statusColor = item.executed
|
|
29
|
+
? item.exitCode === 0
|
|
30
|
+
? theme.success
|
|
31
|
+
: theme.error
|
|
32
|
+
: theme.text.secondary;
|
|
33
|
+
return (React.createElement(Box, { key: index, flexDirection: "column", marginY: 1 },
|
|
34
|
+
React.createElement(Box, null,
|
|
35
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
36
|
+
index + 1,
|
|
37
|
+
". "),
|
|
38
|
+
React.createElement(Text, { color: theme.primary }, item.userPrompt)),
|
|
39
|
+
React.createElement(Box, { marginLeft: 3 },
|
|
40
|
+
React.createElement(Text, { dimColor: true }, "\u2192 "),
|
|
41
|
+
React.createElement(Text, null,
|
|
42
|
+
item.command,
|
|
43
|
+
" "),
|
|
44
|
+
React.createElement(Text, { color: statusColor }, status)),
|
|
45
|
+
React.createElement(Box, { marginLeft: 3 },
|
|
46
|
+
React.createElement(Text, { color: theme.text.secondary }, item.timestamp))));
|
|
47
|
+
}),
|
|
48
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
49
|
+
"\u5386\u53F2\u6587\u4EF6: ",
|
|
50
|
+
getHistoryFilePath())));
|
|
51
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { getHookStatus, installShellHook, uninstallShellHook, detectShell, getShellConfigPath, } from '../shell-hook.js';
|
|
4
|
+
import { theme } from '../ui/theme.js';
|
|
5
|
+
/**
|
|
6
|
+
* HookManager 组件 - Hook 管理界面
|
|
7
|
+
*/
|
|
8
|
+
export const HookManager = ({ action, onComplete }) => {
|
|
9
|
+
const [status, setStatus] = useState(getHookStatus());
|
|
10
|
+
const [message, setMessage] = useState(null);
|
|
11
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const execute = async () => {
|
|
14
|
+
if (action === 'install') {
|
|
15
|
+
setIsProcessing(true);
|
|
16
|
+
const shellType = detectShell();
|
|
17
|
+
const configPath = getShellConfigPath(shellType);
|
|
18
|
+
if (shellType === 'unknown') {
|
|
19
|
+
setMessage('❌ 不支持的 shell 类型');
|
|
20
|
+
setIsProcessing(false);
|
|
21
|
+
setTimeout(onComplete, 2000);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const result = await installShellHook();
|
|
25
|
+
setStatus(getHookStatus());
|
|
26
|
+
setIsProcessing(false);
|
|
27
|
+
if (result) {
|
|
28
|
+
setMessage(`✅ Shell hook 已安装\n⚠️ 请重启终端或执行: source ${configPath}`);
|
|
29
|
+
}
|
|
30
|
+
setTimeout(onComplete, 3000);
|
|
31
|
+
}
|
|
32
|
+
else if (action === 'uninstall') {
|
|
33
|
+
setIsProcessing(true);
|
|
34
|
+
uninstallShellHook();
|
|
35
|
+
setStatus(getHookStatus());
|
|
36
|
+
setMessage('✅ Shell hook 已卸载\n⚠️ 请重启终端使其生效');
|
|
37
|
+
setIsProcessing(false);
|
|
38
|
+
setTimeout(onComplete, 3000);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// status
|
|
42
|
+
setTimeout(onComplete, 100);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
execute();
|
|
46
|
+
}, [action, onComplete]);
|
|
47
|
+
if (action === 'install' || action === 'uninstall') {
|
|
48
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
49
|
+
React.createElement(Text, { bold: true, color: theme.accent },
|
|
50
|
+
"\uD83D\uDD27 Shell Hook ",
|
|
51
|
+
action === 'install' ? '安装' : '卸载',
|
|
52
|
+
"\u5411\u5BFC"),
|
|
53
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
54
|
+
isProcessing && React.createElement(Text, { color: theme.info }, "\u5904\u7406\u4E2D..."),
|
|
55
|
+
message && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, message.split('\n').map((line, i) => (React.createElement(Text, { key: i, color: line.startsWith('✅')
|
|
56
|
+
? theme.success
|
|
57
|
+
: line.startsWith('⚠️')
|
|
58
|
+
? theme.warning
|
|
59
|
+
: line.startsWith('❌')
|
|
60
|
+
? theme.error
|
|
61
|
+
: theme.text.primary }, line)))))));
|
|
62
|
+
}
|
|
63
|
+
// Status display
|
|
64
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
65
|
+
React.createElement(Text, { bold: true }, "\uD83D\uDCCA Shell Hook \u72B6\u6001"),
|
|
66
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
67
|
+
React.createElement(Box, { marginTop: 1 },
|
|
68
|
+
React.createElement(Text, { color: theme.primary }, " Shell \u7C7B\u578B: "),
|
|
69
|
+
React.createElement(Text, null, status.shellType)),
|
|
70
|
+
React.createElement(Box, null,
|
|
71
|
+
React.createElement(Text, { color: theme.primary }, " \u914D\u7F6E\u6587\u4EF6: "),
|
|
72
|
+
React.createElement(Text, null, status.configPath || '未知')),
|
|
73
|
+
React.createElement(Box, null,
|
|
74
|
+
React.createElement(Text, { color: theme.primary }, " \u5DF2\u5B89\u88C5: "),
|
|
75
|
+
status.installed ? (React.createElement(Text, { color: theme.success }, "\u662F")) : (React.createElement(Text, { color: theme.text.secondary }, "\u5426"))),
|
|
76
|
+
React.createElement(Box, null,
|
|
77
|
+
React.createElement(Text, { color: theme.primary }, " \u5DF2\u542F\u7528: "),
|
|
78
|
+
status.enabled ? (React.createElement(Text, { color: theme.success }, "\u662F")) : (React.createElement(Text, { color: theme.text.secondary }, "\u5426"))),
|
|
79
|
+
React.createElement(Box, null,
|
|
80
|
+
React.createElement(Text, { color: theme.primary }, " \u5386\u53F2\u6587\u4EF6: "),
|
|
81
|
+
React.createElement(Text, null, status.historyFile)),
|
|
82
|
+
React.createElement(Text, { color: theme.text.secondary }, '━'.repeat(40)),
|
|
83
|
+
!status.installed && (React.createElement(Box, { marginTop: 1 },
|
|
84
|
+
React.createElement(Text, { color: theme.text.secondary },
|
|
85
|
+
"\u63D0\u793A: \u8FD0\u884C ",
|
|
86
|
+
React.createElement(Text, { color: theme.primary }, "pls hook install"),
|
|
87
|
+
" \u5B89\u88C5 shell hook")))));
|
|
88
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface RenderInlineProps {
|
|
3
|
+
text: string;
|
|
4
|
+
defaultColor?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 行内 Markdown 渲染器
|
|
8
|
+
* 处理 **粗体**、*斜体*、`代码`、~~删除线~~、<u>下划线</u>、链接
|
|
9
|
+
*/
|
|
10
|
+
declare function RenderInlineInternal({ text, defaultColor }: RenderInlineProps): React.JSX.Element;
|
|
11
|
+
export declare const RenderInline: React.MemoExoticComponent<typeof RenderInlineInternal>;
|
|
12
|
+
export {};
|