handycode 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {handycode-2.2.0 → handycode-2.3.0}/PKG-INFO +1 -1
- {handycode-2.2.0 → handycode-2.3.0}/handycode/__init__.py +1 -1
- {handycode-2.2.0 → handycode-2.3.0}/handycode/assistant.py +230 -35
- handycode-2.3.0/handycode/file_manager.py +228 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/utils.py +53 -28
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/PKG-INFO +1 -1
- {handycode-2.2.0 → handycode-2.3.0}/setup.py +1 -1
- handycode-2.2.0/handycode/file_manager.py +0 -238
- {handycode-2.2.0 → handycode-2.3.0}/LICENSE +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/README.md +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/__main__.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/cli.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/config.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/logo.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/main.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/models.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/project_templates.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode/security.py +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/SOURCES.txt +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/dependency_links.txt +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/entry_points.txt +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/requires.txt +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/top_level.txt +0 -0
- {handycode-2.2.0 → handycode-2.3.0}/setup.cfg +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Основной класс ассистента HandyCode
|
|
2
|
+
Основной класс ассистента HandyCode
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import os
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import atexit
|
|
10
|
+
import signal
|
|
6
11
|
from pathlib import Path
|
|
7
12
|
from typing import List, Dict, Optional
|
|
8
13
|
from datetime import datetime
|
|
@@ -18,7 +23,9 @@ try:
|
|
|
18
23
|
HAS_REQUESTS = True
|
|
19
24
|
except ImportError:
|
|
20
25
|
HAS_REQUESTS = False
|
|
21
|
-
import urllib.request
|
|
26
|
+
import urllib.request
|
|
27
|
+
import urllib.error
|
|
28
|
+
import ssl
|
|
22
29
|
|
|
23
30
|
from handycode.config import Config
|
|
24
31
|
from handycode.models import MODELS, get_model_settings
|
|
@@ -26,10 +33,95 @@ from handycode.file_manager import FileManager
|
|
|
26
33
|
from handycode.security import SecurityChecker
|
|
27
34
|
from handycode.utils import (
|
|
28
35
|
Colors, Theme, colorize, print_colored, print_header, print_success,
|
|
29
|
-
print_error, print_warning, print_info, print_logo,
|
|
30
|
-
print_divider, print_file_action, print_status, print_section, print_box
|
|
36
|
+
print_error, print_warning, print_info, print_logo,
|
|
37
|
+
print_divider, print_file_action, print_status, print_section, print_box,
|
|
38
|
+
Spinner, print_package_status
|
|
31
39
|
)
|
|
32
40
|
|
|
41
|
+
|
|
42
|
+
def interactive_confirm(commands):
|
|
43
|
+
"""Интерактивное меню выбора команд"""
|
|
44
|
+
if not commands:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
if os.name == 'nt':
|
|
48
|
+
import msvcrt
|
|
49
|
+
def get_key():
|
|
50
|
+
key = msvcrt.getch()
|
|
51
|
+
if key == b'\xe0':
|
|
52
|
+
key = msvcrt.getch()
|
|
53
|
+
if key == b'H': return 'up'
|
|
54
|
+
if key == b'P': return 'down'
|
|
55
|
+
if key == b'\r': return 'enter'
|
|
56
|
+
if key == b' ': return 'space'
|
|
57
|
+
if key in [b'a', b'A']: return 'a'
|
|
58
|
+
if key in [b's', b'S']: return 's'
|
|
59
|
+
if key in [b'c', b'C']: return 'c'
|
|
60
|
+
if key == b'\x1b': return 'escape'
|
|
61
|
+
return key.decode('utf-8', errors='ignore')
|
|
62
|
+
else:
|
|
63
|
+
import tty, termios
|
|
64
|
+
def get_key():
|
|
65
|
+
fd = sys.stdin.fileno()
|
|
66
|
+
old = termios.tcgetattr(fd)
|
|
67
|
+
try:
|
|
68
|
+
tty.setraw(fd)
|
|
69
|
+
key = sys.stdin.read(1)
|
|
70
|
+
if key == '\x1b':
|
|
71
|
+
key += sys.stdin.read(2)
|
|
72
|
+
if key == '\x1b[A': return 'up'
|
|
73
|
+
if key == '\x1b[B': return 'down'
|
|
74
|
+
return 'escape'
|
|
75
|
+
if key == '\r': return 'enter'
|
|
76
|
+
if key == ' ': return 'space'
|
|
77
|
+
return key
|
|
78
|
+
finally:
|
|
79
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
80
|
+
|
|
81
|
+
selected = [True] * len(commands)
|
|
82
|
+
current = 0
|
|
83
|
+
|
|
84
|
+
def render():
|
|
85
|
+
print(f"\033[{len(commands) + 4}A\033[J", end="")
|
|
86
|
+
print()
|
|
87
|
+
print(colorize(" ⚡ Команды для выполнения:", Theme.HIGHLIGHT + Colors.BOLD))
|
|
88
|
+
print(colorize(" ─────────────────────────────────────────────────", Theme.MUTED))
|
|
89
|
+
for i, cmd in enumerate(commands):
|
|
90
|
+
prefix = colorize(" ›", Theme.PRIMARY + Colors.BOLD) if i == current else " "
|
|
91
|
+
checkbox = colorize("◉", Theme.SUCCESS) if selected[i] else colorize("○", Theme.MUTED)
|
|
92
|
+
cmd_color = Theme.SUCCESS if selected[i] else Theme.MUTED
|
|
93
|
+
print(f"{prefix} {checkbox} {colorize(cmd, cmd_color)}")
|
|
94
|
+
print()
|
|
95
|
+
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить C Отмена", Theme.MUTED))
|
|
96
|
+
|
|
97
|
+
for _ in range(len(commands) + 5):
|
|
98
|
+
print()
|
|
99
|
+
render()
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
key = get_key()
|
|
103
|
+
if key == 'up':
|
|
104
|
+
current = (current - 1) % len(commands)
|
|
105
|
+
render()
|
|
106
|
+
elif key == 'down':
|
|
107
|
+
current = (current + 1) % len(commands)
|
|
108
|
+
render()
|
|
109
|
+
elif key == 'space':
|
|
110
|
+
selected[current] = not selected[current]
|
|
111
|
+
render()
|
|
112
|
+
elif key in ['a', 'A']:
|
|
113
|
+
selected = [True] * len(commands)
|
|
114
|
+
render()
|
|
115
|
+
elif key in ['s', 'S']:
|
|
116
|
+
selected = [False] * len(commands)
|
|
117
|
+
render()
|
|
118
|
+
elif key in ['c', 'C', 'escape']:
|
|
119
|
+
return []
|
|
120
|
+
elif key == 'enter':
|
|
121
|
+
print()
|
|
122
|
+
return [cmd for cmd, sel in zip(commands, selected) if sel]
|
|
123
|
+
|
|
124
|
+
|
|
33
125
|
class HandyCode:
|
|
34
126
|
def __init__(self, project_path, model="deepseek", auto_approve=False, config=None):
|
|
35
127
|
self.project_path = project_path
|
|
@@ -43,7 +135,12 @@ class HandyCode:
|
|
|
43
135
|
self.model_settings = get_model_settings(self.current_model)
|
|
44
136
|
self.file_manager = FileManager(self.project_path)
|
|
45
137
|
self.security = SecurityChecker(self.project_path)
|
|
138
|
+
|
|
139
|
+
# Получаем список установленных пакетов
|
|
140
|
+
self.installed_packages = self.file_manager.get_installed_packages()
|
|
141
|
+
|
|
46
142
|
project_context = self._build_project_context()
|
|
143
|
+
|
|
47
144
|
self.conversation_history = [
|
|
48
145
|
{"role": "system", "content": self._get_system_prompt() + project_context}
|
|
49
146
|
]
|
|
@@ -54,12 +151,21 @@ class HandyCode:
|
|
|
54
151
|
}
|
|
55
152
|
self.stream_buffer = ""
|
|
56
153
|
self.pending_commands = []
|
|
154
|
+
self.command_results = [] # Результаты выполнения команд
|
|
57
155
|
self._setup_readline()
|
|
58
156
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
59
157
|
self._interrupt_count = 0
|
|
60
158
|
|
|
61
159
|
def _build_project_context(self):
|
|
62
160
|
context = f"\n\n=== CURRENT PROJECT ===\nDirectory: {self.project_path}\n"
|
|
161
|
+
context += f"\n=== INSTALLED PACKAGES ===\n"
|
|
162
|
+
if self.installed_packages:
|
|
163
|
+
context += ", ".join(self.installed_packages[:50])
|
|
164
|
+
if len(self.installed_packages) > 50:
|
|
165
|
+
context += f"\n... and {len(self.installed_packages) - 50} more"
|
|
166
|
+
else:
|
|
167
|
+
context += "No packages detected"
|
|
168
|
+
|
|
63
169
|
try:
|
|
64
170
|
all_files = []
|
|
65
171
|
for ext in self.file_manager.allowed_extensions:
|
|
@@ -74,13 +180,26 @@ class HandyCode:
|
|
|
74
180
|
if not any(rel.startswith(ex) for ex in self.file_manager.excluded_dirs):
|
|
75
181
|
files.append(f)
|
|
76
182
|
seen.add(f)
|
|
77
|
-
context += f"\
|
|
183
|
+
context += f"\n\n=== PROJECT FILES ({len(files)}) ===\n"
|
|
78
184
|
for file in files:
|
|
79
185
|
try:
|
|
80
186
|
rel_path = file.relative_to(self.project_path)
|
|
81
187
|
size = file.stat().st_size
|
|
82
188
|
context += f" {rel_path} ({self._format_size(size)})\n"
|
|
83
189
|
except: pass
|
|
190
|
+
|
|
191
|
+
context += f"\n=== FILE CONTENTS ===\n"
|
|
192
|
+
total = 0
|
|
193
|
+
for file in files:
|
|
194
|
+
if total > 50000: break
|
|
195
|
+
try:
|
|
196
|
+
content = file.read_text(encoding='utf-8', errors='ignore')
|
|
197
|
+
if len(content) > 3000:
|
|
198
|
+
content = content[:3000] + "\n... (truncated)"
|
|
199
|
+
rel_path = file.relative_to(self.project_path)
|
|
200
|
+
context += f"\n=== {rel_path} ===\n{content}\n"
|
|
201
|
+
total += len(content)
|
|
202
|
+
except: pass
|
|
84
203
|
except: pass
|
|
85
204
|
return context
|
|
86
205
|
|
|
@@ -91,22 +210,50 @@ class HandyCode:
|
|
|
91
210
|
return f"{size:.1f}TB"
|
|
92
211
|
|
|
93
212
|
def _get_system_prompt(self):
|
|
94
|
-
return """You are HandyCode - AI coding assistant.
|
|
95
|
-
|
|
96
|
-
|
|
213
|
+
return """You are HandyCode - AI coding assistant.
|
|
214
|
+
|
|
215
|
+
CAPABILITIES:
|
|
216
|
+
- Create, modify, delete files
|
|
217
|
+
- Run shell commands
|
|
218
|
+
- Install Python packages via pip
|
|
219
|
+
- Analyze code and errors
|
|
220
|
+
- See installed packages and project files
|
|
221
|
+
|
|
222
|
+
PACKAGE MANAGEMENT:
|
|
223
|
+
- Check INSTALLED PACKAGES section before suggesting imports
|
|
224
|
+
- If a package is needed but not installed, use [[INSTALL:package_name]]
|
|
225
|
+
- Example: [[INSTALL:fastapi]] [[INSTALL:uvicorn]]
|
|
226
|
+
|
|
227
|
+
FILE FORMAT:
|
|
228
|
+
[[CREATE:path/file.py]]
|
|
97
229
|
code here
|
|
98
230
|
[[END]]
|
|
99
|
-
|
|
231
|
+
|
|
232
|
+
[[MODIFY:path/file.py]]
|
|
100
233
|
new code here
|
|
101
234
|
[[END]]
|
|
235
|
+
|
|
236
|
+
[[DELETE:path/file.py]]
|
|
237
|
+
[[READ:path/file.py]]
|
|
238
|
+
[[LIST:directory/]]
|
|
102
239
|
[[EXEC:command]]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
240
|
+
[[INSTALL:package_name]]
|
|
241
|
+
|
|
242
|
+
CRITICAL RULES:
|
|
243
|
+
1. ALWAYS check if required packages are installed before using them
|
|
244
|
+
2. Install missing packages with [[INSTALL:...]]
|
|
245
|
+
3. CREATE files + INSTALL packages + EXEC commands in ONE response
|
|
246
|
+
4. Use [[END]] to close file blocks
|
|
247
|
+
5. NO comments or explanations inside [[CREATE]]...[[END]] - ONLY CODE
|
|
248
|
+
6. Put ALL explanations BEFORE [[CREATE]] blocks
|
|
249
|
+
7. After EXEC, I will show you any errors
|
|
250
|
+
|
|
251
|
+
ERROR HANDLING:
|
|
252
|
+
- After running commands, I'll show you the output
|
|
253
|
+
- If there are errors, I'll show them to you
|
|
254
|
+
- You can then fix the files and re-run
|
|
255
|
+
|
|
256
|
+
Speak Russian. Write code in English. Code ONLY inside [[CREATE]]...[[END]]."""
|
|
110
257
|
|
|
111
258
|
def _setup_readline(self):
|
|
112
259
|
if not HAS_READLINE: return
|
|
@@ -127,9 +274,10 @@ Speak Russian. Write code in English."""
|
|
|
127
274
|
|
|
128
275
|
def reset_interrupt(self): self._interrupt_count = 0
|
|
129
276
|
|
|
130
|
-
# Потоковая обработка
|
|
131
277
|
def _process_stream_chunk(self, chunk):
|
|
132
278
|
self.stream_buffer += chunk
|
|
279
|
+
|
|
280
|
+
# CREATE с анимацией
|
|
133
281
|
while True:
|
|
134
282
|
match = re.search(r'\[\[CREATE:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
135
283
|
if match:
|
|
@@ -138,11 +286,15 @@ Speak Russian. Write code in English."""
|
|
|
138
286
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
139
287
|
content = re.sub(r'\n```$', '', content)
|
|
140
288
|
if content and self.security.is_safe_path(path):
|
|
289
|
+
spinner = Spinner(f"Создание {path}")
|
|
290
|
+
spinner.start()
|
|
141
291
|
self.file_manager.create_file(path, content)
|
|
292
|
+
spinner.stop(f" ✔ {path} ({content.count(chr(10))+1} строк)")
|
|
142
293
|
self.stats["files_created"].append(path)
|
|
143
|
-
print_file_action('create', path, f"({content.count(chr(10))+1} lines)")
|
|
144
294
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
145
295
|
else: break
|
|
296
|
+
|
|
297
|
+
# MODIFY
|
|
146
298
|
while True:
|
|
147
299
|
match = re.search(r'\[\[MODIFY:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
148
300
|
if match:
|
|
@@ -151,11 +303,25 @@ Speak Russian. Write code in English."""
|
|
|
151
303
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
152
304
|
content = re.sub(r'\n```$', '', content)
|
|
153
305
|
if content and self.security.is_safe_path(path):
|
|
306
|
+
spinner = Spinner(f"Изменение {path}")
|
|
307
|
+
spinner.start()
|
|
154
308
|
self.file_manager.modify_file(path, content)
|
|
309
|
+
spinner.stop(f" ✎ {path} ({content.count(chr(10))+1} строк)")
|
|
155
310
|
self.stats["files_modified"].append(path)
|
|
156
|
-
print_file_action('modify', path, f"({content.count(chr(10))+1} lines)")
|
|
157
311
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
158
312
|
else: break
|
|
313
|
+
|
|
314
|
+
# INSTALL
|
|
315
|
+
while True:
|
|
316
|
+
match = re.search(r'\[\[INSTALL:(.+?)\]\]', self.stream_buffer)
|
|
317
|
+
if match:
|
|
318
|
+
package = match.group(1).strip()
|
|
319
|
+
print_status(f"Установка пакета: {package}")
|
|
320
|
+
self.file_manager.install_package(package)
|
|
321
|
+
self.stream_buffer = self.stream_buffer[match.end():]
|
|
322
|
+
else: break
|
|
323
|
+
|
|
324
|
+
# EXEC
|
|
159
325
|
while True:
|
|
160
326
|
match = re.search(r'\[\[EXEC:(.+?)\]\]', self.stream_buffer)
|
|
161
327
|
if match:
|
|
@@ -198,7 +364,7 @@ Speak Russian. Write code in English."""
|
|
|
198
364
|
if '[[END]]' in content:
|
|
199
365
|
in_code = False
|
|
200
366
|
if not in_code:
|
|
201
|
-
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
367
|
+
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace('[[INSTALL:', '').replace(']]', '')
|
|
202
368
|
if clean.strip():
|
|
203
369
|
print(clean, end="", flush=True)
|
|
204
370
|
self._process_stream_chunk(content)
|
|
@@ -236,7 +402,7 @@ Speak Russian. Write code in English."""
|
|
|
236
402
|
if '[[END]]' in content:
|
|
237
403
|
in_code = False
|
|
238
404
|
if not in_code:
|
|
239
|
-
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
|
|
405
|
+
clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace('[[INSTALL:', '').replace(']]', '')
|
|
240
406
|
if clean.strip():
|
|
241
407
|
print(clean, end="", flush=True)
|
|
242
408
|
self._process_stream_chunk(content)
|
|
@@ -263,7 +429,6 @@ Speak Russian. Write code in English."""
|
|
|
263
429
|
}
|
|
264
430
|
|
|
265
431
|
try:
|
|
266
|
-
# Заголовок ответа
|
|
267
432
|
print_divider("─", 60, Theme.MUTED)
|
|
268
433
|
print(colorize(" HandyCode", Theme.PRIMARY + Colors.BOLD), end="")
|
|
269
434
|
print(colorize(" ● ответ", Theme.MUTED))
|
|
@@ -274,22 +439,41 @@ Speak Russian. Write code in English."""
|
|
|
274
439
|
if response:
|
|
275
440
|
self.conversation_history.append({"role": "assistant", "content": response})
|
|
276
441
|
|
|
277
|
-
# Команды
|
|
278
442
|
if self.pending_commands:
|
|
279
|
-
print()
|
|
280
|
-
print_section("⚡ Команды (требуют подтверждения)",
|
|
281
|
-
[colorize(cmd, Theme.WARNING) for cmd in self.pending_commands])
|
|
282
443
|
if self.auto_approve:
|
|
283
|
-
|
|
444
|
+
selected_commands = self.pending_commands
|
|
284
445
|
else:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
|
|
446
|
+
selected_commands = interactive_confirm(self.pending_commands)
|
|
447
|
+
|
|
448
|
+
if selected_commands:
|
|
449
|
+
print()
|
|
450
|
+
print_section("⚡ Выполнение команд", [])
|
|
451
|
+
for cmd in selected_commands:
|
|
289
452
|
if self.security.is_safe_command(cmd):
|
|
290
453
|
print_status(f"Выполняется: {cmd}")
|
|
291
|
-
self.file_manager.execute_command(cmd)
|
|
454
|
+
success, output = self.file_manager.execute_command(cmd)
|
|
292
455
|
self.stats["commands_executed"].append(cmd)
|
|
456
|
+
self.command_results.append({
|
|
457
|
+
"command": cmd,
|
|
458
|
+
"success": success,
|
|
459
|
+
"output": output
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
# Показываем ошибки
|
|
463
|
+
errors = [r for r in self.command_results if not r['success']]
|
|
464
|
+
if errors:
|
|
465
|
+
print()
|
|
466
|
+
print_section("❌ Обнаружены ошибки", [])
|
|
467
|
+
for err in errors:
|
|
468
|
+
print(colorize(f" Команда: {err['command']}", Theme.ERROR))
|
|
469
|
+
if err['output']:
|
|
470
|
+
for line in err['output'].strip().split('\n')[:5]:
|
|
471
|
+
print(colorize(f" {line}", Theme.MUTED))
|
|
472
|
+
print()
|
|
473
|
+
print_info("Вы можете попросить меня исправить ошибки")
|
|
474
|
+
else:
|
|
475
|
+
print_warning("Команды пропущены")
|
|
476
|
+
|
|
293
477
|
self.stats["messages_sent"] += 1
|
|
294
478
|
return response
|
|
295
479
|
except Exception as e:
|
|
@@ -302,6 +486,7 @@ Speak Russian. Write code in English."""
|
|
|
302
486
|
print_box([
|
|
303
487
|
"/help Справка",
|
|
304
488
|
"/scan Показать проект",
|
|
489
|
+
"/packages Показать установленные пакеты",
|
|
305
490
|
"/models Модели",
|
|
306
491
|
"/model N Сменить модель",
|
|
307
492
|
"/clear Очистить историю",
|
|
@@ -310,7 +495,12 @@ Speak Russian. Write code in English."""
|
|
|
310
495
|
"/exit Выход"
|
|
311
496
|
], Theme.PRIMARY)
|
|
312
497
|
elif cmd in ['/scan', '/s']:
|
|
313
|
-
|
|
498
|
+
print(self.file_manager.scan_project())
|
|
499
|
+
elif cmd in ['/packages', '/pkg']:
|
|
500
|
+
packages = self.file_manager.get_installed_packages()
|
|
501
|
+
print_section("📦 Установленные пакеты", packages[:20])
|
|
502
|
+
if len(packages) > 20:
|
|
503
|
+
print(colorize(f" ... и ещё {len(packages) - 20}", Theme.MUTED))
|
|
314
504
|
elif cmd in ['/models', '/m']:
|
|
315
505
|
lines = []
|
|
316
506
|
for name, mid in MODELS.items():
|
|
@@ -325,14 +515,18 @@ Speak Russian. Write code in English."""
|
|
|
325
515
|
print_success(f"Модель изменена на {name}")
|
|
326
516
|
elif cmd in ['/clear', '/c']:
|
|
327
517
|
self.conversation_history = [self.conversation_history[0]]
|
|
518
|
+
self.command_results = []
|
|
328
519
|
print_success("История очищена")
|
|
520
|
+
elif cmd in ['/save']:
|
|
521
|
+
self.file_manager.save_session(self.conversation_history, self.current_model, self.stats)
|
|
329
522
|
elif cmd in ['/stats']:
|
|
330
523
|
print_box([
|
|
331
524
|
f"Сообщений: {self.stats['messages_sent']}",
|
|
332
525
|
f"Создано файлов: {len(self.stats['files_created'])}",
|
|
333
526
|
f"Изменено: {len(self.stats['files_modified'])}",
|
|
334
527
|
f"Удалено: {len(self.stats['files_deleted'])}",
|
|
335
|
-
f"Команд выполнено: {len(self.stats['commands_executed'])}"
|
|
528
|
+
f"Команд выполнено: {len(self.stats['commands_executed'])}",
|
|
529
|
+
f"Пакетов установлено: {len(self.installed_packages)}"
|
|
336
530
|
], Theme.SECONDARY)
|
|
337
531
|
elif cmd in ['/exit', '/q']:
|
|
338
532
|
print_success("До свидания!")
|
|
@@ -347,6 +541,7 @@ Speak Russian. Write code in English."""
|
|
|
347
541
|
print_divider("─", 60, Theme.MUTED)
|
|
348
542
|
print(colorize(f" 📁 Проект: {self.project_path}", Theme.TEXT))
|
|
349
543
|
print(colorize(f" 🤖 Модель: {self.current_model}", Theme.TEXT))
|
|
544
|
+
print(colorize(f" 📦 Пакетов: {len(self.installed_packages)}", Theme.TEXT))
|
|
350
545
|
print(colorize(f" {Theme.MUTED}/help для команд{Colors.RESET}", Theme.MUTED))
|
|
351
546
|
print_divider("─", 60, Theme.MUTED)
|
|
352
547
|
print()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Управление файлами для HandyCode
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from handycode.utils import (
|
|
13
|
+
print_success, print_error, print_warning, print_info,
|
|
14
|
+
print_status, print_package_status, print_command_result, Spinner
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileManager:
|
|
19
|
+
def __init__(self, project_root: Path):
|
|
20
|
+
self.project_root = Path(project_root).resolve()
|
|
21
|
+
self.allowed_extensions = {
|
|
22
|
+
'.html', '.css', '.scss', '.sass', '.less',
|
|
23
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
24
|
+
'.vue', '.svelte', '.astro',
|
|
25
|
+
'.py', '.pyi', '.pyx', '.pxd',
|
|
26
|
+
'.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
|
|
27
|
+
'.xml', '.env', '.gitignore', '.dockerignore',
|
|
28
|
+
'.md', '.mdx', '.rst', '.txt', '.log',
|
|
29
|
+
'.sql', '.sh', '.bash', '.zsh', '.bat', '.ps1',
|
|
30
|
+
'.java', '.kt', '.scala',
|
|
31
|
+
'.cpp', '.c', '.h', '.hpp', '.cs',
|
|
32
|
+
'.rs', '.go', '.rb', '.php', '.swift',
|
|
33
|
+
'.dart', '.r', '.jl', '.lua',
|
|
34
|
+
'.dockerfile', '.makefile', '.cmake',
|
|
35
|
+
}
|
|
36
|
+
self.excluded_dirs = {
|
|
37
|
+
'node_modules', '__pycache__', '.git', '.svn',
|
|
38
|
+
'venv', '.venv', 'env', '.env',
|
|
39
|
+
'dist', 'build', '.next', '.nuxt',
|
|
40
|
+
'target', 'out', '.idea', '.vscode',
|
|
41
|
+
'.DS_Store', 'Thumbs.db',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def scan_project(self) -> str:
|
|
45
|
+
if not self.project_root.exists():
|
|
46
|
+
return ""
|
|
47
|
+
try:
|
|
48
|
+
lines = [f"📁 Проект: {self.project_root.name}"]
|
|
49
|
+
all_files = []
|
|
50
|
+
for ext in self.allowed_extensions:
|
|
51
|
+
all_files.extend(self.project_root.rglob(f"*{ext}"))
|
|
52
|
+
all_files.extend(self.project_root.rglob("*"))
|
|
53
|
+
seen = set()
|
|
54
|
+
files = []
|
|
55
|
+
for f in sorted(all_files):
|
|
56
|
+
if f.is_file() and f not in seen:
|
|
57
|
+
rel = str(f.relative_to(self.project_root))
|
|
58
|
+
if not any(ex in f.parts for ex in self.excluded_dirs):
|
|
59
|
+
if not any(rel.startswith(ex) for ex in self.excluded_dirs):
|
|
60
|
+
files.append(f)
|
|
61
|
+
seen.add(f)
|
|
62
|
+
lines.append(f"📄 Файлов: {len(files)}")
|
|
63
|
+
if files:
|
|
64
|
+
lines.append("\n📂 Структура:")
|
|
65
|
+
for file in files:
|
|
66
|
+
try:
|
|
67
|
+
rel_path = file.relative_to(self.project_root)
|
|
68
|
+
size = file.stat().st_size
|
|
69
|
+
lines.append(f" {rel_path} ({self._format_size(size)})")
|
|
70
|
+
except:
|
|
71
|
+
pass
|
|
72
|
+
return '\n'.join(lines)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return f"Ошибка: {e}"
|
|
75
|
+
|
|
76
|
+
def _format_size(self, size):
|
|
77
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
78
|
+
if size < 1024: return f"{size:.1f}{unit}"
|
|
79
|
+
size /= 1024
|
|
80
|
+
return f"{size:.1f}TB"
|
|
81
|
+
|
|
82
|
+
def create_file(self, path: str, content: str) -> bool:
|
|
83
|
+
try:
|
|
84
|
+
full_path = self.project_root / path
|
|
85
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
if full_path.exists():
|
|
87
|
+
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
88
|
+
shutil.copy2(full_path, backup)
|
|
89
|
+
full_path.write_text(content, encoding='utf-8')
|
|
90
|
+
return True
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print_error(f"Ошибка создания {path}: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def modify_file(self, path: str, content: str) -> bool:
|
|
96
|
+
try:
|
|
97
|
+
full_path = self.project_root / path
|
|
98
|
+
if not full_path.exists():
|
|
99
|
+
return self.create_file(path, content)
|
|
100
|
+
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
101
|
+
shutil.copy2(full_path, backup)
|
|
102
|
+
full_path.write_text(content, encoding='utf-8')
|
|
103
|
+
return True
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print_error(f"Ошибка изменения {path}: {e}")
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def delete_file(self, path: str) -> bool:
|
|
109
|
+
try:
|
|
110
|
+
full_path = self.project_root / path
|
|
111
|
+
if not full_path.exists():
|
|
112
|
+
return False
|
|
113
|
+
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
114
|
+
shutil.copy2(full_path, backup)
|
|
115
|
+
full_path.unlink()
|
|
116
|
+
print_success(f"Удалён: {path}")
|
|
117
|
+
return True
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print_error(f"Ошибка удаления {path}: {e}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def read_file(self, path: str) -> bool:
|
|
123
|
+
try:
|
|
124
|
+
full_path = self.project_root / path
|
|
125
|
+
if not full_path.exists():
|
|
126
|
+
return False
|
|
127
|
+
content = full_path.read_text(encoding='utf-8', errors='ignore')
|
|
128
|
+
print(f"\n=== {path} ===\n{content}")
|
|
129
|
+
return True
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print_error(f"Ошибка чтения {path}: {e}")
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def list_directory(self, path: str = ".") -> bool:
|
|
135
|
+
try:
|
|
136
|
+
full_path = self.project_root / path
|
|
137
|
+
if not full_path.exists():
|
|
138
|
+
return False
|
|
139
|
+
items = list(full_path.iterdir())
|
|
140
|
+
print(f"\n=== {path or '.'} ({len(items)} элементов) ===")
|
|
141
|
+
for item in sorted(items):
|
|
142
|
+
if item.is_dir():
|
|
143
|
+
print(f" 📁 {item.name}/")
|
|
144
|
+
else:
|
|
145
|
+
size = item.stat().st_size
|
|
146
|
+
print(f" 📄 {item.name} ({self._format_size(size)})")
|
|
147
|
+
return True
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print_error(f"Ошибка: {e}")
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def execute_command(self, command: str, timeout: int = 300) -> tuple[bool, str]:
|
|
153
|
+
"""Выполняет команду и возвращает (успех, вывод)"""
|
|
154
|
+
try:
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
command,
|
|
157
|
+
shell=True,
|
|
158
|
+
cwd=self.project_root,
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
timeout=timeout
|
|
162
|
+
)
|
|
163
|
+
output = result.stdout + result.stderr
|
|
164
|
+
success = result.returncode == 0
|
|
165
|
+
print_command_result(command, success, output if not success else None)
|
|
166
|
+
return success, output
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
print_command_result(command, False, "Таймаут выполнения")
|
|
169
|
+
return False, "Таймаут выполнения"
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print_command_result(command, False, str(e))
|
|
172
|
+
return False, str(e)
|
|
173
|
+
|
|
174
|
+
def install_package(self, package: str) -> bool:
|
|
175
|
+
"""Устанавливает пакет через pip"""
|
|
176
|
+
spinner = Spinner(f"Установка {package}")
|
|
177
|
+
spinner.start()
|
|
178
|
+
try:
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
f"pip install {package}",
|
|
181
|
+
shell=True,
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
timeout=120
|
|
185
|
+
)
|
|
186
|
+
spinner.stop()
|
|
187
|
+
success = result.returncode == 0
|
|
188
|
+
print_package_status(package, "установлен", success)
|
|
189
|
+
if not success:
|
|
190
|
+
print_error(result.stderr[:200])
|
|
191
|
+
return success
|
|
192
|
+
except:
|
|
193
|
+
spinner.stop()
|
|
194
|
+
print_package_status(package, "ошибка", False)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def get_installed_packages(self) -> List[str]:
|
|
198
|
+
"""Получает список установленных pip-пакетов"""
|
|
199
|
+
try:
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
"pip list --format=freeze",
|
|
202
|
+
shell=True,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
text=True,
|
|
205
|
+
timeout=10
|
|
206
|
+
)
|
|
207
|
+
if result.returncode == 0:
|
|
208
|
+
return [line.split('==')[0].lower() for line in result.stdout.strip().split('\n') if '==' in line]
|
|
209
|
+
except:
|
|
210
|
+
pass
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
def save_session(self, history: List[Dict], model: str, stats: Dict) -> str:
|
|
214
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
215
|
+
filename = f"handycode_session_{timestamp}.md"
|
|
216
|
+
try:
|
|
217
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
218
|
+
f.write(f"# HandyCode Session\n\n")
|
|
219
|
+
f.write(f"Date: {datetime.now()}\n")
|
|
220
|
+
f.write(f"Model: {model}\n\n---\n\n")
|
|
221
|
+
for msg in history[1:]:
|
|
222
|
+
if msg['role'] == 'user':
|
|
223
|
+
f.write(f"## User\n\n{msg['content']}\n\n")
|
|
224
|
+
else:
|
|
225
|
+
f.write(f"## Assistant\n\n{msg['content']}\n\n---\n\n")
|
|
226
|
+
return print_success(f"Сессия сохранена: {filename}")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return print_error(f"Ошибка сохранения: {e}")
|
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
import os
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
7
9
|
|
|
8
10
|
class Colors:
|
|
9
|
-
# Базовые
|
|
10
11
|
RESET = '\033[0m'
|
|
11
12
|
BOLD = '\033[1m'
|
|
12
13
|
DIM = '\033[2m'
|
|
13
14
|
ITALIC = '\033[3m'
|
|
14
|
-
UNDERLINE = '\033[4m'
|
|
15
15
|
|
|
16
|
-
# Стандартные мягкие тона (не яркие)
|
|
17
16
|
BLACK = '\033[30m'
|
|
18
17
|
RED = '\033[31m'
|
|
19
18
|
GREEN = '\033[32m'
|
|
@@ -23,7 +22,6 @@ class Colors:
|
|
|
23
22
|
CYAN = '\033[36m'
|
|
24
23
|
WHITE = '\033[37m'
|
|
25
24
|
|
|
26
|
-
# Яркие (для акцентов)
|
|
27
25
|
BRIGHT_BLACK = '\033[90m'
|
|
28
26
|
BRIGHT_RED = '\033[91m'
|
|
29
27
|
BRIGHT_GREEN = '\033[92m'
|
|
@@ -33,16 +31,15 @@ class Colors:
|
|
|
33
31
|
BRIGHT_CYAN = '\033[96m'
|
|
34
32
|
BRIGHT_WHITE = '\033[97m'
|
|
35
33
|
|
|
36
|
-
# Тема HandyCode (премиальная пастель)
|
|
37
34
|
class Theme:
|
|
38
|
-
PRIMARY = Colors.CYAN
|
|
39
|
-
SECONDARY = Colors.BLUE
|
|
40
|
-
ACCENT = Colors.MAGENTA
|
|
35
|
+
PRIMARY = Colors.CYAN
|
|
36
|
+
SECONDARY = Colors.BLUE
|
|
37
|
+
ACCENT = Colors.MAGENTA
|
|
41
38
|
SUCCESS = Colors.GREEN
|
|
42
39
|
WARNING = Colors.YELLOW
|
|
43
40
|
ERROR = Colors.RED
|
|
44
41
|
TEXT = Colors.WHITE
|
|
45
|
-
MUTED = Colors.BRIGHT_BLACK
|
|
42
|
+
MUTED = Colors.BRIGHT_BLACK
|
|
46
43
|
HIGHLIGHT = Colors.BRIGHT_WHITE
|
|
47
44
|
|
|
48
45
|
def supports_color():
|
|
@@ -84,27 +81,10 @@ def print_logo():
|
|
|
84
81
|
from .logo import get_logo
|
|
85
82
|
print(get_logo())
|
|
86
83
|
|
|
87
|
-
def print_install_logo():
|
|
88
|
-
from .logo import get_install_logo
|
|
89
|
-
print(get_install_logo())
|
|
90
|
-
|
|
91
|
-
def truncate(text, max_length=100):
|
|
92
|
-
if len(text) <= max_length:
|
|
93
|
-
return text
|
|
94
|
-
return text[:max_length-3] + "..."
|
|
95
|
-
|
|
96
|
-
def format_size(size_bytes):
|
|
97
|
-
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
98
|
-
if size_bytes < 1024:
|
|
99
|
-
return f"{size_bytes:.1f} {unit}"
|
|
100
|
-
size_bytes /= 1024
|
|
101
|
-
return f"{size_bytes:.1f} TB"
|
|
102
|
-
|
|
103
84
|
def print_divider(char="─", width=60, color=Theme.MUTED):
|
|
104
85
|
print(colorize(char * width, color))
|
|
105
86
|
|
|
106
87
|
def print_box(lines, color=Theme.PRIMARY):
|
|
107
|
-
"""Рамка вокруг списка строк"""
|
|
108
88
|
max_len = max((len(line) for line in lines), default=0) + 2
|
|
109
89
|
top = "┌" + "─" * max_len + "┐"
|
|
110
90
|
bottom = "└" + "─" * max_len + "┘"
|
|
@@ -114,7 +94,6 @@ def print_box(lines, color=Theme.PRIMARY):
|
|
|
114
94
|
print(colorize(bottom, color))
|
|
115
95
|
|
|
116
96
|
def print_section(title, content_lines):
|
|
117
|
-
"""Секция с заголовком и содержимым"""
|
|
118
97
|
print_divider("─", 50, Theme.MUTED)
|
|
119
98
|
print(colorize(f" {title}", Theme.HIGHLIGHT + Colors.BOLD))
|
|
120
99
|
for line in content_lines:
|
|
@@ -137,4 +116,50 @@ def print_file_action(action, path, details=""):
|
|
|
137
116
|
msg = f" {icon} {path}"
|
|
138
117
|
if details:
|
|
139
118
|
msg += f" {colorize(details, Theme.MUTED)}"
|
|
140
|
-
print(colorize(msg, color))
|
|
119
|
+
print(colorize(msg, color))
|
|
120
|
+
|
|
121
|
+
class Spinner:
|
|
122
|
+
"""Анимированный спиннер"""
|
|
123
|
+
def __init__(self, message="Загрузка"):
|
|
124
|
+
self.message = message
|
|
125
|
+
self.running = False
|
|
126
|
+
self.thread = None
|
|
127
|
+
self.chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
128
|
+
|
|
129
|
+
def start(self):
|
|
130
|
+
self.running = True
|
|
131
|
+
self.thread = threading.Thread(target=self._spin)
|
|
132
|
+
self.thread.start()
|
|
133
|
+
|
|
134
|
+
def stop(self, final_message=None):
|
|
135
|
+
self.running = False
|
|
136
|
+
if self.thread:
|
|
137
|
+
self.thread.join()
|
|
138
|
+
sys.stdout.write('\r' + ' ' * (len(self.message) + 10) + '\r')
|
|
139
|
+
sys.stdout.flush()
|
|
140
|
+
if final_message:
|
|
141
|
+
print(final_message)
|
|
142
|
+
|
|
143
|
+
def _spin(self):
|
|
144
|
+
i = 0
|
|
145
|
+
while self.running:
|
|
146
|
+
char = self.chars[i % len(self.chars)]
|
|
147
|
+
sys.stdout.write(f'\r {char} {self.message}...')
|
|
148
|
+
sys.stdout.flush()
|
|
149
|
+
time.sleep(0.1)
|
|
150
|
+
i += 1
|
|
151
|
+
|
|
152
|
+
def print_package_status(package, action, success=True):
|
|
153
|
+
"""Красивое отображение установки пакета"""
|
|
154
|
+
icon = '✔' if success else '✘'
|
|
155
|
+
color = Theme.SUCCESS if success else Theme.ERROR
|
|
156
|
+
print(colorize(f" {icon} {package}", color))
|
|
157
|
+
|
|
158
|
+
def print_command_result(command, success, output=None):
|
|
159
|
+
"""Отображение результата команды"""
|
|
160
|
+
icon = '✔' if success else '✘'
|
|
161
|
+
color = Theme.SUCCESS if success else Theme.ERROR
|
|
162
|
+
print(colorize(f" {icon} {command[:60]}", color))
|
|
163
|
+
if output and not success:
|
|
164
|
+
for line in output.strip().split('\n')[:5]:
|
|
165
|
+
print(colorize(f" {line}", Theme.MUTED))
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Управление файлами для HandyCode
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
import shutil
|
|
7
|
-
import subprocess
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import List, Dict, Optional
|
|
10
|
-
from datetime import datetime
|
|
11
|
-
|
|
12
|
-
from handycode.utils import print_success, print_error, print_warning, print_info
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class FileManager:
|
|
16
|
-
"""Управляет файловыми операциями HandyCode"""
|
|
17
|
-
|
|
18
|
-
def __init__(self, project_root: Path):
|
|
19
|
-
"""Инициализация файлового менеджера"""
|
|
20
|
-
self.project_root = Path(project_root).resolve()
|
|
21
|
-
self.allowed_extensions = {
|
|
22
|
-
'.html', '.css', '.scss', '.sass', '.less',
|
|
23
|
-
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
24
|
-
'.vue', '.svelte', '.astro',
|
|
25
|
-
'.py', '.pyi', '.pyx', '.pxd',
|
|
26
|
-
'.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
|
|
27
|
-
'.xml', '.env', '.gitignore', '.dockerignore',
|
|
28
|
-
'.md', '.mdx', '.rst', '.txt', '.log',
|
|
29
|
-
'.sql', '.sh', '.bash', '.zsh', '.bat', '.ps1',
|
|
30
|
-
'.java', '.kt', '.scala',
|
|
31
|
-
'.cpp', '.c', '.h', '.hpp', '.cs',
|
|
32
|
-
'.rs', '.go', '.rb', '.php', '.swift',
|
|
33
|
-
'.dart', '.r', '.jl', '.lua',
|
|
34
|
-
'.dockerfile', '.makefile', '.cmake',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
self.excluded_dirs = {
|
|
38
|
-
'node_modules', '__pycache__', '.git', '.svn',
|
|
39
|
-
'venv', '.venv', 'env', '.env',
|
|
40
|
-
'dist', 'build', '.next', '.nuxt',
|
|
41
|
-
'target', 'out', '.idea', '.vscode',
|
|
42
|
-
'.DS_Store', 'Thumbs.db',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
def scan_project(self) -> str:
|
|
46
|
-
"""Сканирует проект и возвращает структуру"""
|
|
47
|
-
if not self.project_root.exists():
|
|
48
|
-
return ""
|
|
49
|
-
|
|
50
|
-
try:
|
|
51
|
-
lines = [f"📁 Проект: {self.project_root.name}"]
|
|
52
|
-
|
|
53
|
-
# Собираем все файлы
|
|
54
|
-
all_files = []
|
|
55
|
-
for ext in self.allowed_extensions:
|
|
56
|
-
all_files.extend(self.project_root.rglob(f"*{ext}"))
|
|
57
|
-
|
|
58
|
-
# Фильтруем исключённые
|
|
59
|
-
files = [
|
|
60
|
-
f for f in all_files
|
|
61
|
-
if f.is_file() and not any(ex in f.parts for ex in self.excluded_dirs)
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
lines.append(f"📄 Файлов: {len(files)}")
|
|
65
|
-
|
|
66
|
-
# Строим дерево
|
|
67
|
-
if files:
|
|
68
|
-
lines.append("\n📂 Структура:")
|
|
69
|
-
tree = self._build_tree(files)
|
|
70
|
-
lines.extend(self._print_tree(tree))
|
|
71
|
-
|
|
72
|
-
# Ключевые файлы
|
|
73
|
-
key_files = [
|
|
74
|
-
'package.json', 'requirements.txt', 'Dockerfile',
|
|
75
|
-
'README.md', 'setup.py', 'pyproject.toml',
|
|
76
|
-
'tsconfig.json', '.gitignore', 'docker-compose.yml',
|
|
77
|
-
'main.py', 'app.py', 'index.js', 'server.js'
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
for file in files:
|
|
81
|
-
if file.name in key_files:
|
|
82
|
-
try:
|
|
83
|
-
content = file.read_text(encoding='utf-8')
|
|
84
|
-
if len(content) < 5000:
|
|
85
|
-
lines.append(f"\n{'=' * 40}")
|
|
86
|
-
lines.append(f"📄 {file.relative_to(self.project_root)}")
|
|
87
|
-
lines.append('=' * 40)
|
|
88
|
-
lines.append(content)
|
|
89
|
-
except:
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
return '\n'.join(lines)
|
|
93
|
-
except Exception as e:
|
|
94
|
-
return f"Ошибка сканирования: {e}"
|
|
95
|
-
|
|
96
|
-
def _build_tree(self, files: List[Path]) -> dict:
|
|
97
|
-
"""Строит дерево директорий"""
|
|
98
|
-
tree = {}
|
|
99
|
-
for file in files:
|
|
100
|
-
try:
|
|
101
|
-
relative = file.relative_to(self.project_root)
|
|
102
|
-
parts = relative.parts
|
|
103
|
-
current = tree
|
|
104
|
-
for part in parts[:-1]:
|
|
105
|
-
if part not in current:
|
|
106
|
-
current[part] = {}
|
|
107
|
-
current = current[part]
|
|
108
|
-
|
|
109
|
-
if '__files__' not in current:
|
|
110
|
-
current['__files__'] = []
|
|
111
|
-
current['__files__'].append(parts[-1])
|
|
112
|
-
except:
|
|
113
|
-
pass
|
|
114
|
-
return tree
|
|
115
|
-
|
|
116
|
-
def _print_tree(self, node: dict, indent: str = "") -> List[str]:
|
|
117
|
-
"""Выводит дерево директорий"""
|
|
118
|
-
result = []
|
|
119
|
-
# Сначала директории
|
|
120
|
-
dirs = sorted([k for k in node if k != '__files__'])
|
|
121
|
-
for d in dirs:
|
|
122
|
-
result.append(f"{indent}📁 {d}/")
|
|
123
|
-
result.extend(self._print_tree(node[d], indent + " "))
|
|
124
|
-
# Потом файлы
|
|
125
|
-
if '__files__' in node:
|
|
126
|
-
for f in sorted(node['__files__']):
|
|
127
|
-
result.append(f"{indent}📄 {f}")
|
|
128
|
-
return result
|
|
129
|
-
|
|
130
|
-
def create_file(self, path: str, content: str) -> bool:
|
|
131
|
-
"""Создаёт файл"""
|
|
132
|
-
try:
|
|
133
|
-
full_path = self.project_root / path
|
|
134
|
-
|
|
135
|
-
# Создаём директории
|
|
136
|
-
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
-
|
|
138
|
-
# Резервная копия если файл существует
|
|
139
|
-
if full_path.exists():
|
|
140
|
-
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
141
|
-
shutil.copy2(full_path, backup)
|
|
142
|
-
print_warning(f"📦 Резервная копия: {backup.name}")
|
|
143
|
-
|
|
144
|
-
# Записываем файл
|
|
145
|
-
full_path.write_text(content, encoding='utf-8')
|
|
146
|
-
|
|
147
|
-
lines = content.count('\n') + 1
|
|
148
|
-
size = len(content)
|
|
149
|
-
print_success(f"✅ Создан: {path} ({lines} строк, {size} байт)")
|
|
150
|
-
|
|
151
|
-
return True
|
|
152
|
-
except Exception as e:
|
|
153
|
-
print_error(f"Ошибка создания {path}: {e}")
|
|
154
|
-
return False
|
|
155
|
-
|
|
156
|
-
def modify_file(self, path: str, content: str) -> bool:
|
|
157
|
-
"""Изменяет файл"""
|
|
158
|
-
try:
|
|
159
|
-
full_path = self.project_root / path
|
|
160
|
-
|
|
161
|
-
if not full_path.exists():
|
|
162
|
-
print_warning(f"Файл не существует, создаём: {path}")
|
|
163
|
-
return self.create_file(path, content)
|
|
164
|
-
|
|
165
|
-
# Резервная копия
|
|
166
|
-
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
167
|
-
shutil.copy2(full_path, backup)
|
|
168
|
-
print_warning(f"📦 Резервная копия: {backup.name}")
|
|
169
|
-
|
|
170
|
-
# Записываем новое содержимое
|
|
171
|
-
full_path.write_text(content, encoding='utf-8')
|
|
172
|
-
|
|
173
|
-
lines = content.count('\n') + 1
|
|
174
|
-
print_success(f"✅ Изменён: {path} ({lines} строк)")
|
|
175
|
-
|
|
176
|
-
return True
|
|
177
|
-
except Exception as e:
|
|
178
|
-
print_error(f"Ошибка изменения {path}: {e}")
|
|
179
|
-
return False
|
|
180
|
-
|
|
181
|
-
def execute_command(self, command: str, timeout: int = 300) -> bool:
|
|
182
|
-
"""Выполняет команду"""
|
|
183
|
-
try:
|
|
184
|
-
print_warning(f"\n⚡ Выполнение: {command}")
|
|
185
|
-
|
|
186
|
-
result = subprocess.run(
|
|
187
|
-
command,
|
|
188
|
-
shell=True,
|
|
189
|
-
cwd=self.project_root,
|
|
190
|
-
capture_output=True,
|
|
191
|
-
text=True,
|
|
192
|
-
timeout=timeout
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
if result.stdout:
|
|
196
|
-
print(result.stdout)
|
|
197
|
-
|
|
198
|
-
if result.returncode != 0:
|
|
199
|
-
if result.stderr:
|
|
200
|
-
print_error(result.stderr)
|
|
201
|
-
print_error(f"Команда завершилась с ошибкой (код {result.returncode})")
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
print_success("✅ Команда выполнена успешно")
|
|
205
|
-
return True
|
|
206
|
-
|
|
207
|
-
except subprocess.TimeoutExpired:
|
|
208
|
-
print_error(f"Таймаут выполнения ({timeout}с)")
|
|
209
|
-
return False
|
|
210
|
-
except Exception as e:
|
|
211
|
-
print_error(f"Ошибка выполнения: {e}")
|
|
212
|
-
return False
|
|
213
|
-
|
|
214
|
-
def save_session(self, history: List[Dict], model: str, stats: Dict) -> str:
|
|
215
|
-
"""Сохраняет сессию в файл"""
|
|
216
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
217
|
-
filename = f"handycode_сессия_{timestamp}.md"
|
|
218
|
-
|
|
219
|
-
try:
|
|
220
|
-
with open(filename, 'w', encoding='utf-8') as f:
|
|
221
|
-
f.write(f"# Сессия HandyCode\n\n")
|
|
222
|
-
f.write(f"**Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
223
|
-
f.write(f"**Модель:** {model}\n")
|
|
224
|
-
f.write(f"**Сообщений:** {stats.get('messages_sent', 0)}\n\n")
|
|
225
|
-
f.write("---\n\n")
|
|
226
|
-
|
|
227
|
-
for msg in history[1:]: # Пропускаем системный промпт
|
|
228
|
-
if msg['role'] == 'user':
|
|
229
|
-
f.write(f"## 👤 Пользователь\n\n{msg['content']}\n\n")
|
|
230
|
-
else:
|
|
231
|
-
clean = msg['content']
|
|
232
|
-
# Убираем маркеры действий для читаемости
|
|
233
|
-
clean = clean.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[EXEC:', '')
|
|
234
|
-
f.write(f"## 🤖 Ассистент\n\n{clean}\n\n---\n\n")
|
|
235
|
-
|
|
236
|
-
return print_success(f"✅ Сессия сохранена: {filename}")
|
|
237
|
-
except Exception as e:
|
|
238
|
-
return print_error(f"Ошибка сохранения: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|