handycode 2.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ HandyCode - AI Ассистент для разработки
3
+ Аналог Claude Code для командной строки
4
+ """
5
+
6
+ __version__ = "1.0.0"
7
+ __author__ = "AURA Tec."
8
+ __license__ = "MIT"
9
+
10
+ from .main import main
11
+ from .assistant import HandyCode
12
+
13
+ __all__ = ["main", "HandyCode"]
handycode/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Точка входа для python -m handycode"""
2
+ from handycode.main import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
handycode/assistant.py ADDED
@@ -0,0 +1,533 @@
1
+ """
2
+ Основной класс ассистента HandyCode
3
+ """
4
+
5
+ import os
6
+ import re
7
+ import json
8
+ import sys
9
+ import atexit
10
+ import signal
11
+ import shutil
12
+ from pathlib import Path
13
+ from typing import List, Dict, Optional
14
+ from datetime import datetime
15
+
16
+ try:
17
+ import readline
18
+ HAS_READLINE = True
19
+ except ImportError:
20
+ HAS_READLINE = False
21
+
22
+ try:
23
+ import requests
24
+ HAS_REQUESTS = True
25
+ except ImportError:
26
+ HAS_REQUESTS = False
27
+ import urllib.request
28
+ import urllib.error
29
+ import ssl
30
+
31
+ from handycode.config import Config
32
+ from handycode.models import MODELS, get_model_settings
33
+ from handycode.file_manager import FileManager
34
+ from handycode.security import SecurityChecker
35
+ from handycode.utils import (
36
+ print_colored, print_header, print_success,
37
+ print_error, print_warning, print_info, print_logo
38
+ )
39
+
40
+
41
+ class HandyCode:
42
+ def __init__(self, project_path, model="deepseek", auto_approve=False, config=None):
43
+ self.project_path = project_path
44
+ self.auto_approve = auto_approve
45
+ self.config = config or Config()
46
+
47
+ self.api_key = self.config.get_api_key()
48
+ if not self.api_key:
49
+ raise ValueError("API key not found")
50
+
51
+ self.api_url = "https://openrouter.ai/api/v1/chat/completions"
52
+ self.current_model = MODELS.get(model, MODELS["deepseek"])
53
+ self.model_settings = get_model_settings(self.current_model)
54
+
55
+ self.file_manager = FileManager(self.project_path)
56
+ self.security = SecurityChecker(self.project_path)
57
+
58
+ project_context = self._build_project_context()
59
+
60
+ self.conversation_history = [
61
+ {"role": "system", "content": self._get_system_prompt() + project_context}
62
+ ]
63
+
64
+ self.stats = {
65
+ "messages_sent": 0,
66
+ "files_created": [],
67
+ "files_modified": [],
68
+ "files_deleted": [],
69
+ "commands_executed": [],
70
+ "start_time": datetime.now()
71
+ }
72
+
73
+ self._setup_readline()
74
+ signal.signal(signal.SIGINT, self._signal_handler)
75
+ self._interrupt_count = 0
76
+
77
+ def _build_project_context(self):
78
+ """Собирает контекст проекта"""
79
+ context = f"\n\n=== PROJECT CONTEXT ===\n"
80
+ context += f"Working directory: {self.project_path}\n"
81
+ context += f"OS: {sys.platform}\n"
82
+
83
+ try:
84
+ all_files = []
85
+ for ext in self.file_manager.allowed_extensions:
86
+ all_files.extend(self.project_path.rglob(f"*{ext}"))
87
+
88
+ all_files.extend(self.project_path.rglob("*"))
89
+ all_files = list(set(all_files))
90
+
91
+ files = [f for f in all_files if f.is_file()
92
+ and not any(ex in f.parts for ex in self.file_manager.excluded_dirs)]
93
+
94
+ context += f"\nFiles in project ({len(files)} total):\n"
95
+
96
+ for file in sorted(files):
97
+ try:
98
+ rel_path = file.relative_to(self.project_path)
99
+ size = file.stat().st_size
100
+ context += f" - {rel_path} ({self._format_size(size)})\n"
101
+ except:
102
+ pass
103
+
104
+ context += f"\nFile contents (for context):\n"
105
+ total_size = 0
106
+ max_total = 30000
107
+
108
+ for file in sorted(files):
109
+ if total_size >= max_total:
110
+ break
111
+
112
+ try:
113
+ content = file.read_text(encoding='utf-8', errors='ignore')
114
+ if len(content) > 3000:
115
+ content = content[:3000] + "\n... (truncated)"
116
+
117
+ rel_path = file.relative_to(self.project_path)
118
+ context += f"\n=== {rel_path} ===\n{content}\n"
119
+ total_size += len(content)
120
+ except:
121
+ pass
122
+
123
+ except Exception as e:
124
+ context += f"\nError: {e}\n"
125
+
126
+ return context
127
+
128
+ def _format_size(self, size):
129
+ for unit in ['B', 'KB', 'MB', 'GB']:
130
+ if size < 1024:
131
+ return f"{size:.0f}{unit}"
132
+ size /= 1024
133
+ return f"{size:.0f}GB"
134
+
135
+ def _get_system_prompt(self):
136
+ return """You are HandyCode, a powerful AI code assistant with FULL file system access.
137
+
138
+ YOU CAN:
139
+ - CREATE files: [[CREATE:path/to/file.ext]] content
140
+ - MODIFY files: [[MODIFY:path/to/file.ext]] new content
141
+ - DELETE files: [[DELETE:path/to/file.ext]]
142
+ - READ files: [[READ:path/to/file.ext]]
143
+ - LIST directory: [[LIST:path/to/dir]]
144
+ - EXECUTE commands: [[EXEC:command]]
145
+ - CREATE folders: [[MKDIR:path/to/dir]]
146
+ - COPY files: [[COPY:source]] -> [[CREATE:destination]] (use CREATE with content)
147
+ - MOVE files: [[MOVE:source]] [[CREATE:destination]]
148
+
149
+ FILES ARE AUTO-CREATED without asking. Only COMMANDS need confirmation.
150
+ You see ALL project files and their contents.
151
+
152
+ RULES:
153
+ 1. CREATE/MODIFY/DELETE/MKDIR happen automatically
154
+ 2. EXEC commands need user confirmation
155
+ 3. Show COMPLETE file contents
156
+ 4. Create ALL needed files
157
+ 5. Use the project context
158
+
159
+ Speak Russian. Write code in English."""
160
+
161
+ def _setup_readline(self):
162
+ if not HAS_READLINE:
163
+ return
164
+ try:
165
+ histfile = os.path.join(os.path.expanduser("~"), ".handycode", "history")
166
+ os.makedirs(os.path.dirname(histfile), exist_ok=True)
167
+ readline.read_history_file(histfile)
168
+ readline.set_history_length(1000)
169
+ atexit.register(readline.write_history_file, histfile)
170
+ except:
171
+ pass
172
+
173
+ def _signal_handler(self, sig, frame):
174
+ self._interrupt_count += 1
175
+ if self._interrupt_count == 1:
176
+ print("\n\nPress Ctrl+C again to exit")
177
+ else:
178
+ os._exit(0)
179
+
180
+ def reset_interrupt(self):
181
+ self._interrupt_count = 0
182
+
183
+ def _make_request_stream(self, data):
184
+ """Отправляет запрос и получает ответ в реальном времени"""
185
+ if not HAS_REQUESTS:
186
+ return self._make_request(data)
187
+
188
+ try:
189
+ response = requests.post(
190
+ self.api_url,
191
+ headers={
192
+ "Authorization": f"Bearer {self.api_key}",
193
+ "Content-Type": "application/json"
194
+ },
195
+ json={**data, "stream": True},
196
+ timeout=120,
197
+ stream=True
198
+ )
199
+
200
+ full_response = ""
201
+
202
+ for line in response.iter_lines():
203
+ if line:
204
+ line = line.decode('utf-8')
205
+ if line.startswith('data: '):
206
+ data_str = line[6:]
207
+ if data_str.strip() == '[DONE]':
208
+ break
209
+ try:
210
+ chunk = json.loads(data_str)
211
+ if 'choices' in chunk and chunk['choices']:
212
+ delta = chunk['choices'][0].get('delta', {})
213
+ content = delta.get('content', '')
214
+ if content:
215
+ print(content, end="", flush=True)
216
+ full_response += content
217
+ except:
218
+ continue
219
+
220
+ print()
221
+ return full_response
222
+
223
+ except Exception as e:
224
+ print_error(f"API Error: {e}")
225
+ return ""
226
+
227
+ def _make_request(self, data):
228
+ """Обычный запрос без стриминга"""
229
+ if HAS_REQUESTS:
230
+ try:
231
+ response = requests.post(
232
+ self.api_url,
233
+ headers={
234
+ "Authorization": f"Bearer {self.api_key}",
235
+ "Content-Type": "application/json"
236
+ },
237
+ json=data,
238
+ timeout=120
239
+ )
240
+ result = response.json()
241
+ if 'choices' in result and result['choices']:
242
+ return result['choices'][0]['message']['content']
243
+ except Exception as e:
244
+ print_error(f"API Error: {e}")
245
+ return ""
246
+
247
+ def send_message(self, user_input):
248
+ if user_input.startswith('/'):
249
+ return self._handle_command(user_input)
250
+
251
+ self.conversation_history.append({"role": "user", "content": user_input})
252
+
253
+ payload = {
254
+ "model": self.current_model,
255
+ "messages": self.conversation_history,
256
+ "temperature": self.model_settings.get("temperature", 0.3),
257
+ "max_tokens": self.model_settings.get("max_tokens", 8000),
258
+ }
259
+
260
+ try:
261
+ print_info(f"\nDEEPSEEK:")
262
+ response = self._make_request_stream(payload)
263
+
264
+ if response:
265
+ self.conversation_history.append({"role": "assistant", "content": response})
266
+
267
+ actions = self._parse_actions(response)
268
+ if actions:
269
+ self._execute_actions_smart(actions)
270
+
271
+ self.stats["messages_sent"] += 1
272
+ return response
273
+ except Exception as e:
274
+ return print_error(f"Error: {e}")
275
+
276
+ def _parse_actions(self, response):
277
+ """Парсит все типы действий"""
278
+ actions = []
279
+
280
+ # CREATE
281
+ for match in re.finditer(r'\[\[CREATE:(.+?)\]\](.*?)(?=\[\[|$)', response, re.DOTALL):
282
+ path = match.group(1).strip()
283
+ content = match.group(2).strip()
284
+ content = re.sub(r'^```[\w]*\n?', '', content)
285
+ content = re.sub(r'\n?```$', '', content)
286
+ actions.append({'type': 'create', 'path': path, 'content': content})
287
+
288
+ # MODIFY
289
+ for match in re.finditer(r'\[\[MODIFY:(.+?)\]\](.*?)(?=\[\[|$)', response, re.DOTALL):
290
+ path = match.group(1).strip()
291
+ content = match.group(2).strip()
292
+ content = re.sub(r'^```[\w]*\n?', '', content)
293
+ content = re.sub(r'\n?```$', '', content)
294
+ actions.append({'type': 'modify', 'path': path, 'content': content})
295
+
296
+ # DELETE
297
+ for match in re.finditer(r'\[\[DELETE:(.+?)\]\]', response):
298
+ actions.append({'type': 'delete', 'path': match.group(1).strip()})
299
+
300
+ # READ
301
+ for match in re.finditer(r'\[\[READ:(.+?)\]\]', response):
302
+ actions.append({'type': 'read', 'path': match.group(1).strip()})
303
+
304
+ # LIST
305
+ for match in re.finditer(r'\[\[LIST:(.+?)\]\]', response):
306
+ actions.append({'type': 'list', 'path': match.group(1).strip()})
307
+
308
+ # MKDIR
309
+ for match in re.finditer(r'\[\[MKDIR:(.+?)\]\]', response):
310
+ actions.append({'type': 'mkdir', 'path': match.group(1).strip()})
311
+
312
+ # COPY
313
+ for match in re.finditer(r'\[\[COPY:(.+?)\]\]', response):
314
+ actions.append({'type': 'copy', 'path': match.group(1).strip()})
315
+
316
+ # MOVE
317
+ for match in re.finditer(r'\[\[MOVE:(.+?)\]\]', response):
318
+ actions.append({'type': 'move', 'path': match.group(1).strip()})
319
+
320
+ # EXEC
321
+ for match in re.finditer(r'\[\[EXEC:(.+?)\]\]', response):
322
+ actions.append({'type': 'exec', 'command': match.group(1).strip()})
323
+
324
+ return actions
325
+
326
+ def _execute_actions_smart(self, actions):
327
+ """Умное выполнение: файлы авто, команды с подтверждением"""
328
+ if not actions:
329
+ return
330
+
331
+ file_actions = [a for a in actions if a['type'] in ['create', 'modify', 'delete', 'mkdir', 'read', 'list', 'copy', 'move']]
332
+ exec_actions = [a for a in actions if a['type'] == 'exec']
333
+
334
+ # Файловые операции выполняем автоматически
335
+ if file_actions:
336
+ print_header("\nAUTO FILE OPERATIONS:")
337
+ for action in file_actions:
338
+ self._execute_action(action)
339
+
340
+ # Команды требуют подтверждения
341
+ if exec_actions:
342
+ print_header("\nCOMMANDS TO EXECUTE:")
343
+ for i, action in enumerate(exec_actions, 1):
344
+ print(f" {i}. {action['command']}")
345
+
346
+ print("\n[A] Execute all [S] Skip all [1-N] Select [C] Cancel")
347
+ choice = input("> ").strip().upper()
348
+
349
+ if choice == 'A':
350
+ for action in exec_actions:
351
+ self._execute_action(action)
352
+ elif choice == 'S':
353
+ print_warning("Skipped")
354
+ elif choice == 'C':
355
+ print_warning("Cancelled")
356
+ elif choice.isdigit():
357
+ idx = int(choice) - 1
358
+ if 0 <= idx < len(exec_actions):
359
+ self._execute_action(exec_actions[idx])
360
+
361
+ def _execute_action(self, action):
362
+ """Выполняет одно действие"""
363
+ try:
364
+ if action['type'] == 'create':
365
+ self.file_manager.create_file(action['path'], action['content'])
366
+ self.stats["files_created"].append(action['path'])
367
+
368
+ elif action['type'] == 'modify':
369
+ self.file_manager.modify_file(action['path'], action['content'])
370
+ self.stats["files_modified"].append(action['path'])
371
+
372
+ elif action['type'] == 'delete':
373
+ self._delete_file(action['path'])
374
+ self.stats["files_deleted"].append(action['path'])
375
+
376
+ elif action['type'] == 'read':
377
+ self._read_file(action['path'])
378
+
379
+ elif action['type'] == 'list':
380
+ self._list_directory(action['path'])
381
+
382
+ elif action['type'] == 'mkdir':
383
+ self._make_directory(action['path'])
384
+
385
+ elif action['type'] == 'exec':
386
+ self.file_manager.execute_command(action['command'])
387
+ self.stats["commands_executed"].append(action['command'])
388
+
389
+ elif action['type'] == 'copy':
390
+ self._copy_file(action['path'])
391
+
392
+ elif action['type'] == 'move':
393
+ self._move_file(action['path'])
394
+
395
+ except Exception as e:
396
+ print_error(f"Failed: {e}")
397
+
398
+ def _delete_file(self, path):
399
+ """Удаляет файл"""
400
+ full_path = self.project_path / path
401
+ if not self.security.is_safe_path(str(path)):
402
+ print_error(f"Unsafe: {path}")
403
+ return
404
+
405
+ if full_path.exists():
406
+ # Бэкап перед удалением
407
+ backup = full_path.with_suffix(full_path.suffix + '.bak')
408
+ shutil.copy2(full_path, backup)
409
+ full_path.unlink()
410
+ print_success(f"Deleted: {path} (backup: {backup.name})")
411
+ else:
412
+ print_warning(f"Not found: {path}")
413
+
414
+ def _read_file(self, path):
415
+ """Читает и показывает файл"""
416
+ full_path = self.project_path / path
417
+ if full_path.exists():
418
+ content = full_path.read_text(encoding='utf-8', errors='ignore')
419
+ print_header(f"\n=== {path} ===")
420
+ print(content)
421
+ print_header("=" * (len(path) + 8))
422
+ else:
423
+ print_warning(f"Not found: {path}")
424
+
425
+ def _list_directory(self, path):
426
+ """Показывает содержимое директории"""
427
+ full_path = self.project_path / path
428
+ if full_path.exists() and full_path.is_dir():
429
+ items = sorted(full_path.iterdir())
430
+ print_header(f"\n=== {path}/ ({len(items)} items) ===")
431
+ for item in items:
432
+ if item.is_dir():
433
+ print(f" [DIR] {item.name}/")
434
+ else:
435
+ size = item.stat().st_size
436
+ print(f" [FILE] {item.name} ({self._format_size(size)})")
437
+ else:
438
+ print_warning(f"Not found: {path}")
439
+
440
+ def _make_directory(self, path):
441
+ """Создаёт директорию"""
442
+ full_path = self.project_path / path
443
+ full_path.mkdir(parents=True, exist_ok=True)
444
+ print_success(f"Created dir: {path}")
445
+
446
+ def _copy_file(self, source):
447
+ """Копирует файл (ожидает, что следом будет CREATE)"""
448
+ print_info(f"Copy source noted: {source}")
449
+
450
+ def _move_file(self, source):
451
+ """Перемещает файл (ожидает, что следом будет CREATE)"""
452
+ full_path = self.project_path / source
453
+ if full_path.exists():
454
+ print_info(f"Move source noted: {source}")
455
+ else:
456
+ print_warning(f"Source not found: {source}")
457
+
458
+ def _handle_command(self, user_input):
459
+ cmd = user_input.lower().split()[0]
460
+ if cmd in ['/help', '/h']:
461
+ print("""
462
+ COMMANDS:
463
+ /help Show help
464
+ /scan Scan project
465
+ /models List models
466
+ /model N Switch model
467
+ /clear Clear history
468
+ /save Save session
469
+ /stats Statistics
470
+ /exit Exit
471
+ """)
472
+ elif cmd in ['/scan', '/s']:
473
+ print(self.file_manager.scan_project())
474
+ elif cmd in ['/models', '/m']:
475
+ for name in MODELS:
476
+ print(f" {name}")
477
+ elif cmd in ['/clear', '/c']:
478
+ self.conversation_history = [self.conversation_history[0]]
479
+ print_success("Cleared")
480
+ elif cmd in ['/save']:
481
+ self.file_manager.save_session(self.conversation_history, self.current_model, self.stats)
482
+ elif cmd in ['/stats']:
483
+ duration = datetime.now() - self.stats["start_time"]
484
+ print(f"Messages: {self.stats['messages_sent']}")
485
+ print(f"Created: {len(self.stats['files_created'])} files")
486
+ print(f"Modified: {len(self.stats['files_modified'])} files")
487
+ print(f"Deleted: {len(self.stats['files_deleted'])} files")
488
+ print(f"Commands: {len(self.stats['commands_executed'])}")
489
+ print(f"Duration: {duration}")
490
+ elif cmd in ['/exit', '/q']:
491
+ print_success("Goodbye!")
492
+ os._exit(0)
493
+ elif cmd in ['/model'] and len(user_input.split()) > 1:
494
+ model_name = user_input.split()[1]
495
+ if model_name in MODELS:
496
+ self.current_model = MODELS[model_name]
497
+ self.model_settings = get_model_settings(self.current_model)
498
+ print_success(f"Switched to: {model_name}")
499
+ else:
500
+ print_error(f"Unknown model: {model_name}")
501
+ return ""
502
+
503
+ def execute_command(self, command):
504
+ self.send_message(command)
505
+
506
+ def run(self):
507
+ print_logo()
508
+ print()
509
+ print_info(f"Project: {self.project_path}")
510
+ print_info(f"Model: {self.current_model}")
511
+
512
+ try:
513
+ files = list(self.project_path.rglob("*"))
514
+ files = [f for f in files if f.is_file()
515
+ and not any(ex in f.parts for ex in self.file_manager.excluded_dirs)]
516
+ visible = [f for f in files if not f.name.startswith('.')]
517
+ if visible:
518
+ print_info(f"\nFound {len(visible)} files (AI sees all)")
519
+ except:
520
+ pass
521
+
522
+ print_info("/help for commands\n")
523
+
524
+ while True:
525
+ try:
526
+ self.reset_interrupt()
527
+ user_input = input("> ").strip()
528
+ if user_input:
529
+ self.send_message(user_input)
530
+ except KeyboardInterrupt:
531
+ continue
532
+ except EOFError:
533
+ break
handycode/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ """
2
+ Интерфейс командной строки для HandyCode
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from handycode.assistant import HandyCode
11
+ from handycode.config import Config
12
+ from handycode.utils import print_error, print_warning, print_info, print_logo, print_success
13
+
14
+
15
+ def create_parser() -> argparse.ArgumentParser:
16
+ """Создаёт парсер аргументов"""
17
+ parser = argparse.ArgumentParser(
18
+ prog='handycode',
19
+ description='HandyCode - AI Assistant for coding',
20
+ formatter_class=argparse.RawDescriptionHelpFormatter,
21
+ epilog="""
22
+ Examples:
23
+ hc Interactive mode
24
+ hc -p /path/to/project Open project
25
+ hc -c "Create React app" Run command
26
+ hc -m gpt4 Use model
27
+ hc -y -c "Create Python API" Auto-approve
28
+ """
29
+ )
30
+
31
+ parser.add_argument('-p', '--project', type=str, help='Project directory')
32
+ parser.add_argument('-m', '--model', type=str, default='deepseek', help='AI model')
33
+ parser.add_argument('-y', '--yes', action='store_true', help='Auto-approve')
34
+ parser.add_argument('-c', '--command', type=str, help='Execute command and exit')
35
+ parser.add_argument('--version', action='version', version='HandyCode v2.0.0')
36
+
37
+ return parser
38
+
39
+
40
+ def main():
41
+ """Главная функция CLI"""
42
+ parser = create_parser()
43
+ args = parser.parse_args()
44
+
45
+ config = Config()
46
+
47
+ project_dir = args.project if args.project else os.getcwd()
48
+ project_path = Path(project_dir).resolve()
49
+
50
+ if not validate_project_dir(project_path):
51
+ sys.exit(1)
52
+
53
+ try:
54
+ assistant = HandyCode(
55
+ project_path=project_path,
56
+ model=args.model,
57
+ auto_approve=args.yes,
58
+ config=config
59
+ )
60
+
61
+ if args.command:
62
+ assistant.execute_command(args.command)
63
+ else:
64
+ assistant.run()
65
+ except KeyboardInterrupt:
66
+ print("\n\nGoodbye!")
67
+ sys.exit(0)
68
+ except Exception as e:
69
+ print_error(f"\nError: {e}")
70
+ sys.exit(1)
71
+
72
+
73
+ def validate_project_dir(project_path: Path) -> bool:
74
+ """Проверяет директорию проекта"""
75
+ if not project_path.exists():
76
+ print_warning(f"Directory does not exist: {project_path}")
77
+ create = input("Create it? [Y/n]: ").strip().lower()
78
+ if create in ['', 'y', 'yes']:
79
+ project_path.mkdir(parents=True, exist_ok=True)
80
+ print_success(f"Created: {project_path}")
81
+ else:
82
+ return False
83
+
84
+ if not project_path.is_dir():
85
+ print_error(f"Not a directory: {project_path}")
86
+ return False
87
+
88
+ dangerous_paths = [
89
+ '/', '/etc', '/bin', '/sbin', '/usr', '/System',
90
+ '/Windows', '/boot', '/root', '/var', '/tmp'
91
+ ]
92
+
93
+ path_str = str(project_path)
94
+ is_dangerous = any(path_str == d or path_str.startswith(d + '/') for d in dangerous_paths)
95
+
96
+ if is_dangerous:
97
+ print_error(f"WARNING: System directory!")
98
+ print_error(f"Path: {project_path}")
99
+ confirm = input("Continue? Type 'YES': ").strip()
100
+ if confirm != 'YES':
101
+ return False
102
+
103
+ print(f"\nProject directory: {project_path}")
104
+
105
+ home = Path.home()
106
+ if project_path == home:
107
+ print_warning("WARNING: This is your HOME directory!")
108
+ confirm = input("Continue? [y/N]: ").strip().lower()
109
+ if confirm not in ['y', 'yes']:
110
+ return False
111
+
112
+ contents = list(project_path.iterdir())
113
+ if contents:
114
+ visible = [c for c in contents if not c.name.startswith('.')]
115
+ if visible:
116
+ print(f"\nDirectory contains {len(visible)} items:")
117
+ for item in visible[:10]:
118
+ item_type = "[DIR]" if item.is_dir() else "[FILE]"
119
+ print(f" {item_type} {item.name}")
120
+ if len(visible) > 10:
121
+ print(f" ... and {len(visible) - 10} more")
122
+ print_warning("\nDirectory is not empty!")
123
+
124
+ confirm = input("\nUse this directory? [Y/n]: ").strip().lower()
125
+ if confirm not in ['', 'y', 'yes']:
126
+ print_warning("Cancelled")
127
+ return False
128
+
129
+ print_success(f"Using: {project_path}\n")
130
+ return True