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.
Files changed (24) hide show
  1. {handycode-2.2.1 → handycode-2.3.0}/PKG-INFO +1 -1
  2. {handycode-2.2.1 → handycode-2.3.0}/handycode/__init__.py +1 -1
  3. {handycode-2.2.1 → handycode-2.3.0}/handycode/assistant.py +138 -67
  4. handycode-2.3.0/handycode/file_manager.py +228 -0
  5. {handycode-2.2.1 → handycode-2.3.0}/handycode/utils.py +53 -28
  6. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/PKG-INFO +1 -1
  7. {handycode-2.2.1 → handycode-2.3.0}/setup.py +1 -1
  8. handycode-2.2.1/handycode/file_manager.py +0 -238
  9. {handycode-2.2.1 → handycode-2.3.0}/LICENSE +0 -0
  10. {handycode-2.2.1 → handycode-2.3.0}/README.md +0 -0
  11. {handycode-2.2.1 → handycode-2.3.0}/handycode/__main__.py +0 -0
  12. {handycode-2.2.1 → handycode-2.3.0}/handycode/cli.py +0 -0
  13. {handycode-2.2.1 → handycode-2.3.0}/handycode/config.py +0 -0
  14. {handycode-2.2.1 → handycode-2.3.0}/handycode/logo.py +0 -0
  15. {handycode-2.2.1 → handycode-2.3.0}/handycode/main.py +0 -0
  16. {handycode-2.2.1 → handycode-2.3.0}/handycode/models.py +0 -0
  17. {handycode-2.2.1 → handycode-2.3.0}/handycode/project_templates.py +0 -0
  18. {handycode-2.2.1 → handycode-2.3.0}/handycode/security.py +0 -0
  19. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/SOURCES.txt +0 -0
  20. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/dependency_links.txt +0 -0
  21. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/entry_points.txt +0 -0
  22. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/requires.txt +0 -0
  23. {handycode-2.2.1 → handycode-2.3.0}/handycode.egg-info/top_level.txt +0 -0
  24. {handycode-2.2.1 → 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.1
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.1"
6
+ __version__ = "2.3.0"
7
7
  __author__ = "AURA Tec."
8
8
  __license__ = "MIT"
9
9
 
@@ -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 == b'a': return 'a'
63
- if key == b'A': return 'A'
64
- if key == b's': return 's'
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
- prefix = colorize("", Theme.PRIMARY + Colors.BOLD)
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"\nFiles ({len(files)}):\n"
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. Create/modify/delete files and run commands.
220
- FORMAT:
221
- [[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]]
222
229
  code here
223
230
  [[END]]
224
- [[MODIFY:path/file]]
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
- RULES:
229
- 1. CREATE + EXEC in ONE response
230
- 2. Use [[END]] after file content
231
- 3. NO comments inside [[CREATE]]...[[END]]
232
- 4. Explanations BEFORE [[CREATE]] blocks
233
- 5. Files create automatically, commands need confirmation
234
- 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]]."""
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.2.1
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.1",
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