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/__init__.py +39 -0
- wrd/__main__.py +999 -0
- wrd/__version__.py +7 -0
- wrd/cli.py +312 -0
- wrd/py.typed +0 -0
- wrd/template_manager.py +163 -0
- wrd/templates/__init__.py +4 -0
- wrd/templates/__pycache__/__init__.cpython-312.pyc +0 -0
- wrd/templates/python/README.md.j2 +33 -0
- wrd/templates/python/gitignore.j2 +51 -0
- wrd/templates/python/project.yml +39 -0
- wrd/templates/python/pyproject.toml.j2 +35 -0
- wrd-0.1.2.dist-info/METADATA +751 -0
- wrd-0.1.2.dist-info/RECORD +18 -0
- wrd-0.1.2.dist-info/WHEEL +6 -0
- wrd-0.1.2.dist-info/entry_points.txt +2 -0
- wrd-0.1.2.dist-info/licenses/LICENSE +201 -0
- wrd-0.1.2.dist-info/top_level.txt +1 -0
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()
|