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.
Files changed (24) hide show
  1. {handycode-2.2.0 → handycode-2.3.0}/PKG-INFO +1 -1
  2. {handycode-2.2.0 → handycode-2.3.0}/handycode/__init__.py +1 -1
  3. {handycode-2.2.0 → handycode-2.3.0}/handycode/assistant.py +230 -35
  4. handycode-2.3.0/handycode/file_manager.py +228 -0
  5. {handycode-2.2.0 → handycode-2.3.0}/handycode/utils.py +53 -28
  6. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/PKG-INFO +1 -1
  7. {handycode-2.2.0 → handycode-2.3.0}/setup.py +1 -1
  8. handycode-2.2.0/handycode/file_manager.py +0 -238
  9. {handycode-2.2.0 → handycode-2.3.0}/LICENSE +0 -0
  10. {handycode-2.2.0 → handycode-2.3.0}/README.md +0 -0
  11. {handycode-2.2.0 → handycode-2.3.0}/handycode/__main__.py +0 -0
  12. {handycode-2.2.0 → handycode-2.3.0}/handycode/cli.py +0 -0
  13. {handycode-2.2.0 → handycode-2.3.0}/handycode/config.py +0 -0
  14. {handycode-2.2.0 → handycode-2.3.0}/handycode/logo.py +0 -0
  15. {handycode-2.2.0 → handycode-2.3.0}/handycode/main.py +0 -0
  16. {handycode-2.2.0 → handycode-2.3.0}/handycode/models.py +0 -0
  17. {handycode-2.2.0 → handycode-2.3.0}/handycode/project_templates.py +0 -0
  18. {handycode-2.2.0 → handycode-2.3.0}/handycode/security.py +0 -0
  19. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/SOURCES.txt +0 -0
  20. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/dependency_links.txt +0 -0
  21. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/entry_points.txt +0 -0
  22. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/requires.txt +0 -0
  23. {handycode-2.2.0 → handycode-2.3.0}/handycode.egg-info/top_level.txt +0 -0
  24. {handycode-2.2.0 → handycode-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: AI Code Assistant for DeepSeek
5
5
  Home-page: https://github.com/AuraTechno/HandyCode
6
6
  Author: AuraTechno
@@ -3,7 +3,7 @@ HandyCode - AI Ассистент для разработки
3
3
  Аналог Claude Code для командной строки
4
4
  """
5
5
 
6
- __version__ = "2.2.0"
6
+ __version__ = "2.3.0"
7
7
  __author__ = "AURA Tec."
8
8
  __license__ = "MIT"
9
9
 
@@ -1,8 +1,13 @@
1
1
  """
2
- Основной класс ассистента HandyCode с премиальным интерфейсом
2
+ Основной класс ассистента HandyCode
3
3
  """
4
4
 
5
- import os, re, json, sys, atexit, signal
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, urllib.error, ssl
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, print_install_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"\nFiles ({len(files)}):\n"
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. Create/modify/delete files and run commands.
95
- FORMAT:
96
- [[CREATE:path/file]]
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
- [[MODIFY:path/file]]
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
- RULES:
104
- 1. CREATE + EXEC in ONE response
105
- 2. Use [[END]] after file content
106
- 3. NO comments inside [[CREATE]]...[[END]]
107
- 4. Explanations BEFORE [[CREATE]] blocks
108
- 5. Files create automatically, commands need confirmation
109
- Speak Russian. Write code in English."""
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
- choice = 'A'
444
+ selected_commands = self.pending_commands
284
445
  else:
285
- print(colorize(" [A] Выполнить все [S] Пропустить [C] Отмена", Theme.MUTED))
286
- choice = input(colorize(" > ", Theme.HIGHLIGHT)).strip().upper()
287
- if choice == 'A':
288
- for cmd in self.pending_commands:
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
- print_section("📁 Проект", self.file_manager.scan_project().split('\n'))
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: AI Code Assistant for DeepSeek
5
5
  Home-page: https://github.com/AuraTechno/HandyCode
6
6
  Author: AuraTechno
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="handycode",
5
- version="2.2.0",
5
+ version="2.3.0",
6
6
  author="AuraTechno",
7
7
  description="AI Code Assistant for DeepSeek",
8
8
  long_description="HandyCode - AI Code Assistant",
@@ -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