handycode 2.2.1__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.1 → handycode-2.3.0}/PKG-INFO +1 -1
- {handycode-2.2.1 → handycode-2.3.0}/handycode/__init__.py +1 -1
- {handycode-2.2.1 → handycode-2.3.0}/handycode/assistant.py +138 -67
- handycode-2.3.0/handycode/file_manager.py +228 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/utils.py +53 -28
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/PKG-INFO +1 -1
- {handycode-2.2.1 → handycode-2.3.0}/setup.py +1 -1
- handycode-2.2.1/handycode/file_manager.py +0 -238
- {handycode-2.2.1 → handycode-2.3.0}/LICENSE +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/README.md +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/__main__.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/cli.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/config.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/logo.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/main.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/models.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/project_templates.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode/security.py +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/SOURCES.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/dependency_links.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/entry_points.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/requires.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/top_level.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.0}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Основной класс ассистента HandyCode
|
|
2
|
+
Основной класс ассистента HandyCode
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
@@ -34,43 +34,33 @@ from handycode.security import SecurityChecker
|
|
|
34
34
|
from handycode.utils import (
|
|
35
35
|
Colors, Theme, colorize, print_colored, print_header, print_success,
|
|
36
36
|
print_error, print_warning, print_info, print_logo,
|
|
37
|
-
print_divider, print_file_action, print_status, print_section, print_box
|
|
37
|
+
print_divider, print_file_action, print_status, print_section, print_box,
|
|
38
|
+
Spinner, print_package_status
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def interactive_confirm(commands):
|
|
42
|
-
"""
|
|
43
|
-
Интерактивное меню выбора команд.
|
|
44
|
-
Управление: ↑/↓ для навигации, ПРОБЕЛ для выбора, ENTER для подтверждения.
|
|
45
|
-
Возвращает список выбранных команд.
|
|
46
|
-
"""
|
|
43
|
+
"""Интерактивное меню выбора команд"""
|
|
47
44
|
if not commands:
|
|
48
45
|
return []
|
|
49
46
|
|
|
50
|
-
# Настройка для Windows
|
|
51
47
|
if os.name == 'nt':
|
|
52
48
|
import msvcrt
|
|
53
|
-
|
|
54
49
|
def get_key():
|
|
55
50
|
key = msvcrt.getch()
|
|
56
|
-
if key == b'\xe0':
|
|
51
|
+
if key == b'\xe0':
|
|
57
52
|
key = msvcrt.getch()
|
|
58
53
|
if key == b'H': return 'up'
|
|
59
54
|
if key == b'P': return 'down'
|
|
60
55
|
if key == b'\r': return 'enter'
|
|
61
56
|
if key == b' ': return 'space'
|
|
62
|
-
if key
|
|
63
|
-
if key
|
|
64
|
-
if key
|
|
65
|
-
if key == b'S': return 'S'
|
|
66
|
-
if key == b'c': return 'c'
|
|
67
|
-
if key == b'C': return 'C'
|
|
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'
|
|
68
60
|
if key == b'\x1b': return 'escape'
|
|
69
61
|
return key.decode('utf-8', errors='ignore')
|
|
70
62
|
else:
|
|
71
|
-
import tty
|
|
72
|
-
import termios
|
|
73
|
-
|
|
63
|
+
import tty, termios
|
|
74
64
|
def get_key():
|
|
75
65
|
fd = sys.stdin.fileno()
|
|
76
66
|
old = termios.tcgetattr(fd)
|
|
@@ -88,51 +78,28 @@ def interactive_confirm(commands):
|
|
|
88
78
|
finally:
|
|
89
79
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
90
80
|
|
|
91
|
-
selected = [True] * len(commands)
|
|
81
|
+
selected = [True] * len(commands)
|
|
92
82
|
current = 0
|
|
93
83
|
|
|
94
84
|
def render():
|
|
95
|
-
# Очищаем предыдущий вывод
|
|
96
85
|
print(f"\033[{len(commands) + 4}A\033[J", end="")
|
|
97
|
-
|
|
98
86
|
print()
|
|
99
87
|
print(colorize(" ⚡ Команды для выполнения:", Theme.HIGHLIGHT + Colors.BOLD))
|
|
100
88
|
print(colorize(" ─────────────────────────────────────────────────", Theme.MUTED))
|
|
101
|
-
|
|
102
89
|
for i, cmd in enumerate(commands):
|
|
103
|
-
if i == current
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
prefix = " "
|
|
107
|
-
|
|
108
|
-
if selected[i]:
|
|
109
|
-
checkbox = colorize("◉", Theme.SUCCESS)
|
|
110
|
-
cmd_color = Theme.SUCCESS
|
|
111
|
-
else:
|
|
112
|
-
checkbox = colorize("○", Theme.MUTED)
|
|
113
|
-
cmd_color = Theme.MUTED
|
|
114
|
-
|
|
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
|
|
115
93
|
print(f"{prefix} {checkbox} {colorize(cmd, cmd_color)}")
|
|
116
|
-
|
|
117
|
-
print()
|
|
118
|
-
print(colorize(" Управление:", Theme.MUTED))
|
|
119
|
-
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить", Theme.MUTED))
|
|
120
|
-
|
|
121
|
-
# Рендерим первый раз
|
|
122
|
-
print()
|
|
123
|
-
print()
|
|
124
|
-
print()
|
|
125
|
-
print()
|
|
126
|
-
print()
|
|
127
|
-
print()
|
|
128
|
-
for _ in range(len(commands) + 4):
|
|
129
94
|
print()
|
|
95
|
+
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить C Отмена", Theme.MUTED))
|
|
130
96
|
|
|
97
|
+
for _ in range(len(commands) + 5):
|
|
98
|
+
print()
|
|
131
99
|
render()
|
|
132
100
|
|
|
133
101
|
while True:
|
|
134
102
|
key = get_key()
|
|
135
|
-
|
|
136
103
|
if key == 'up':
|
|
137
104
|
current = (current - 1) % len(commands)
|
|
138
105
|
render()
|
|
@@ -168,7 +135,12 @@ class HandyCode:
|
|
|
168
135
|
self.model_settings = get_model_settings(self.current_model)
|
|
169
136
|
self.file_manager = FileManager(self.project_path)
|
|
170
137
|
self.security = SecurityChecker(self.project_path)
|
|
138
|
+
|
|
139
|
+
# Получаем список установленных пакетов
|
|
140
|
+
self.installed_packages = self.file_manager.get_installed_packages()
|
|
141
|
+
|
|
171
142
|
project_context = self._build_project_context()
|
|
143
|
+
|
|
172
144
|
self.conversation_history = [
|
|
173
145
|
{"role": "system", "content": self._get_system_prompt() + project_context}
|
|
174
146
|
]
|
|
@@ -179,12 +151,21 @@ class HandyCode:
|
|
|
179
151
|
}
|
|
180
152
|
self.stream_buffer = ""
|
|
181
153
|
self.pending_commands = []
|
|
154
|
+
self.command_results = [] # Результаты выполнения команд
|
|
182
155
|
self._setup_readline()
|
|
183
156
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
184
157
|
self._interrupt_count = 0
|
|
185
158
|
|
|
186
159
|
def _build_project_context(self):
|
|
187
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
|
+
|
|
188
169
|
try:
|
|
189
170
|
all_files = []
|
|
190
171
|
for ext in self.file_manager.allowed_extensions:
|
|
@@ -199,13 +180,26 @@ class HandyCode:
|
|
|
199
180
|
if not any(rel.startswith(ex) for ex in self.file_manager.excluded_dirs):
|
|
200
181
|
files.append(f)
|
|
201
182
|
seen.add(f)
|
|
202
|
-
context += f"\
|
|
183
|
+
context += f"\n\n=== PROJECT FILES ({len(files)}) ===\n"
|
|
203
184
|
for file in files:
|
|
204
185
|
try:
|
|
205
186
|
rel_path = file.relative_to(self.project_path)
|
|
206
187
|
size = file.stat().st_size
|
|
207
188
|
context += f" {rel_path} ({self._format_size(size)})\n"
|
|
208
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
|
|
209
203
|
except: pass
|
|
210
204
|
return context
|
|
211
205
|
|
|
@@ -216,22 +210,50 @@ class HandyCode:
|
|
|
216
210
|
return f"{size:.1f}TB"
|
|
217
211
|
|
|
218
212
|
def _get_system_prompt(self):
|
|
219
|
-
return """You are HandyCode - AI coding assistant.
|
|
220
|
-
|
|
221
|
-
|
|
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]]
|
|
222
229
|
code here
|
|
223
230
|
[[END]]
|
|
224
|
-
|
|
231
|
+
|
|
232
|
+
[[MODIFY:path/file.py]]
|
|
225
233
|
new code here
|
|
226
234
|
[[END]]
|
|
235
|
+
|
|
236
|
+
[[DELETE:path/file.py]]
|
|
237
|
+
[[READ:path/file.py]]
|
|
238
|
+
[[LIST:directory/]]
|
|
227
239
|
[[EXEC:command]]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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]]."""
|
|
235
257
|
|
|
236
258
|
def _setup_readline(self):
|
|
237
259
|
if not HAS_READLINE: return
|
|
@@ -254,6 +276,8 @@ Speak Russian. Write code in English."""
|
|
|
254
276
|
|
|
255
277
|
def _process_stream_chunk(self, chunk):
|
|
256
278
|
self.stream_buffer += chunk
|
|
279
|
+
|
|
280
|
+
# CREATE с анимацией
|
|
257
281
|
while True:
|
|
258
282
|
match = re.search(r'\[\[CREATE:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
259
283
|
if match:
|
|
@@ -262,11 +286,15 @@ Speak Russian. Write code in English."""
|
|
|
262
286
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
263
287
|
content = re.sub(r'\n```$', '', content)
|
|
264
288
|
if content and self.security.is_safe_path(path):
|
|
289
|
+
spinner = Spinner(f"Создание {path}")
|
|
290
|
+
spinner.start()
|
|
265
291
|
self.file_manager.create_file(path, content)
|
|
292
|
+
spinner.stop(f" ✔ {path} ({content.count(chr(10))+1} строк)")
|
|
266
293
|
self.stats["files_created"].append(path)
|
|
267
|
-
print_file_action('create', path, f"({content.count(chr(10))+1} lines)")
|
|
268
294
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
269
295
|
else: break
|
|
296
|
+
|
|
297
|
+
# MODIFY
|
|
270
298
|
while True:
|
|
271
299
|
match = re.search(r'\[\[MODIFY:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
272
300
|
if match:
|
|
@@ -275,11 +303,25 @@ Speak Russian. Write code in English."""
|
|
|
275
303
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
276
304
|
content = re.sub(r'\n```$', '', content)
|
|
277
305
|
if content and self.security.is_safe_path(path):
|
|
306
|
+
spinner = Spinner(f"Изменение {path}")
|
|
307
|
+
spinner.start()
|
|
278
308
|
self.file_manager.modify_file(path, content)
|
|
309
|
+
spinner.stop(f" ✎ {path} ({content.count(chr(10))+1} строк)")
|
|
279
310
|
self.stats["files_modified"].append(path)
|
|
280
|
-
print_file_action('modify', path, f"({content.count(chr(10))+1} lines)")
|
|
281
311
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
282
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
|
|
283
325
|
while True:
|
|
284
326
|
match = re.search(r'\[\[EXEC:(.+?)\]\]', self.stream_buffer)
|
|
285
327
|
if match:
|
|
@@ -322,7 +364,7 @@ Speak Russian. Write code in English."""
|
|
|
322
364
|
if '[[END]]' in content:
|
|
323
365
|
in_code = False
|
|
324
366
|
if not in_code:
|
|
325
|
-
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(']]', '')
|
|
326
368
|
if clean.strip():
|
|
327
369
|
print(clean, end="", flush=True)
|
|
328
370
|
self._process_stream_chunk(content)
|
|
@@ -360,7 +402,7 @@ Speak Russian. Write code in English."""
|
|
|
360
402
|
if '[[END]]' in content:
|
|
361
403
|
in_code = False
|
|
362
404
|
if not in_code:
|
|
363
|
-
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(']]', '')
|
|
364
406
|
if clean.strip():
|
|
365
407
|
print(clean, end="", flush=True)
|
|
366
408
|
self._process_stream_chunk(content)
|
|
@@ -409,8 +451,26 @@ Speak Russian. Write code in English."""
|
|
|
409
451
|
for cmd in selected_commands:
|
|
410
452
|
if self.security.is_safe_command(cmd):
|
|
411
453
|
print_status(f"Выполняется: {cmd}")
|
|
412
|
-
self.file_manager.execute_command(cmd)
|
|
454
|
+
success, output = self.file_manager.execute_command(cmd)
|
|
413
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("Вы можете попросить меня исправить ошибки")
|
|
414
474
|
else:
|
|
415
475
|
print_warning("Команды пропущены")
|
|
416
476
|
|
|
@@ -426,6 +486,7 @@ Speak Russian. Write code in English."""
|
|
|
426
486
|
print_box([
|
|
427
487
|
"/help Справка",
|
|
428
488
|
"/scan Показать проект",
|
|
489
|
+
"/packages Показать установленные пакеты",
|
|
429
490
|
"/models Модели",
|
|
430
491
|
"/model N Сменить модель",
|
|
431
492
|
"/clear Очистить историю",
|
|
@@ -435,6 +496,11 @@ Speak Russian. Write code in English."""
|
|
|
435
496
|
], Theme.PRIMARY)
|
|
436
497
|
elif cmd in ['/scan', '/s']:
|
|
437
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))
|
|
438
504
|
elif cmd in ['/models', '/m']:
|
|
439
505
|
lines = []
|
|
440
506
|
for name, mid in MODELS.items():
|
|
@@ -449,14 +515,18 @@ Speak Russian. Write code in English."""
|
|
|
449
515
|
print_success(f"Модель изменена на {name}")
|
|
450
516
|
elif cmd in ['/clear', '/c']:
|
|
451
517
|
self.conversation_history = [self.conversation_history[0]]
|
|
518
|
+
self.command_results = []
|
|
452
519
|
print_success("История очищена")
|
|
520
|
+
elif cmd in ['/save']:
|
|
521
|
+
self.file_manager.save_session(self.conversation_history, self.current_model, self.stats)
|
|
453
522
|
elif cmd in ['/stats']:
|
|
454
523
|
print_box([
|
|
455
524
|
f"Сообщений: {self.stats['messages_sent']}",
|
|
456
525
|
f"Создано файлов: {len(self.stats['files_created'])}",
|
|
457
526
|
f"Изменено: {len(self.stats['files_modified'])}",
|
|
458
527
|
f"Удалено: {len(self.stats['files_deleted'])}",
|
|
459
|
-
f"Команд выполнено: {len(self.stats['commands_executed'])}"
|
|
528
|
+
f"Команд выполнено: {len(self.stats['commands_executed'])}",
|
|
529
|
+
f"Пакетов установлено: {len(self.installed_packages)}"
|
|
460
530
|
], Theme.SECONDARY)
|
|
461
531
|
elif cmd in ['/exit', '/q']:
|
|
462
532
|
print_success("До свидания!")
|
|
@@ -471,6 +541,7 @@ Speak Russian. Write code in English."""
|
|
|
471
541
|
print_divider("─", 60, Theme.MUTED)
|
|
472
542
|
print(colorize(f" 📁 Проект: {self.project_path}", Theme.TEXT))
|
|
473
543
|
print(colorize(f" 🤖 Модель: {self.current_model}", Theme.TEXT))
|
|
544
|
+
print(colorize(f" 📦 Пакетов: {len(self.installed_packages)}", Theme.TEXT))
|
|
474
545
|
print(colorize(f" {Theme.MUTED}/help для команд{Colors.RESET}", Theme.MUTED))
|
|
475
546
|
print_divider("─", 60, Theme.MUTED)
|
|
476
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
|