wrd 0.1.2__py2.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.
wrd/__main__.py ADDED
@@ -0,0 +1,999 @@
1
+ #!/usr/bin/env python3
2
+ """WRD (Word) - Python Package
3
+
4
+ A powerful workflow automation tool for developers, inspired by Claude Code workflow.
5
+
6
+ Features:
7
+ - Project management
8
+ - Documentation automation
9
+ - AI tools integration
10
+ - Workflow optimization
11
+ - Interactive shell
12
+ - Project templates
13
+ """
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import signal
19
+ import subprocess
20
+ import sys
21
+ import yaml
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+ from typing import Dict, List, Optional, Any, TypeVar, Union, Tuple
25
+
26
+ import typer
27
+ from rich.console import Console
28
+ from rich.logging import RichHandler
29
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
30
+ from rich.table import Table
31
+ from rich.prompt import Confirm, Prompt
32
+
33
+ from . import __version__
34
+ from .cli import WRDShell
35
+ from .template_manager import get_template_manager
36
+
37
+ # Configure logging
38
+ logging.basicConfig(
39
+ level=logging.INFO,
40
+ format="%(message)s",
41
+ datefmt="[%X]",
42
+ handlers=[
43
+ RichHandler(rich_tracebacks=True, markup=True, show_time=False, show_path=False),
44
+ logging.FileHandler(Path.home() / ".wrd" / "wrd.log")
45
+ ]
46
+ )
47
+ logger = logging.getLogger("wrd")
48
+ console = Console()
49
+
50
+ # Type variable for generic function return type
51
+ T = TypeVar("T")
52
+
53
+ # Initialize Typer app
54
+ app = typer.Typer(
55
+ name="wrd",
56
+ help="WRD (WRonai Development) - A powerful workflow tool for developers",
57
+ add_completion=False,
58
+ no_args_is_help=True,
59
+ rich_markup_mode="rich"
60
+ )
61
+
62
+
63
+ class WRDConfig:
64
+ """WRD Configuration Manager"""
65
+
66
+ def __init__(self):
67
+ self.home_dir = Path.home()
68
+ self.wrd_dir = self.home_dir / ".wrd"
69
+ self.config_file = self.wrd_dir / "config.yaml"
70
+ self.projects_dir = Path(os.getenv("WRD_PROJECTS_DIR", self.home_dir / "projects"))
71
+ self.templates_dir = self.wrd_dir / "templates"
72
+ self.user_templates_dir = self.wrd_dir / "user_templates"
73
+ self.cache_dir = self.wrd_dir / "cache"
74
+
75
+ self.ensure_directories()
76
+ self.config = self.load_config()
77
+
78
+ def ensure_directories(self) -> None:
79
+ """Ensure all required directories exist."""
80
+ for directory in [
81
+ self.wrd_dir,
82
+ self.projects_dir,
83
+ self.templates_dir,
84
+ self.user_templates_dir,
85
+ self.cache_dir,
86
+ ]:
87
+ directory.mkdir(parents=True, exist_ok=True)
88
+
89
+ def load_config(self) -> Dict[str, Any]:
90
+ """Load configuration from YAML file."""
91
+ if not self.config_file.exists():
92
+ return self.get_default_config()
93
+
94
+ try:
95
+ with open(self.config_file, "r") as f:
96
+ config = yaml.safe_load(f) or {}
97
+ return {**self.get_default_config(), **config}
98
+ except Exception as e:
99
+ logger.warning(f"Error loading config: {e}. Using default configuration.")
100
+ return self.get_default_config()
101
+
102
+ def save_config(self) -> None:
103
+ """Save configuration to YAML file."""
104
+ try:
105
+ with open(self.config_file, "w") as f:
106
+ yaml.dump(self.config, f, default_flow_style=False)
107
+ except Exception as e:
108
+ logger.error(f"Error saving config: {e}")
109
+
110
+ def get_default_config(self) -> Dict[str, Any]:
111
+ """Get default configuration."""
112
+ return {
113
+ "editor": os.getenv("EDITOR", "code"),
114
+ "default_language": "python",
115
+ "default_template": "python",
116
+ "templates": {},
117
+ "recent_projects": [],
118
+ "settings": {
119
+ "auto_update": True,
120
+ "check_updates": True,
121
+ "notifications": True,
122
+ },
123
+ }
124
+
125
+
126
+ class WRDConfig:
127
+ """WRD Configuration Manager"""
128
+
129
+ def __init__(self):
130
+ self.home_dir = Path.home()
131
+ self.wrd_dir = self.home_dir / ".wrd"
132
+ self.config_file = self.wrd_dir / "config.yaml"
133
+ self.projects_dir = Path(os.getenv("WRD_PROJECTS_DIR", self.home_dir / "projects"))
134
+ self.templates_dir = self.wrd_dir / "templates"
135
+ self.user_templates_dir = self.wrd_dir / "user_templates"
136
+ self.cache_dir = self.wrd_dir / "cache"
137
+
138
+ # Ensure all required directories exist
139
+ self.ensure_directories()
140
+ self.config = self.load_config()
141
+
142
+ def ensure_directories(self) -> None:
143
+ """Ensure all required directories exist."""
144
+ directories = [
145
+ self.wrd_dir,
146
+ self.projects_dir,
147
+ self.templates_dir,
148
+ self.user_templates_dir,
149
+ self.cache_dir
150
+ ]
151
+ for directory in directories:
152
+ directory.mkdir(parents=True, exist_ok=True)
153
+
154
+ def load_config(self) -> Dict[str, Any]:
155
+ """Load configuration from YAML file."""
156
+ if not self.config_file.exists():
157
+ return self.get_default_config()
158
+
159
+ try:
160
+ with open(self.config_file, 'r') as f:
161
+ config = yaml.safe_load(f) or {}
162
+ return {**self.get_default_config(), **config}
163
+ except Exception as e:
164
+ logger.error(f"Error loading configuration: {e}")
165
+ return self.get_default_config()
166
+
167
+ def save_config(self) -> None:
168
+ """Save configuration to YAML file."""
169
+ try:
170
+ with open(self.config_file, 'w') as f:
171
+ yaml.dump(self.config, f, default_flow_style=False)
172
+ except Exception as e:
173
+ logger.error(f"Error saving configuration: {e}")
174
+
175
+ def get_default_config(self) -> Dict[str, Any]:
176
+ """Return default configuration."""
177
+ return {
178
+ 'version': '1.0.0',
179
+ 'projects_dir': str(self.projects_dir),
180
+ 'editor': os.getenv('EDITOR', 'code'),
181
+ 'default_language': 'python',
182
+ 'default_template': 'python',
183
+ 'templates': {},
184
+ 'ai_tools': {
185
+ 'claude_code': {
186
+ 'enabled': True,
187
+ 'api_key': os.getenv('CLAUDE_API_KEY', '')
188
+ }
189
+ },
190
+ 'workflows': {
191
+ 'documentation_auto': True,
192
+ 'commit_auto_describe': True
193
+ },
194
+ 'recent_projects': [],
195
+ 'settings': {
196
+ 'auto_update': True,
197
+ 'check_updates': True,
198
+ 'notifications': True
199
+ }
200
+ }
201
+
202
+
203
+ class WRDProject:
204
+ """Klasa reprezentująca projekt WRD"""
205
+
206
+ def __init__(self, name: str, project_type: str = 'python'):
207
+ self.name = name
208
+ self.project_type = project_type
209
+ self.config = WRDConfig()
210
+ self.project_dir = self.config.projects_dir / name
211
+ self.claude_md_file = self.project_dir / 'CLAUDE.md'
212
+
213
+ def create(self, description: str = ""):
214
+ """Tworzenie nowego projektu"""
215
+ if self.project_dir.exists():
216
+ logger.warning(f"Projekt {self.name} już istnieje")
217
+ return False
218
+
219
+ # Tworzenie struktury katalogów
220
+ dirs = ['src', 'tests', 'docs', 'scripts', 'config']
221
+ for dir_name in dirs:
222
+ (self.project_dir / dir_name).mkdir(parents=True)
223
+
224
+ # Tworzenie plików bazowych
225
+ self._create_readme(description)
226
+ self._create_claude_md(description)
227
+ self._create_requirements_txt()
228
+ self._create_gitignore()
229
+
230
+ # Inicjalizacja git
231
+ subprocess.run(['git', 'init'], cwd=self.project_dir, capture_output=True)
232
+
233
+ logger.info(f"Projekt {self.name} został utworzony w {self.project_dir}")
234
+ return True
235
+
236
+ def _create_readme(self, description: str):
237
+ """Tworzenie README.md"""
238
+ readme_content = f"""# {self.name}
239
+
240
+ {description}
241
+
242
+ ## Opis projektu
243
+ Projekt typu: {self.project_type}
244
+ Utworzony: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
245
+
246
+ ## Instalacja
247
+ ```bash
248
+ cd {self.name}
249
+ python -m venv venv
250
+ source venv/bin/activate # Linux/Mac
251
+ pip install -r requirements.txt
252
+ ```
253
+
254
+ ## Użycie
255
+ ```bash
256
+ python src/main.py
257
+ ```
258
+
259
+ ## Rozwój
260
+ Ten projekt został utworzony z użyciem WRD (Word) - narzędzia do zarządzania projektami Claude Code.
261
+
262
+ ## Dokumentacja
263
+ Szczegółowa dokumentacja znajduje się w pliku CLAUDE.md
264
+ """
265
+
266
+ with open(self.project_dir / 'README.md', 'w') as f:
267
+ f.write(readme_content)
268
+
269
+ def _create_claude_md(self, description: str):
270
+ """Tworzenie CLAUDE.md dla Claude Code"""
271
+ claude_content = f"""# Claude Code Project: {self.name}
272
+
273
+ ## Przegląd projektu
274
+ - **Nazwa**: {self.name}
275
+ - **Typ**: {self.project_type}
276
+ - **Opis**: {description}
277
+ - **Utworzony**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
278
+ - **Narzędzie**: WRD (Word) v1.0
279
+
280
+ ## Środowisko deweloperskie
281
+ - **OS**: Fedora Linux
282
+ - **Python**: {sys.version.split()[0]}
283
+ - **Virtual Environment**: venv/
284
+ - **Narzędzia**: Claude Code, WRD
285
+
286
+ ## Struktura projektu
287
+ ```
288
+ {self.name}/
289
+ ├── src/ # Kod źródłowy
290
+ ├── tests/ # Testy
291
+ ├── docs/ # Dokumentacja
292
+ ├── scripts/ # Skrypty pomocnicze
293
+ ├── config/ # Konfiguracja
294
+ ├── venv/ # Środowisko wirtualne
295
+ ├── README.md # Podstawowa dokumentacja
296
+ ├── CLAUDE.md # Ten plik - dokumentacja dla Claude Code
297
+ ├── requirements.txt # Zależności Python
298
+ └── .gitignore # Git ignore
299
+ ```
300
+
301
+ ## Workflow z Claude Code
302
+ 1. **Planowanie** (Gemini 2.5 Pro/Claude.ai)
303
+ - Architektura rozwiązania
304
+ - Specyfikacja funkcjonalności
305
+ - Planowanie iteracji
306
+
307
+ 2. **Implementacja** (Claude Code)
308
+ - Kodowanie w 5-godzinnych sesjach
309
+ - Automatyczne commity z opisami
310
+ - Iteracyjny rozwój
311
+
312
+ 3. **Dokumentacja** (Automatyczna)
313
+ - WRD automatycznie dokumentuje proces
314
+ - Śledzenie błędów i rozwiązań
315
+ - Historia zmian
316
+
317
+ ## Postęp prac
318
+ ### {datetime.datetime.now().strftime('%Y-%m-%d')}
319
+ - ✅ Inicjalizacja projektu
320
+ - ✅ Struktura katalogów
321
+ - ⏳ Implementacja podstawowej funkcjonalności
322
+
323
+ ## Błędy i rozwiązania
324
+ <!-- Automatycznie aktualizowane przez WRD -->
325
+
326
+ ## Notatki techniczne
327
+ <!-- Miejsce na notatki specyficzne dla Claude Code -->
328
+
329
+ ## Optymalizacje wydajności
330
+ <!-- Dokumentacja optymalizacji -->
331
+
332
+ ## Deployment
333
+ <!-- Instrukcje wdrożenia -->
334
+
335
+ ---
336
+ *Dokumentacja generowana przez WRD (Word) - narzędzie workflow dla Claude Code*
337
+ """
338
+
339
+ with open(self.claude_md_file, 'w') as f:
340
+ f.write(claude_content)
341
+
342
+ def _create_requirements_txt(self):
343
+ """Tworzenie requirements.txt"""
344
+ requirements = [
345
+ "requests>=2.28.0",
346
+ "click>=8.0.0",
347
+ "rich>=12.0.0",
348
+ "pydantic>=1.9.0",
349
+ "python-dotenv>=0.19.0"
350
+ ]
351
+
352
+ if self.project_type == 'fastapi':
353
+ requirements.extend([
354
+ "fastapi>=0.95.0",
355
+ "uvicorn>=0.20.0"
356
+ ])
357
+ elif self.project_type == 'data':
358
+ requirements.extend([
359
+ "pandas>=1.5.0",
360
+ "numpy>=1.24.0",
361
+ "matplotlib>=3.6.0"
362
+ ])
363
+
364
+ with open(self.project_dir / 'requirements.txt', 'w') as f:
365
+ f.write('\n'.join(requirements))
366
+
367
+ def _create_gitignore(self):
368
+ """Tworzenie .gitignore"""
369
+ gitignore_content = """# WRD Generated .gitignore
370
+
371
+ # Python
372
+ __pycache__/
373
+ *.py[cod]
374
+ *$py.class
375
+ *.so
376
+ .Python
377
+ build/
378
+ develop-eggs/
379
+ dist/
380
+ downloads/
381
+ eggs/
382
+ .eggs/
383
+ lib/
384
+ lib64/
385
+ parts/
386
+ sdist/
387
+ var/
388
+ wheels/
389
+ *.egg-info/
390
+ .installed.cfg
391
+ *.egg
392
+
393
+ # Virtual Environment
394
+ venv/
395
+ env/
396
+ ENV/
397
+
398
+ # IDE
399
+ .vscode/
400
+ .idea/
401
+ *.swp
402
+ *.swo
403
+
404
+ # OS
405
+ .DS_Store
406
+ Thumbs.db
407
+
408
+ # Logs
409
+ *.log
410
+ logs/
411
+
412
+ # Environment variables
413
+ .env
414
+ .env.local
415
+
416
+
417
+
418
+ class WRDManager:
419
+ """Main class for managing WRD projects."""
420
+
421
+ def __init__(self, config: 'WRDConfig'):
422
+ self.config = config
423
+ self.projects: Dict[str, WRDProject] = {}
424
+ self.load_projects()
425
+
426
+ def load_projects(self) -> None:
427
+ """Load all projects from the projects directory."""
428
+ self.projects = {}
429
+ if not self.config.projects_dir.exists():
430
+ return
431
+
432
+ for project_dir in self.config.projects_dir.glob('*'):
433
+ if project_dir.is_dir() and not project_dir.name.startswith('.'):
434
+ try:
435
+ project = WRDProject(project_dir, self.config)
436
+ self.projects[project.name] = project
437
+ except Exception as e:
438
+ logger.warning(f"Error loading project {project_dir.name}: {e}")
439
+
440
+ def create_project(
441
+ self,
442
+ name: str,
443
+ project_type: str = 'python',
444
+ description: str = '',
445
+ template: Optional[str] = None,
446
+ **kwargs
447
+ ) -> 'WRDProject':
448
+ """Create a new project.
449
+
450
+ Args:
451
+ name: Name of the project
452
+ project_type: Type of project (e.g., 'python', 'web', 'data')
453
+ description: Project description
454
+ template: Optional template to use for project creation
455
+ **kwargs: Additional project metadata
456
+
457
+ Returns:
458
+ WRDProject: The created project
459
+
460
+ Raises:
461
+ ValueError: If project already exists or creation fails
462
+ """
463
+ project_path = self.config.projects_dir / name
464
+
465
+ if project_path.exists():
466
+ raise ValueError(f"Project '{name}' already exists")
467
+
468
+ try:
469
+ # Create project directory structure
470
+ project_path.mkdir(parents=True, exist_ok=True)
471
+
472
+ # Initialize project with metadata
473
+ project = WRDProject(project_path, self.config)
474
+ project._metadata.update({
475
+ 'name': name,
476
+ 'type': project_type,
477
+ 'description': description,
478
+ 'created_at': datetime.now().isoformat(),
479
+ 'status': 'active',
480
+ 'ai_tools': [],
481
+ 'workflows': {},
482
+ **kwargs
483
+ })
484
+
485
+ # Apply template if specified
486
+ if template:
487
+ self._apply_template(project, template)
488
+
489
+ # Initialize Git repository
490
+ self._init_git_repo(project_path)
491
+
492
+ # Save metadata and add to projects
493
+ project.save_metadata()
494
+ self.projects[name] = project
495
+
496
+ # Update recent projects
497
+ self._update_recent_projects(name)
498
+
499
+ return project
500
+
501
+ except Exception as e:
502
+ # Clean up on failure
503
+ if project_path.exists():
504
+ import shutil
505
+ shutil.rmtree(project_path, ignore_errors=True)
506
+ raise ValueError(f"Failed to create project: {e}")
507
+
508
+ def _apply_template(self, project: 'WRDProject', template_name: str) -> None:
509
+ """Apply a template to the project.
510
+
511
+ Args:
512
+ project: The project to apply the template to
513
+ template_name: Name of the template to apply
514
+
515
+ Raises:
516
+ ValueError: If template is not found or application fails
517
+ """
518
+ try:
519
+ template_manager = get_template_manager()
520
+ context = {
521
+ 'project_name': project.name,
522
+ 'project_path': str(project.path),
523
+ 'project_type': project._metadata.get('type', 'python'),
524
+ 'description': project._metadata.get('description', ''),
525
+ 'author': os.getenv('USER', 'Your Name'),
526
+ 'email': '',
527
+ 'year': datetime.now().year,
528
+ 'version': '0.1.0',
529
+ }
530
+
531
+ success = template_manager.create_project(
532
+ template_name=template_name,
533
+ project_path=project.path,
534
+ context=context,
535
+ overwrite=False
536
+ )
537
+
538
+ if not success:
539
+ raise ValueError(f"Failed to apply template: {template_name}")
540
+
541
+ except Exception as e:
542
+ logger.error(f"Error applying template: {e}")
543
+ if os.getenv("WRD_DEBUG"):
544
+ import traceback
545
+ logger.debug(traceback.format_exc())
546
+ raise
547
+
548
+ def _init_git_repo(self, path: Path) -> None:
549
+ """Initialize a Git repository for the project."""
550
+ try:
551
+ # Initialize repository
552
+ run_command(['git', 'init'], cwd=path)
553
+
554
+ # Configure basic Git settings
555
+ run_command(['git', 'config', 'pull.rebase', 'false'], cwd=path)
556
+
557
+ # Create basic .gitignore if it doesn't exist
558
+ gitignore = path / '.gitignore'
559
+ if not gitignore.exists():
560
+ gitignore_content = (
561
+ "# Python\n"
562
+ "__pycache__/\n"
563
+ "*.py[cod]\n"
564
+ "*$py.class\n"
565
+ "*.so\n"
566
+ ".Python\n"
567
+ "build/\n"
568
+ "develop-eggs/\n"
569
+ "dist/\n"
570
+ "downloads/\n"
571
+ "eggs/\n"
572
+ ".eggs/\n"
573
+ "lib\n"
574
+ "lib64\n"
575
+ "parts/\n"
576
+ "sdist/\n"
577
+ "var/\n"
578
+ "wheels/\n"
579
+ "*.egg-info/\n"
580
+ ".env\n"
581
+ ".venv\n"
582
+ "venv/\n"
583
+ "ENV/\n"
584
+ ".mypy_cache/\n"
585
+ ".pytest_cache/\n"
586
+ ".coverage\n"
587
+ "htmlcov/\n"
588
+ ".DS_Store\n"
589
+ "Thumbs.db\n"
590
+ )
591
+ with open(gitignore, 'w') as f:
592
+ f.write(gitignore_content)
593
+
594
+ # Make initial commit
595
+ run_command(['git', 'add', '.'], cwd=path)
596
+ run_command(
597
+ ['git', 'commit', '-m', 'Initial commit'],
598
+ cwd=path
599
+ )
600
+
601
+ except Exception as e:
602
+ logger.warning(f"Git repository initialization failed: {e}")
603
+
604
+ def _update_recent_projects(self, project_name: str) -> None:
605
+ """Update the list of recently used projects."""
606
+ recent = self.config.config.get('recent_projects', [])
607
+ if project_name in recent:
608
+ recent.remove(project_name)
609
+ recent.insert(0, project_name)
610
+ self.config.config['recent_projects'] = recent[:10] # Keep only 10 most recent
611
+ self.config.save_config()
612
+
613
+ def list_projects(self, status: Optional[str] = None) -> List['WRDProject']:
614
+ """List all projects with optional status filter."""
615
+ if status:
616
+ return [p for p in self.projects.values() if p.get_status() == status]
617
+ return list(self.projects.values())
618
+
619
+ def get_project(self, name: str) -> Optional['WRDProject']:
620
+ """Get a project by name."""
621
+ return self.projects.get(name)
622
+
623
+ def delete_project(self, name: str, force: bool = False) -> bool:
624
+ """Delete a project.
625
+
626
+ Args:
627
+ name: Name of the project to delete
628
+ force: If True, don't ask for confirmation
629
+
630
+ Returns:
631
+ bool: True if project was deleted, False otherwise
632
+ """
633
+ if name not in self.projects:
634
+ logger.error(f"Project '{name}' not found")
635
+ return False
636
+
637
+ project = self.projects[name]
638
+
639
+ if not force:
640
+ confirm = click.confirm(
641
+ f"Are you sure you want to delete project '{name}'? This cannot be undone.",
642
+ default=False
643
+ )
644
+ if not confirm:
645
+ return False
646
+
647
+ try:
648
+ if project.path.exists():
649
+ import shutil
650
+ shutil.rmtree(project.path, ignore_errors=True)
651
+
652
+ # Remove from projects list and recent projects
653
+ del self.projects[name]
654
+ self._remove_from_recent_projects(name)
655
+
656
+ logger.info(f"Project '{name}' deleted successfully")
657
+ return True
658
+
659
+ except Exception as e:
660
+ logger.error(f"Failed to delete project '{name}': {e}")
661
+ return False
662
+
663
+ def _remove_from_recent_projects(self, project_name: str) -> None:
664
+ """Remove a project from recent projects list."""
665
+ if 'recent_projects' in self.config.config:
666
+ recent = self.config.config['recent_projects']
667
+ if project_name in recent:
668
+ recent.remove(project_name)
669
+ self.config.save_config()
670
+
671
+ def auto_commit(self, message: Optional[str] = None) -> None:
672
+ """Automatically commit changes in all projects."""
673
+ for project in self.projects.values():
674
+ if (project.path / '.git').exists():
675
+ try:
676
+ # Check for changes
677
+ result = run_command(
678
+ ['git', 'status', '--porcelain'],
679
+ cwd=project.path,
680
+ capture_output=True
681
+ )
682
+
683
+ if result.stdout.strip():
684
+ # There are changes to commit
685
+ if not message:
686
+ message = f"Auto-commit: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
687
+
688
+ run_command(['git', 'add', '.'], cwd=project.path)
689
+ run_command(
690
+ ['git', 'commit', '-m', message],
691
+ cwd=project.path
692
+ )
693
+ logger.info(f"Committed changes in {project.name}")
694
+
695
+ except Exception as e:
696
+ logger.error(f"Failed to auto-commit in {project.name}: {e}")
697
+
698
+ def show_status(self) -> None:
699
+ """Show status of all projects."""
700
+ projects = self.list_projects()
701
+ if not projects:
702
+ console.print("[yellow]No projects found.[/]")
703
+ return
704
+
705
+ table = Table(title="WRD Projects")
706
+ table.add_column("Name", style="cyan")
707
+ table.add_column("Type")
708
+ table.add_column("Status")
709
+ table.add_column("Path")
710
+
711
+ for project in sorted(projects, key=lambda p: p.name):
712
+ project_dir = self.config.projects_dir / project.name
713
+
714
+ # Get Git status if available
715
+ git_status = ""
716
+ if (project_dir / '.git').exists():
717
+ try:
718
+ # Check if there are uncommitted changes
719
+ result = run_command(
720
+ ['git', 'status', '--porcelain'],
721
+ cwd=project_dir,
722
+ capture_output=True
723
+ )
724
+ if result.stdout.strip():
725
+ git_status = "[yellow]uncommitted[/]"
726
+ else:
727
+ git_status = "[green]clean[/]"
728
+ except Exception:
729
+ git_status = "[red]error[/]"
730
+
731
+ table.add_row(
732
+ project.name,
733
+ project._metadata.get('type', 'unknown'),
734
+ git_status or "[dim]no git[/]",
735
+ str(project.path)
736
+ )
737
+
738
+ console.print(table)
739
+
740
+
741
+ # Helper functions
742
+ def run_command(
743
+ cmd: Union[str, List[str]],
744
+ cwd: Optional[Union[str, Path]] = None,
745
+ capture_output: bool = False,
746
+ check: bool = True
747
+ ) -> subprocess.CompletedProcess:
748
+ """Run a shell command with proper error handling.
749
+
750
+ Args:
751
+ cmd: Command to run as string or list of arguments
752
+ cwd: Working directory for the command
753
+ capture_output: Whether to capture command output
754
+ check: Whether to raise an exception on non-zero exit code
755
+
756
+ Returns:
757
+ CompletedProcess object with command results
758
+ """
759
+ try:
760
+ if isinstance(cmd, str):
761
+ # Use shell=True for string commands
762
+ return subprocess.run(
763
+ cmd,
764
+ shell=True,
765
+ cwd=str(cwd) if cwd else None,
766
+ check=check,
767
+ capture_output=capture_output,
768
+ text=True,
769
+ encoding='utf-8',
770
+ errors='replace'
771
+ )
772
+ else:
773
+ # Use shell=False for list of arguments (safer)
774
+ return subprocess.run(
775
+ [str(arg) for arg in cmd],
776
+ shell=False,
777
+ cwd=str(cwd) if cwd else None,
778
+ check=check,
779
+ capture_output=capture_output,
780
+ text=True,
781
+ encoding='utf-8',
782
+ errors='replace'
783
+ )
784
+ except subprocess.CalledProcessError as e:
785
+ logger.error(f"Command failed with exit code {e.returncode}: {e.cmd}")
786
+ if e.stdout:
787
+ logger.debug(f"Command output:\n{e.stdout}")
788
+ if e.stderr:
789
+ logger.error(f"Command error:\n{e.stderr}")
790
+ raise
791
+ except FileNotFoundError as e:
792
+ logger.error(f"Command not found: {e.filename}")
793
+ raise
794
+ except Exception as e:
795
+ logger.error(f"Unexpected error running command: {e}")
796
+ raise
797
+
798
+
799
+ def handle_keyboard_interrupt() -> None:
800
+ """Handle keyboard interrupt gracefully."""
801
+ console.print("\n[red]Operation cancelled by user.[/]")
802
+ sys.exit(1)
803
+
804
+
805
+ # Register signal handler for graceful exit
806
+ signal.signal(signal.SIGINT, lambda *_: handle_keyboard_interrupt())
807
+
808
+
809
+ # CLI Commands
810
+ @app.command()
811
+ def shell():
812
+ """Start interactive WRD shell."""
813
+ try:
814
+ WRDShell().shell()
815
+ except Exception as e:
816
+ logger.error(f"Error in shell: {e}")
817
+ raise typer.Exit(1)
818
+
819
+
820
+ @app.command()
821
+ def init(
822
+ name: str = typer.Argument(..., help="Project name"),
823
+ path: Path = typer.Argument(
824
+ None,
825
+ exists=False,
826
+ file_okay=False,
827
+ dir_okay=True,
828
+ writable=True,
829
+ resolve_path=True,
830
+ help="Project path (default: current directory/name)",
831
+ ),
832
+ template: str = typer.Option(
833
+ None, "--template", "-t", help="Template to use for the project"
834
+ ),
835
+ description: str = typer.Option(
836
+ "", "--description", "-d", help="Project description"
837
+ ),
838
+ interactive: bool = typer.Option(
839
+ True, "--interactive/--no-interactive", "-i/-I", help="Interactive mode"
840
+ ),
841
+ ):
842
+ """Initialize a new project."""
843
+ try:
844
+ config = get_config()
845
+ template_manager = get_template_manager()
846
+
847
+ # Set default path if not provided
848
+ if path is None:
849
+ path = Path.cwd() / name
850
+
851
+ # List available templates if none specified
852
+ available_templates = template_manager.list_templates()
853
+ if not template and interactive:
854
+ if not available_templates:
855
+ console.print("[yellow]No templates found. Using default Python template.[/]")
856
+ template = "python"
857
+ else:
858
+ table = Table(title="Available Templates")
859
+ table.add_column("Name", style="cyan")
860
+ table.add_column("Description")
861
+
862
+ for tpl_name in available_templates:
863
+ tpl = template_manager.get_template(tpl_name)
864
+ desc = tpl['config'].get('description', 'No description')
865
+ table.add_row(tpl_name, desc)
866
+
867
+ console.print(table)
868
+ template = console.input("\n[bold]Choose a template[/] (default: python): ") or "python"
869
+
870
+ # Get project details interactively
871
+ if interactive:
872
+ name = console.input(f"Project name [[cyan]{name}[/]]: ") or name
873
+ path = Path(console.input(f"Project path [[cyan]{path}[/]]: ") or path)
874
+ description = console.input(f"Description [[cyan]{description or 'No description'}[/]]: ") or description or ""
875
+
876
+ # Create project context
877
+ context = {
878
+ "project_name": name,
879
+ "package_name": name.lower().replace("-", "_").replace(" ", "_"),
880
+ "description": description,
881
+ "author_name": os.getenv("USER", "Your Name"),
882
+ "author_email": "",
883
+ "license": "MIT",
884
+ "year": datetime.now().year,
885
+ }
886
+
887
+ # Create project
888
+ with Progress(
889
+ SpinnerColumn(),
890
+ TextColumn("[progress.description]{task.description}"),
891
+ transient=True,
892
+ ) as progress:
893
+ progress.add_task(description="Creating project...", total=None)
894
+ success = template_manager.create_project(
895
+ template_name=template,
896
+ project_path=path,
897
+ context=context,
898
+ overwrite=False,
899
+ )
900
+
901
+ if success:
902
+ console.print(f"\n[green]✓ Project created at {path}[/]")
903
+
904
+ # Open in editor if configured
905
+ editor = config.config.get("editor")
906
+ if editor and interactive and typer.confirm("Open project in editor?"):
907
+ run_command(f"{editor} {path}")
908
+ else:
909
+ console.print("[red]Failed to create project.[/]")
910
+ raise typer.Exit(1)
911
+
912
+ except Exception as e:
913
+ logger.error(f"Error initializing project: {e}")
914
+ raise typer.Exit(1)
915
+
916
+
917
+ @app.command()
918
+ def list_templates():
919
+ """List all available project templates."""
920
+ try:
921
+ template_manager = get_template_manager()
922
+ templates = template_manager.list_templates()
923
+
924
+ if not templates:
925
+ console.print("[yellow]No templates found.[/]")
926
+ return
927
+
928
+ table = Table(
929
+ title="Available Templates",
930
+ caption=f"Use 'wrd init --template <name>' to create a project with a template"
931
+ )
932
+ table.add_column("Name", style="cyan", no_wrap=True)
933
+ table.add_column("Description")
934
+ table.add_column("Type", no_wrap=True)
935
+
936
+ # Sort templates by name
937
+ for name in sorted(templates):
938
+ template = template_manager.get_template(name)
939
+ if not template:
940
+ continue
941
+
942
+ desc = template.get('config', {}).get('description', 'No description')
943
+
944
+ # Determine template type
945
+ template_path = str(template.get('path', ''))
946
+ if "site-packages" in template_path:
947
+ tpl_type = "Built-in"
948
+ elif str(Path.home()) in template_path:
949
+ tpl_type = "User"
950
+ else:
951
+ tpl_type = "System"
952
+
953
+ table.add_row(name, desc, tpl_type)
954
+
955
+ console.print(table)
956
+
957
+ except Exception as e:
958
+ logger.error(f"Error listing templates: {e}")
959
+ if os.getenv("WRD_DEBUG"):
960
+ import traceback
961
+ logger.debug(traceback.format_exc())
962
+ raise typer.Exit(1)
963
+
964
+
965
+ @app.command()
966
+ def config():
967
+ """Configure WRD settings."""
968
+ config = get_config()
969
+
970
+ console.print("[bold]Current Configuration:[/]")
971
+ console.print_json(json.dumps(config.config, indent=2, default=str))
972
+
973
+ if typer.confirm("\nDo you want to edit the configuration?"):
974
+ editor = os.getenv("EDITOR", "nano")
975
+ os.system(f"{editor} {config.config_file}")
976
+
977
+
978
+ @app.command()
979
+ def version():
980
+ """Show WRD version."""
981
+ console.print(f"WRD version: [bold cyan]{__version__}[/]")
982
+
983
+ success = manager.auto_commit(args.project, args.message or "")
984
+ if success:
985
+ print(f"✅ Commit wykonany dla projektu '{args.project}'")
986
+ else:
987
+ print(f"❌ Nie udało się wykonać commit dla projektu '{args.project}'")
988
+
989
+ elif args.command == 'backup':
990
+ success = manager.backup_projects()
991
+ if success:
992
+ print("✅ Backup projektów wykonany")
993
+ else:
994
+ print("❌ Nie udało się wykonać backup")
995
+
996
+ elif args.command == 'progress':
997
+
998
+ if __name__ == "__main__":
999
+ main()