nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,347 @@
1
+ // ============================================================================
2
+ // Ink Prototype - Component-based UI framework evaluation
3
+ // ============================================================================
4
+
5
+ /**
6
+ * This is a prototype to evaluate Ink as a replacement for readline.
7
+ * To run this prototype:
8
+ * 1. npm install ink react
9
+ * 2. npx tsx ink-prototype.tsx
10
+ */
11
+
12
+ import React, { useState, useEffect, useCallback } from 'react';
13
+ import { render, Box, Text, useInput, useApp, Spacer, Static } from 'ink';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ interface Message {
20
+ id: string;
21
+ role: 'user' | 'assistant' | 'system';
22
+ content: string;
23
+ timestamp: number;
24
+ }
25
+
26
+ interface ModelInfo {
27
+ name: string;
28
+ provider: string;
29
+ }
30
+
31
+ // ============================================================================
32
+ // StatusBar Component
33
+ // ============================================================================
34
+
35
+ interface StatusBarProps {
36
+ model: ModelInfo;
37
+ mode: string;
38
+ contextPercent: number;
39
+ }
40
+
41
+ const StatusBar: React.FC<StatusBarProps> = ({ model, mode, contextPercent }) => {
42
+ const getModeColor = (mode: string) => {
43
+ switch (mode) {
44
+ case 'AUTO': return 'green';
45
+ case 'PLAN': return 'yellow';
46
+ case 'ASK': return 'blue';
47
+ default: return 'white';
48
+ }
49
+ };
50
+
51
+ const getContextColor = (percent: number) => {
52
+ if (percent > 80) return 'red';
53
+ if (percent > 50) return 'yellow';
54
+ return 'green';
55
+ };
56
+
57
+ return (
58
+ <Box borderStyle="round" borderColor="magenta" paddingX={1}>
59
+ <Text color="cyan">Model:</Text>
60
+ <Text color="white"> {model.name} </Text>
61
+ <Text color="dim">|</Text>
62
+ <Text color="cyan"> Mode:</Text>
63
+ <Text color={getModeColor(mode)}> {mode} </Text>
64
+ <Text color="dim">|</Text>
65
+ <Text color="cyan"> Context:</Text>
66
+ <Text color={getContextColor(contextPercent)}> {contextPercent}% </Text>
67
+ <Spacer />
68
+ </Box>
69
+ );
70
+ };
71
+
72
+ // ============================================================================
73
+ // InputBox Component
74
+ // ============================================================================
75
+
76
+ interface InputBoxProps {
77
+ onSubmit: (input: string) => void;
78
+ placeholder?: string;
79
+ }
80
+
81
+ const InputBox: React.FC<InputBoxProps> = ({ onSubmit, placeholder }) => {
82
+ const [input, setInput] = useState('');
83
+
84
+ useInput((input, key) => {
85
+ if (key.return) {
86
+ if (input.trim()) {
87
+ onSubmit(input.trim());
88
+ setInput('');
89
+ }
90
+ } else if (key.backspace || key.delete) {
91
+ setInput(prev => prev.slice(0, -1));
92
+ } else if (input) {
93
+ setInput(prev => prev + input);
94
+ }
95
+ });
96
+
97
+ return (
98
+ <Box flexDirection="column">
99
+ <Box borderStyle="single" borderColor="magenta" paddingLeft={1}>
100
+ <Text color="cyan">❯</Text>
101
+ <Text color="white"> {input}</Text>
102
+ <Text color="gray">{placeholder?.slice(input.length) || ''}</Text>
103
+ </Box>
104
+ </Box>
105
+ );
106
+ };
107
+
108
+ // ============================================================================
109
+ // MessageList Component
110
+ // ============================================================================
111
+
112
+ interface MessageListProps {
113
+ messages: Message[];
114
+ maxHeight?: number;
115
+ }
116
+
117
+ const MessageList: React.FC<MessageListProps> = ({ messages, maxHeight = 10 }) => {
118
+ const visibleMessages = messages.slice(-maxHeight);
119
+
120
+ return (
121
+ <Box flexDirection="column">
122
+ <Static items={visibleMessages}>
123
+ {(message) => {
124
+ const time = new Date(message.timestamp).toLocaleTimeString();
125
+ const getColor = (role: string) => {
126
+ switch (role) {
127
+ case 'user': return 'white';
128
+ case 'assistant': return 'green';
129
+ case 'system': return 'dim';
130
+ default: return 'white';
131
+ }
132
+ };
133
+
134
+ return (
135
+ <Box key={message.id} flexDirection="column" marginBottom={1}>
136
+ <Box>
137
+ <Text color="dim">[{time}]</Text>
138
+ <Text color="cyan"> {message.role.toUpperCase()}</Text>
139
+ </Box>
140
+ <Box marginLeft={2}>
141
+ <Text color={getColor(message.role)}>{message.content}</Text>
142
+ </Box>
143
+ </Box>
144
+ );
145
+ }}
146
+ </Static>
147
+ </Box>
148
+ );
149
+ };
150
+
151
+ // ============================================================================
152
+ // ProgressBar Component
153
+ // ============================================================================
154
+
155
+ interface ProgressBarProps {
156
+ label: string;
157
+ percent: number;
158
+ color?: string;
159
+ }
160
+
161
+ const ProgressBar: React.FC<ProgressBarProps> = ({ label, percent, color = 'green' }) => {
162
+ const width = 40;
163
+ const filled = Math.floor((percent / 100) * width);
164
+ const empty = width - filled;
165
+
166
+ return (
167
+ <Box flexDirection="column">
168
+ <Text color="cyan">{label}</Text>
169
+ <Box>
170
+ <Text color="magenta">╭</Text>
171
+ <Text color={color}>{'━'.repeat(filled)}</Text>
172
+ <Text color="dim">{'─'.repeat(empty)}</Text>
173
+ <Text color="magenta">╮</Text>
174
+ <Text color="white"> {percent.toFixed(1)}%</Text>
175
+ </Box>
176
+ </Box>
177
+ );
178
+ };
179
+
180
+ // ============================================================================
181
+ // Main App Component
182
+ // ============================================================================
183
+
184
+ interface AppProps {
185
+ onExit?: () => void;
186
+ }
187
+
188
+ const App: React.FC<AppProps> = ({ onExit }) => {
189
+ const [messages, setMessages] = useState<Message[]>([
190
+ {
191
+ id: '1',
192
+ role: 'system',
193
+ content: 'Nova CLI with Ink UI Framework',
194
+ timestamp: Date.now(),
195
+ },
196
+ {
197
+ id: '2',
198
+ role: 'assistant',
199
+ content: 'Hello! This is a prototype of Nova CLI using Ink framework.',
200
+ timestamp: Date.now() + 1000,
201
+ },
202
+ ]);
203
+
204
+ const [model] = useState<ModelInfo>({
205
+ name: 'claude-3-opus',
206
+ provider: 'Anthropic',
207
+ });
208
+
209
+ const [mode] = useState('AUTO');
210
+ const [contextPercent] = useState(23);
211
+ const [isProcessing, setIsProcessing] = useState(false);
212
+
213
+ const { exit } = useApp();
214
+
215
+ useInput((input, key) => {
216
+ if (key.escape) {
217
+ exit();
218
+ onExit?.();
219
+ }
220
+ });
221
+
222
+ const handleSubmit = useCallback((input: string) => {
223
+ if (isProcessing) return;
224
+
225
+ // Add user message
226
+ const userMessage: Message = {
227
+ id: Date.now().toString(),
228
+ role: 'user',
229
+ content: input,
230
+ timestamp: Date.now(),
231
+ };
232
+ setMessages(prev => [...prev, userMessage]);
233
+
234
+ // Simulate AI processing
235
+ setIsProcessing(true);
236
+ setTimeout(() => {
237
+ const assistantMessage: Message = {
238
+ id: (Date.now() + 1).toString(),
239
+ role: 'assistant',
240
+ content: `I received: "${input}"`,
241
+ timestamp: Date.now() + 1000,
242
+ };
243
+ setMessages(prev => [...prev, assistantMessage]);
244
+ setIsProcessing(false);
245
+ }, 1500);
246
+ }, [isProcessing]);
247
+
248
+ return (
249
+ <Box flexDirection="column" padding={1}>
250
+ {/* Header */}
251
+ <Box borderStyle="double" borderColor="magenta" paddingX={1} marginBottom={1}>
252
+ <Text bold color="magenta">NOVA CLI</Text>
253
+ <Spacer />
254
+ <Text color="dim">AI-Powered Terminal Assistant</Text>
255
+ </Box>
256
+
257
+ {/* Status Bar */}
258
+ <StatusBar model={model} mode={mode} contextPercent={contextPercent} />
259
+
260
+ {/* Messages */}
261
+ <Box marginY={1}>
262
+ <MessageList messages={messages} />
263
+ </Box>
264
+
265
+ {/* Processing Indicator */}
266
+ {isProcessing && (
267
+ <Box marginBottom={1}>
268
+ <Text color="yellow">⏳ Processing...</Text>
269
+ </Box>
270
+ )}
271
+
272
+ {/* Input */}
273
+ <InputBox
274
+ onSubmit={handleSubmit}
275
+ placeholder="Type your message and press Enter..."
276
+ />
277
+
278
+ {/* Footer */}
279
+ <Box marginTop={1}>
280
+ <Text color="dim">Press ESC to exit</Text>
281
+ </Box>
282
+ </Box>
283
+ );
284
+ };
285
+
286
+ // ============================================================================
287
+ // Render Function
288
+ // ============================================================================
289
+
290
+ export function renderInkUI(): void {
291
+ render(<App onExit={() => {
292
+ console.log('Goodbye!');
293
+ process.exit(0);
294
+ }} />);
295
+ }
296
+
297
+ // ============================================================================
298
+ // Demo - Progress Bar Showcase
299
+ // ============================================================================
300
+
301
+ export function renderProgressDemo(): void {
302
+ const DemoApp: React.FC = () => {
303
+ const [progress, setProgress] = useState(0);
304
+
305
+ useEffect(() => {
306
+ const interval = setInterval(() => {
307
+ setProgress(prev => {
308
+ const next = prev + Math.random() * 10;
309
+ return next >= 100 ? 0 : next;
310
+ });
311
+ }, 200);
312
+
313
+ return () => clearInterval(interval);
314
+ }, []);
315
+
316
+ return (
317
+ <Box flexDirection="column" padding={2}>
318
+ <Box marginBottom={1}>
319
+ <Text bold color="magenta">
320
+ ProgressBar Component Demo
321
+ </Text>
322
+ </Box>
323
+
324
+ <ProgressBar label="Processing files" percent={progress} color="green" />
325
+
326
+ <Box marginTop={1}>
327
+ <Text color="cyan">Mode:</Text>
328
+ <Text color="yellow"> AUTO </Text>
329
+ <Text color="dim">|</Text>
330
+ <Text color="cyan"> Model:</Text>
331
+ <Text color="white"> claude-3-opus </Text>
332
+ </Box>
333
+ </Box>
334
+ );
335
+ };
336
+
337
+ render(<DemoApp />);
338
+ }
339
+
340
+ // ============================================================================
341
+ // Main
342
+ // ============================================================================
343
+
344
+ if (require.main === module) {
345
+ // Run the prototype if executed directly
346
+ renderInkUI();
347
+ }
@@ -0,0 +1,336 @@
1
+ // ============================================================================
2
+ // CLI UI Helper - 统一的 CLI 界面美化工具
3
+ // ============================================================================
4
+
5
+ /**
6
+ * 颜色和样式常量
7
+ */
8
+ export const Colors = {
9
+ // 品牌色
10
+ brand: '\x1b[38;5;93m', // 紫色
11
+ brandLight: '\x1b[38;5;141m', // 浅紫色
12
+ brandDim: '\x1b[38;5;93m\x1b[2m', // 暗紫色
13
+
14
+ // 状态色
15
+ success: '\x1b[32m', // 绿色
16
+ successDim: '\x1b[32m\x1b[2m', // 暗绿色
17
+ warning: '\x1b[33m', // 黄色
18
+ warningDim: '\x1b[33m\x1b[2m', // 暗黄色
19
+ error: '\x1b[31m', // 红色
20
+ errorDim: '\x1b[31m\x1b[2m', // 暗红色
21
+ info: '\x1b[36m', // 青色
22
+ infoDim: '\x1b[36m\x1b[2m', // 暗青色
23
+
24
+ // 文本色
25
+ primary: '\x1b[1m', // 加粗白色
26
+ muted: '\x1b[90m', // 灰色
27
+ dim: '\x1b[2m', // 暗色
28
+ reset: '\x1b[0m', // 重置
29
+ };
30
+
31
+ /**
32
+ * 盒子字符
33
+ */
34
+ export const BoxChars = {
35
+ tl: '╭', tr: '╮', bl: '╰', br: '╯',
36
+ h: '─', v: '│', ht: '├', htr: '┤', cross: '┼',
37
+ hThick: '━', vThick: '┃',
38
+ arrow: '›', bullet: '•', check: '✓', crossMark: '✗', dot: '·',
39
+ diamond: '◆', star: '★', circle: '○', circleFull: '●',
40
+ spinner: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'],
41
+ arrowRight: '→', arrowLeft: '←', arrowUp: '↑', arrowDown: '↓',
42
+ };
43
+
44
+ /**
45
+ * CLI UI 辅助类
46
+ */
47
+ export class CliUI {
48
+ private static termCols = process.stdout.columns || 80;
49
+ private static maxWidth = Math.min(this.termCols - 4, 100);
50
+
51
+ /**
52
+ * 获取终端宽度,限制在 min 和 max 之间
53
+ */
54
+ static getWidth(min = 40, max = 100): number {
55
+ const cols = process.stdout.columns || 80;
56
+ return Math.max(min, Math.min(cols - 4, max));
57
+ }
58
+
59
+ /**
60
+ * 创建分割线
61
+ */
62
+ static hr(char = '─', width = this.maxWidth, color = Colors.muted): string {
63
+ return color + char.repeat(width) + Colors.reset;
64
+ }
65
+
66
+ /**
67
+ * 创建标题框
68
+ */
69
+ static createBoxHeader(title: string, subtext?: string): string[] {
70
+ const width = this.getWidth();
71
+ const hr = this.hr(BoxChars.h, width, Colors.brandDim);
72
+ const hrThick = this.hr(BoxChars.hThick, width, Colors.brand);
73
+
74
+ const lines: string[] = ['', Colors.brand + BoxChars.tl + hrThick + BoxChars.tr + Colors.reset];
75
+
76
+ // 标题行
77
+ if (title) {
78
+ const titleText = ` ${Colors.brand}${BoxChars.diamond} ${Colors.primary}${title}${Colors.reset}`;
79
+ const padding = ' '.repeat(Math.max(0, width - title.length - 6));
80
+ lines.push(Colors.brand + BoxChars.v + Colors.reset + titleText + padding + Colors.brand + BoxChars.v + Colors.reset);
81
+ }
82
+
83
+ // 副标题
84
+ if (subtext) {
85
+ const subText = ` ${Colors.muted}${subtext}${Colors.reset}`;
86
+ const padding = ' '.repeat(Math.max(0, width - subtext.length + 6));
87
+ lines.push(Colors.brand + BoxChars.v + Colors.reset + subText + padding + Colors.brand + BoxChars.v + Colors.reset);
88
+ }
89
+
90
+ lines.push(Colors.brand + BoxChars.bl + hrThick + BoxChars.br + Colors.reset, '');
91
+ return lines;
92
+ }
93
+
94
+ /**
95
+ * 创建章节标题
96
+ */
97
+ static createSection(title: string): string {
98
+ const width = this.getWidth();
99
+ const padding = ' '.repeat(Math.max(0, width - title.length - 6));
100
+ return `\n${Colors.brand}${BoxChars.diamond}${Colors.reset} ${Colors.primary}${title}${Colors.reset}${padding}`;
101
+ }
102
+
103
+ /**
104
+ * 创建命令行
105
+ */
106
+ static createCmd(name: string, desc: string, nameWidth = 24): string {
107
+ return ` ${Colors.info}${name.padEnd(nameWidth)}${Colors.reset} ${Colors.muted}${desc}${Colors.reset}`;
108
+ }
109
+
110
+ /**
111
+ * 打印成功消息
112
+ */
113
+ static success(message: string, icon = BoxChars.check): void {
114
+ console.log(`${Colors.success} ${icon} ${message}${Colors.reset}`);
115
+ }
116
+
117
+ /**
118
+ * 打印错误消息
119
+ */
120
+ static error(message: string, icon = BoxChars.cross): void {
121
+ console.error(`${Colors.error} ${icon} ${message}${Colors.reset}`);
122
+ }
123
+
124
+ /**
125
+ * 打印警告消息
126
+ */
127
+ static warning(message: string, icon = BoxChars.diamond): void {
128
+ console.log(`${Colors.warning} ${icon} ${message}${Colors.reset}`);
129
+ }
130
+
131
+ /**
132
+ * 打印信息消息
133
+ */
134
+ static info(message: string, icon = BoxChars.circle): void {
135
+ console.log(`${Colors.info} ${icon} ${message}${Colors.reset}`);
136
+ }
137
+
138
+ /**
139
+ * 打印带框的成功消息
140
+ */
141
+ static successBox(message: string): void {
142
+ const width = this.getWidth();
143
+ const lines = [
144
+ Colors.brand + BoxChars.tl + this.hr(BoxChars.hThick, width, Colors.success) + BoxChars.tr + Colors.reset,
145
+ Colors.success + BoxChars.v + Colors.reset + ' ' + message + ' '.repeat(Math.max(0, width - message.length - 4)) + Colors.success + BoxChars.v + Colors.reset,
146
+ Colors.success + BoxChars.bl + this.hr(BoxChars.hThick, width, Colors.success) + BoxChars.br + Colors.reset,
147
+ ];
148
+ console.log(lines.join('\n'));
149
+ }
150
+
151
+ /**
152
+ * 打印带框的错误消息
153
+ */
154
+ static errorBox(message: string): void {
155
+ const width = this.getWidth();
156
+ const lines = [
157
+ Colors.brand + BoxChars.tl + this.hr(BoxChars.hThick, width, Colors.error) + BoxChars.tr + Colors.reset,
158
+ Colors.error + BoxChars.v + Colors.reset + ' ' + message + ' '.repeat(Math.max(0, width - message.length - 4)) + Colors.error + BoxChars.v + Colors.reset,
159
+ Colors.error + BoxChars.bl + this.hr(BoxChars.hThick, width, Colors.error) + BoxChars.br + Colors.reset,
160
+ ];
161
+ console.error(lines.join('\n'));
162
+ }
163
+
164
+ /**
165
+ * 打印使用说明
166
+ */
167
+ static printUsage(command: string, description: string, examples?: string[]): void {
168
+ const lines = this.createBoxHeader('Usage');
169
+
170
+ console.log(`${Colors.primary}${command}${Colors.reset} - ${Colors.muted}${description}${Colors.reset}`);
171
+
172
+ if (examples && examples.length > 0) {
173
+ console.log('');
174
+ console.log(this.createSection('Examples'));
175
+ examples.forEach((ex) => {
176
+ console.log(` ${Colors.muted}${ex}${Colors.reset}`);
177
+ });
178
+ }
179
+
180
+ console.log('');
181
+ }
182
+
183
+ /**
184
+ * 打印加载状态
185
+ */
186
+ static loading(message: string): () => void {
187
+ let frames = 0;
188
+ const spinner = BoxChars.spinner;
189
+ const interval = setInterval(() => {
190
+ process.stdout.write(`\r${Colors.info}${spinner[frames % spinner.length]}${Colors.reset} ${message}`);
191
+ frames++;
192
+ }, 80);
193
+
194
+ return () => {
195
+ clearInterval(interval);
196
+ process.stdout.write('\r' + ' '.repeat(message.length + 2) + '\r');
197
+ };
198
+ }
199
+
200
+ /**
201
+ * 打印列表
202
+ */
203
+ static printList(items: Array<{ label: string; value: string; description?: string }>): void {
204
+ const width = this.getWidth();
205
+ items.forEach((item) => {
206
+ const label = `${Colors.primary}${item.label}:${Colors.reset}`;
207
+ const value = `${Colors.info}${item.value}${Colors.reset}`;
208
+ const desc = item.description ? ` ${Colors.dim}(${item.description})${Colors.reset}` : '';
209
+ const padding = ' '.repeat(Math.max(0, width - label.length - value.length - desc.length - 8));
210
+ console.log(` ${label} ${value}${desc}${padding}`);
211
+ });
212
+ }
213
+
214
+ /**
215
+ * 创建表格
216
+ */
217
+ static createTable(headers: string[], rows: string[][]): string[] {
218
+ const colWidths = headers.map((h, i) => {
219
+ const maxWidth = Math.max(h.length, ...rows.map(r => (r[i] || '').length));
220
+ return maxWidth + 4;
221
+ });
222
+
223
+ const headerRow = headers.map((h, i) => `${Colors.primary}${h.padEnd(colWidths[i])}${Colors.reset}`).join('');
224
+ const separator = colWidths.map(w => Colors.dim + BoxChars.h.repeat(w - 1) + BoxChars.ht).join('') + BoxChars.h;
225
+
226
+ const result = ['', headerRow, separator];
227
+
228
+ rows.forEach(row => {
229
+ const cells = row.map((cell, i) => `${Colors.reset}${cell.padEnd(colWidths[i])}`).join('');
230
+ result.push(cells);
231
+ });
232
+
233
+ result.push('');
234
+ return result;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 错误处理器
240
+ */
241
+ export class ErrorHandler {
242
+ /**
243
+ * 显示友好的错误信息
244
+ */
245
+ static showError(error: Error | string, context?: string): void {
246
+ const message = error instanceof Error ? error.message : error;
247
+
248
+ CliUI.errorBox('Error occurred');
249
+ console.log('');
250
+ console.log(`${Colors.error} Message:${Colors.reset} ${message}`);
251
+
252
+ if (context) {
253
+ console.log(`${Colors.dim} Context:${Colors.reset} ${context}`);
254
+ }
255
+
256
+ console.log('');
257
+ console.log(`${Colors.muted} Tip: Use ${Colors.info}--help${Colors.reset} to see usage information`);
258
+ }
259
+
260
+ /**
261
+ * 显示使用错误
262
+ */
263
+ static showUsageError(command: string, error: string, correctUsage: string): void {
264
+ console.error('');
265
+ CliUI.error('Usage Error', BoxChars.cross);
266
+ console.error('');
267
+ console.error(`${Colors.muted} ${error}${Colors.reset}`);
268
+ console.error('');
269
+ console.error(`${Colors.info} Correct usage:${Colors.reset}`);
270
+ console.error(`${Colors.muted} ${correctUsage}${Colors.reset}`);
271
+ console.error('');
272
+ }
273
+
274
+ /**
275
+ * 显示警告
276
+ */
277
+ static showWarning(message: string, suggestion?: string): void {
278
+ console.warn('');
279
+ CliUI.warning(message, BoxChars.diamond);
280
+ if (suggestion) {
281
+ console.warn(` ${Colors.dim}${suggestion}${Colors.reset}`);
282
+ }
283
+ console.warn('');
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 进度指示器
289
+ */
290
+ export class ProgressIndicator {
291
+ private static current = 0;
292
+ private static total = 0;
293
+ private static message = '';
294
+
295
+ /**
296
+ * 开始进度
297
+ */
298
+ static start(message: string, total: number): void {
299
+ this.total = total;
300
+ this.current = 0;
301
+ this.message = message;
302
+ this.update();
303
+ }
304
+
305
+ /**
306
+ * 更新进度
307
+ */
308
+ static update(increment = 1): void {
309
+ this.current = Math.min(this.current + increment, this.total);
310
+ this.render();
311
+ }
312
+
313
+ /**
314
+ * 完成
315
+ */
316
+ static complete(): void {
317
+ this.current = this.total;
318
+ this.render();
319
+ process.stdout.write('\n');
320
+ }
321
+
322
+ /**
323
+ * 渲染进度条
324
+ */
325
+ private static render(): void {
326
+ const width = CliUI.getWidth(20, 50);
327
+ const percentage = Math.round((this.current / this.total) * 100);
328
+ const filled = Math.round((this.current / this.total) * width);
329
+ const empty = width - filled;
330
+
331
+ const bar = Colors.success + BoxChars.hThick.repeat(filled) + Colors.dim + BoxChars.hThick.repeat(empty) + Colors.reset;
332
+ const text = `${Colors.info}${this.message}${Colors.reset} ${bar} ${percentage}%`;
333
+
334
+ process.stdout.write(`\r${text}`);
335
+ }
336
+ }