djangx 1.5.5__py3-none-any.whl → 1.5.7__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.
- djangx/__init__.py +14 -6
- djangx/api/enums.py +8 -0
- djangx/api/settings/databases.py +9 -8
- djangx/enums.py +2 -0
- djangx/management/cli.py +14 -1
- djangx/management/commands/generate.py +213 -14
- djangx/management/commands/startproject.py +562 -115
- djangx/management/enums.py +63 -0
- djangx/management/settings/apps.py +41 -83
- djangx/management/urls.py +5 -1
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/METADATA +2 -1
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/RECORD +14 -13
- djangx/management/commands/generators/__init__.py +0 -1
- djangx/management/commands/generators/file.py +0 -217
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/WHEEL +0 -0
- {djangx-1.5.5.dist-info → djangx-1.5.7.dist-info}/entry_points.txt +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from enum import StrEnum
|
|
3
2
|
from pathlib import Path
|
|
3
|
+
from typing import Final
|
|
4
4
|
|
|
5
5
|
from christianwhocodes.core import ExitCode, Version
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
from rich.prompt import Confirm, Prompt
|
|
6
9
|
|
|
7
10
|
from ... import PKG_DISPLAY_NAME, PKG_NAME, PROJECT_DIR, PROJECT_INIT_NAME
|
|
11
|
+
from ...enums import DatabaseBackend, PresetType
|
|
8
12
|
|
|
9
13
|
__all__ = ["initialize"]
|
|
10
14
|
|
|
@@ -12,30 +16,34 @@ __all__ = ["initialize"]
|
|
|
12
16
|
# Configuration & Templates
|
|
13
17
|
# ============================================================================
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# VCS and common items that shouldn't prevent initialization
|
|
20
|
+
SAFE_DIRECTORY_ITEMS: Final[set[str]] = {
|
|
21
|
+
".git",
|
|
22
|
+
".gitignore",
|
|
23
|
+
".gitattributes",
|
|
24
|
+
".hg",
|
|
25
|
+
".hgignore",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"LICENSE.txt",
|
|
28
|
+
"LICENSE.md",
|
|
29
|
+
}
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
@dataclass(frozen=True)
|
|
24
33
|
class _ProjectDependencies:
|
|
25
34
|
"""Manages project dependencies."""
|
|
26
35
|
|
|
27
|
-
base: tuple[str, ...] = (
|
|
28
|
-
"pillow>=12.1.0",
|
|
29
|
-
"psycopg[binary,pool]>=3.3.2",
|
|
30
|
-
)
|
|
36
|
+
base: tuple[str, ...] = ("pillow>=12.1.0",)
|
|
31
37
|
dev: tuple[str, ...] = ("djlint>=1.36.4", "ruff>=0.15.0")
|
|
38
|
+
postgresql: tuple[str, ...] = ("psycopg[binary,pool]>=3.3.2",)
|
|
32
39
|
vercel: tuple[str, ...] = ("vercel>=0.3.8",)
|
|
33
40
|
|
|
34
|
-
def
|
|
35
|
-
"""Get dependencies for a specific
|
|
41
|
+
def get_for_config(self, preset: PresetType, database: DatabaseBackend) -> list[str]:
|
|
42
|
+
"""Get dependencies for a specific configuration.
|
|
36
43
|
|
|
37
44
|
Args:
|
|
38
45
|
preset: The preset type.
|
|
46
|
+
database: The database backend.
|
|
39
47
|
|
|
40
48
|
Returns:
|
|
41
49
|
List of dependency strings including base dependencies.
|
|
@@ -43,6 +51,11 @@ class _ProjectDependencies:
|
|
|
43
51
|
deps = list(self.base)
|
|
44
52
|
deps.append(f"{PKG_NAME}>={Version.get(PKG_NAME)[0]}")
|
|
45
53
|
|
|
54
|
+
# Add database-specific dependencies
|
|
55
|
+
if database == DatabaseBackend.POSTGRESQL:
|
|
56
|
+
deps.extend(self.postgresql)
|
|
57
|
+
|
|
58
|
+
# Add preset-specific dependencies
|
|
46
59
|
if preset == PresetType.VERCEL:
|
|
47
60
|
deps.extend(self.vercel)
|
|
48
61
|
|
|
@@ -55,7 +68,7 @@ class _TemplateManager:
|
|
|
55
68
|
@staticmethod
|
|
56
69
|
def gitignore() -> str:
|
|
57
70
|
"""Generate .gitignore content."""
|
|
58
|
-
return """
|
|
71
|
+
return f"""
|
|
59
72
|
# Python-generated files
|
|
60
73
|
__pycache__/
|
|
61
74
|
*.py[oc]
|
|
@@ -79,8 +92,8 @@ wheels/
|
|
|
79
92
|
# Environment variables files
|
|
80
93
|
/.env*
|
|
81
94
|
|
|
82
|
-
#
|
|
83
|
-
/db.
|
|
95
|
+
# {DatabaseBackend.SQLITE3.value.capitalize()} database file
|
|
96
|
+
/db.{DatabaseBackend.SQLITE3.value}
|
|
84
97
|
""".strip()
|
|
85
98
|
|
|
86
99
|
@staticmethod
|
|
@@ -98,12 +111,19 @@ A new project built with {PKG_DISPLAY_NAME}.
|
|
|
98
111
|
""".strip()
|
|
99
112
|
|
|
100
113
|
@staticmethod
|
|
101
|
-
def pyproject_toml(
|
|
114
|
+
def pyproject_toml(
|
|
115
|
+
preset: PresetType,
|
|
116
|
+
database: DatabaseBackend,
|
|
117
|
+
dependencies: list[str],
|
|
118
|
+
use_postgres_env_vars: bool = False,
|
|
119
|
+
) -> str:
|
|
102
120
|
"""Generate pyproject.toml content.
|
|
103
121
|
|
|
104
122
|
Args:
|
|
105
123
|
preset: The preset type.
|
|
124
|
+
database: The database backend.
|
|
106
125
|
dependencies: List of project dependencies.
|
|
126
|
+
use_postgres_env_vars: Whether to use env vars for PostgreSQL config.
|
|
107
127
|
|
|
108
128
|
Returns:
|
|
109
129
|
Formatted pyproject.toml content.
|
|
@@ -112,15 +132,25 @@ A new project built with {PKG_DISPLAY_NAME}.
|
|
|
112
132
|
deps_formatted = ",\n ".join(f'"{dep}"' for dep in dependencies)
|
|
113
133
|
dev_deps_formatted = ",\n ".join(f'"{dep}"' for dep in deps.dev)
|
|
114
134
|
|
|
115
|
-
#
|
|
135
|
+
# Build tool configuration based on preset and database
|
|
136
|
+
tool_config_parts: list[str] = []
|
|
137
|
+
|
|
138
|
+
# Database configuration
|
|
139
|
+
if database == DatabaseBackend.POSTGRESQL:
|
|
140
|
+
tool_config_parts.append(
|
|
141
|
+
f'db = {{ backend = "{DatabaseBackend.POSTGRESQL.value}", use-vars = {str(use_postgres_env_vars).lower()} }}'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Storage configuration (Vercel-specific)
|
|
145
|
+
if preset == PresetType.VERCEL:
|
|
146
|
+
tool_config_parts.append(
|
|
147
|
+
'storage = { backend = "vercel", blob-token = "keep-your-vercel-blob-token-secret-in-env" }'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Format the tool config section
|
|
116
151
|
tool_config = ""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
tool_config = (
|
|
120
|
-
'\nstorage = { backend = "vercel", blob-token = "your-vercel-blob-token" }\n'
|
|
121
|
-
)
|
|
122
|
-
case _:
|
|
123
|
-
pass
|
|
152
|
+
if tool_config_parts:
|
|
153
|
+
tool_config = "\n" + "\n".join(tool_config_parts) + "\n"
|
|
124
154
|
|
|
125
155
|
return f"""[project]
|
|
126
156
|
name = "{PROJECT_INIT_NAME}"
|
|
@@ -168,21 +198,47 @@ class HomeView(TemplateView):
|
|
|
168
198
|
template_name = "home/index.html"
|
|
169
199
|
"""
|
|
170
200
|
|
|
201
|
+
@staticmethod
|
|
202
|
+
def home_index_html() -> str:
|
|
203
|
+
"""Generate home app index.html content."""
|
|
204
|
+
return """{% extends "ui/index.html" %}
|
|
205
|
+
|
|
206
|
+
{% load org %}
|
|
207
|
+
|
|
208
|
+
{% block title %}
|
|
209
|
+
<title>Welcome - {% org "name" %} App</title>
|
|
210
|
+
{% endblock title %}
|
|
211
|
+
|
|
212
|
+
{% block fonts %}
|
|
213
|
+
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
214
|
+
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin />
|
|
215
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Raleway:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Mulish:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
|
216
|
+
rel="stylesheet" />
|
|
217
|
+
{% endblock fonts %}
|
|
218
|
+
|
|
219
|
+
{% block main %}
|
|
220
|
+
<main>
|
|
221
|
+
<section class="container-full py-8">
|
|
222
|
+
</section>
|
|
223
|
+
</main>
|
|
224
|
+
{% endblock main %}
|
|
225
|
+
"""
|
|
226
|
+
|
|
171
227
|
@staticmethod
|
|
172
228
|
def tailwind_css() -> str:
|
|
173
229
|
"""Generate Tailwind CSS content."""
|
|
174
|
-
return """@import "tailwindcss";
|
|
230
|
+
return f"""@import "tailwindcss";
|
|
175
231
|
|
|
176
232
|
/* =============================================================================
|
|
177
233
|
SOURCE FILES
|
|
178
234
|
============================================================================= */
|
|
179
|
-
@source "../../../../.venv/**/
|
|
235
|
+
@source "../../../../.venv/**/{PKG_NAME}/ui/templates/ui/**/*.html";
|
|
180
236
|
@source "../../../templates/home/**/*.html";
|
|
181
237
|
|
|
182
238
|
/* =============================================================================
|
|
183
239
|
THEME CONFIGURATION
|
|
184
240
|
============================================================================= */
|
|
185
|
-
@theme {
|
|
241
|
+
@theme {{
|
|
186
242
|
/* ---------------------------------------------------------------------------
|
|
187
243
|
TYPOGRAPHY
|
|
188
244
|
--------------------------------------------------------------------------- */
|
|
@@ -221,147 +277,229 @@ class HomeView(TemplateView):
|
|
|
221
277
|
--color-nav-dropdown-bg: #2e2e2e; /* Dropdown background */
|
|
222
278
|
--color-nav-dropdown: #d9d9d9; /* Dropdown text color */
|
|
223
279
|
--color-nav-dropdown-hover: #ff4d4f; /* Dropdown hover state */
|
|
224
|
-
}
|
|
280
|
+
}}
|
|
225
281
|
|
|
226
282
|
/* =============================================================================
|
|
227
283
|
LIGHT THEME OVERRIDES
|
|
228
284
|
============================================================================= */
|
|
229
|
-
@theme light {
|
|
285
|
+
@theme light {{
|
|
230
286
|
--color-background: rgba(41, 41, 41, 0.8);
|
|
231
287
|
--color-surface: #484848;
|
|
232
|
-
}
|
|
288
|
+
}}
|
|
233
289
|
|
|
234
290
|
/* =============================================================================
|
|
235
291
|
DARK THEME OVERRIDES
|
|
236
292
|
============================================================================= */
|
|
237
|
-
@theme dark {
|
|
293
|
+
@theme dark {{
|
|
238
294
|
--color-background: #060606;
|
|
239
295
|
--color-surface: #252525;
|
|
240
296
|
--color-default: #ffffff;
|
|
241
297
|
--color-heading: #ffffff;
|
|
242
|
-
}
|
|
298
|
+
}}
|
|
243
299
|
|
|
244
300
|
/* =============================================================================
|
|
245
301
|
UTILITY CLASSES
|
|
246
302
|
============================================================================= */
|
|
247
|
-
@layer utilities {
|
|
303
|
+
@layer utilities {{
|
|
248
304
|
/* Full-width container */
|
|
249
|
-
.container-full {
|
|
305
|
+
.container-full {{
|
|
250
306
|
@apply mx-auto w-full px-8;
|
|
251
|
-
}
|
|
307
|
+
}}
|
|
252
308
|
|
|
253
309
|
/* Responsive container (Mobile→SM→MD→LG→XL→2XL: 100%→92%→83%→80%→75%→1400px max) */
|
|
254
|
-
.container {
|
|
310
|
+
.container {{
|
|
255
311
|
@apply mx-auto w-full px-8 sm:w-11/12 sm:px-4 md:w-5/6 lg:w-4/5 xl:w-3/4 xl:px-0 2xl:max-w-[1400px];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
312
|
+
}}
|
|
313
|
+
}}
|
|
258
314
|
|
|
259
315
|
/* =============================================================================
|
|
260
316
|
BASE STYLES - Global element styling
|
|
261
317
|
============================================================================= */
|
|
262
|
-
@layer base {
|
|
263
|
-
:root {
|
|
318
|
+
@layer base {{
|
|
319
|
+
:root {{
|
|
264
320
|
@apply scroll-smooth;
|
|
265
|
-
}
|
|
321
|
+
}}
|
|
266
322
|
|
|
267
|
-
body {
|
|
323
|
+
body {{
|
|
268
324
|
@apply bg-background text-default font-default antialiased;
|
|
269
|
-
}
|
|
325
|
+
}}
|
|
270
326
|
|
|
271
327
|
h1,
|
|
272
328
|
h2,
|
|
273
329
|
h3,
|
|
274
330
|
h4,
|
|
275
331
|
h5,
|
|
276
|
-
h6 {
|
|
332
|
+
h6 {{
|
|
277
333
|
@apply text-heading font-heading text-balance;
|
|
278
|
-
}
|
|
334
|
+
}}
|
|
279
335
|
|
|
280
|
-
a {
|
|
336
|
+
a {{
|
|
281
337
|
@apply text-accent no-underline transition-colors duration-200 ease-in-out;
|
|
282
|
-
}
|
|
338
|
+
}}
|
|
283
339
|
|
|
284
|
-
a:hover {
|
|
340
|
+
a:hover {{
|
|
285
341
|
color: color-mix(in srgb, var(--color-accent), white 15%);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
342
|
+
}}
|
|
343
|
+
}}
|
|
288
344
|
"""
|
|
289
345
|
|
|
290
346
|
|
|
291
347
|
# ============================================================================
|
|
292
|
-
# File
|
|
348
|
+
# File Management
|
|
293
349
|
# ============================================================================
|
|
294
350
|
|
|
295
351
|
|
|
352
|
+
class _FileTracker:
|
|
353
|
+
"""Tracks files and directories created during initialization for rollback."""
|
|
354
|
+
|
|
355
|
+
def __init__(self):
|
|
356
|
+
"""Initialize the file tracker."""
|
|
357
|
+
self._created_paths: list[Path] = []
|
|
358
|
+
|
|
359
|
+
def track(self, path: Path) -> None:
|
|
360
|
+
"""Track a created file or directory.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
path: The path that was created.
|
|
364
|
+
"""
|
|
365
|
+
if path not in self._created_paths:
|
|
366
|
+
self._created_paths.append(path)
|
|
367
|
+
|
|
368
|
+
def cleanup_all(self) -> None:
|
|
369
|
+
"""Remove all tracked files and directories in reverse order of creation."""
|
|
370
|
+
from shutil import rmtree
|
|
371
|
+
|
|
372
|
+
# Reverse order to remove files before their parent directories
|
|
373
|
+
for path in reversed(self._created_paths):
|
|
374
|
+
try:
|
|
375
|
+
if path.exists():
|
|
376
|
+
if path.is_dir():
|
|
377
|
+
rmtree(path)
|
|
378
|
+
else:
|
|
379
|
+
path.unlink()
|
|
380
|
+
except Exception:
|
|
381
|
+
# Best effort cleanup - don't raise on cleanup failures
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
self._created_paths.clear()
|
|
385
|
+
|
|
386
|
+
|
|
296
387
|
class _ProjectFileWriter:
|
|
297
388
|
"""Handles file writing operations for project initialization."""
|
|
298
389
|
|
|
299
|
-
def __init__(self, project_dir: Path):
|
|
390
|
+
def __init__(self, project_dir: Path, console: Console, tracker: _FileTracker):
|
|
300
391
|
"""Initialize the file writer.
|
|
301
392
|
|
|
302
393
|
Args:
|
|
303
394
|
project_dir: The project directory path.
|
|
395
|
+
console: Rich console for output.
|
|
396
|
+
tracker: File tracker for rollback support.
|
|
304
397
|
"""
|
|
305
398
|
self.project_dir = project_dir
|
|
399
|
+
self.console = console
|
|
400
|
+
self.tracker = tracker
|
|
306
401
|
|
|
307
|
-
def
|
|
308
|
-
"""Write content to a file.
|
|
402
|
+
def write_if_not_exists(self, filename: str, content: str) -> bool:
|
|
403
|
+
"""Write content to a file only if it doesn't exist.
|
|
309
404
|
|
|
310
405
|
Args:
|
|
311
406
|
filename: Name of the file to create.
|
|
312
407
|
content: Content to write.
|
|
313
408
|
|
|
409
|
+
Returns:
|
|
410
|
+
True if file was created, False if it already existed.
|
|
411
|
+
|
|
314
412
|
Raises:
|
|
315
413
|
IOError: If file cannot be written.
|
|
316
414
|
PermissionError: If lacking permission to write.
|
|
317
415
|
"""
|
|
318
416
|
file_path = self.project_dir / filename
|
|
417
|
+
|
|
418
|
+
if file_path.exists():
|
|
419
|
+
return False
|
|
420
|
+
|
|
319
421
|
file_path.write_text(content.strip(), encoding="utf-8")
|
|
422
|
+
self.tracker.track(file_path)
|
|
423
|
+
return True
|
|
320
424
|
|
|
321
|
-
def
|
|
322
|
-
"""Write content to a specific path.
|
|
425
|
+
def write_to_path_if_not_exists(self, path: Path, content: str) -> bool:
|
|
426
|
+
"""Write content to a specific path only if it doesn't exist.
|
|
323
427
|
|
|
324
428
|
Args:
|
|
325
429
|
path: Full path to the file.
|
|
326
430
|
content: Content to write.
|
|
327
431
|
|
|
432
|
+
Returns:
|
|
433
|
+
True if file was created, False if it already existed.
|
|
434
|
+
|
|
328
435
|
Raises:
|
|
329
436
|
IOError: If file cannot be written.
|
|
330
437
|
PermissionError: If lacking permission to write.
|
|
331
438
|
"""
|
|
439
|
+
if path.exists():
|
|
440
|
+
return False
|
|
441
|
+
|
|
332
442
|
path.write_text(content.strip(), encoding="utf-8")
|
|
443
|
+
self.tracker.track(path)
|
|
444
|
+
return True
|
|
333
445
|
|
|
334
|
-
def ensure_dir(self, path: Path) ->
|
|
446
|
+
def ensure_dir(self, path: Path) -> bool:
|
|
335
447
|
"""Ensure a directory exists.
|
|
336
448
|
|
|
337
449
|
Args:
|
|
338
450
|
path: Directory path to create.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
True if directory was created, False if it already existed.
|
|
339
454
|
"""
|
|
455
|
+
if path.exists():
|
|
456
|
+
return False
|
|
457
|
+
|
|
340
458
|
path.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
self.tracker.track(path)
|
|
460
|
+
return True
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# ============================================================================
|
|
464
|
+
# Home App Creator
|
|
465
|
+
# ============================================================================
|
|
341
466
|
|
|
342
467
|
|
|
343
468
|
class _HomeAppCreator:
|
|
344
469
|
"""Creates the default 'home' Django application."""
|
|
345
470
|
|
|
346
|
-
def __init__(
|
|
471
|
+
def __init__(
|
|
472
|
+
self,
|
|
473
|
+
project_dir: Path,
|
|
474
|
+
writer: _ProjectFileWriter,
|
|
475
|
+
templates: _TemplateManager,
|
|
476
|
+
console: Console,
|
|
477
|
+
):
|
|
347
478
|
"""Initialize the home app creator.
|
|
348
479
|
|
|
349
480
|
Args:
|
|
350
481
|
project_dir: The project directory path.
|
|
351
482
|
writer: File writer instance.
|
|
352
483
|
templates: Template manager instance.
|
|
484
|
+
console: Rich console for output.
|
|
353
485
|
"""
|
|
354
486
|
self.project_dir = project_dir
|
|
355
487
|
self.writer = writer
|
|
356
488
|
self.templates = templates
|
|
489
|
+
self.console = console
|
|
357
490
|
self.home_dir = project_dir / "home"
|
|
358
491
|
|
|
359
492
|
def create(self) -> None:
|
|
360
493
|
"""Create the home app with all necessary files and directories."""
|
|
361
|
-
|
|
494
|
+
# Check if home app already exists
|
|
495
|
+
if self.home_dir.exists():
|
|
496
|
+
self.console.print(
|
|
497
|
+
"[yellow]Home app directory already exists, skipping app creation[/yellow]"
|
|
498
|
+
)
|
|
499
|
+
return
|
|
362
500
|
|
|
363
501
|
# Create the Django app structure
|
|
364
|
-
|
|
502
|
+
self._create_app_structure()
|
|
365
503
|
|
|
366
504
|
# Create app files
|
|
367
505
|
self._create_urls()
|
|
@@ -369,15 +507,36 @@ class _HomeAppCreator:
|
|
|
369
507
|
self._create_templates()
|
|
370
508
|
self._create_static_files()
|
|
371
509
|
|
|
510
|
+
def _create_app_structure(self) -> None:
|
|
511
|
+
"""Create the basic Django app structure using startapp command."""
|
|
512
|
+
from django.core.management import call_command
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
call_command("startapp", "home")
|
|
516
|
+
# Track the created directory
|
|
517
|
+
self.writer.tracker.track(self.home_dir)
|
|
518
|
+
except Exception as e:
|
|
519
|
+
raise IOError(f"Failed to create home app: {e}") from e
|
|
520
|
+
|
|
372
521
|
def _create_urls(self) -> None:
|
|
373
522
|
"""Create urls.py for the home app."""
|
|
374
523
|
urls_path = self.home_dir / "urls.py"
|
|
375
|
-
self.writer.
|
|
524
|
+
if self.writer.write_to_path_if_not_exists(urls_path, self.templates.home_urls()):
|
|
525
|
+
pass # File was created
|
|
526
|
+
else:
|
|
527
|
+
self.console.print("[dim]urls.py already exists, skipping[/dim]")
|
|
376
528
|
|
|
377
529
|
def _create_views(self) -> None:
|
|
378
530
|
"""Create views.py for the home app."""
|
|
379
531
|
views_path = self.home_dir / "views.py"
|
|
380
|
-
|
|
532
|
+
# Only overwrite if it's the default Django startapp content
|
|
533
|
+
if views_path.exists():
|
|
534
|
+
content = views_path.read_text(encoding="utf-8")
|
|
535
|
+
# Check if it's the default Django views.py (contains only imports or is minimal)
|
|
536
|
+
if len(content.strip()) < 100: # Default Django file is very short
|
|
537
|
+
views_path.write_text(self.templates.home_views().strip(), encoding="utf-8")
|
|
538
|
+
else:
|
|
539
|
+
self.writer.write_to_path_if_not_exists(views_path, self.templates.home_views())
|
|
381
540
|
|
|
382
541
|
def _create_templates(self) -> None:
|
|
383
542
|
"""Create template directory structure and files."""
|
|
@@ -385,7 +544,7 @@ class _HomeAppCreator:
|
|
|
385
544
|
self.writer.ensure_dir(templates_dir)
|
|
386
545
|
|
|
387
546
|
index_path = templates_dir / "index.html"
|
|
388
|
-
self.writer.
|
|
547
|
+
self.writer.write_to_path_if_not_exists(index_path, self.templates.home_index_html())
|
|
389
548
|
|
|
390
549
|
def _create_static_files(self) -> None:
|
|
391
550
|
"""Create static directory structure and CSS files."""
|
|
@@ -393,7 +552,7 @@ class _HomeAppCreator:
|
|
|
393
552
|
self.writer.ensure_dir(static_dir)
|
|
394
553
|
|
|
395
554
|
css_path = static_dir / "tailwind.css"
|
|
396
|
-
self.writer.
|
|
555
|
+
self.writer.write_to_path_if_not_exists(css_path, self.templates.tailwind_css())
|
|
397
556
|
|
|
398
557
|
|
|
399
558
|
# ============================================================================
|
|
@@ -415,6 +574,7 @@ class _ProjectInitializer:
|
|
|
415
574
|
project_dir: Path,
|
|
416
575
|
dependencies: _ProjectDependencies | None = None,
|
|
417
576
|
templates: _TemplateManager | None = None,
|
|
577
|
+
console: Console | None = None,
|
|
418
578
|
):
|
|
419
579
|
"""Initialize the project initializer.
|
|
420
580
|
|
|
@@ -422,22 +582,54 @@ class _ProjectInitializer:
|
|
|
422
582
|
project_dir: The directory where the project will be created.
|
|
423
583
|
dependencies: Dependency manager (uses default if None).
|
|
424
584
|
templates: Template manager (uses default if None).
|
|
585
|
+
console: Rich console for output (creates new if None).
|
|
425
586
|
"""
|
|
426
587
|
self.project_dir = Path(project_dir)
|
|
427
588
|
self.dependencies = dependencies or _ProjectDependencies()
|
|
428
589
|
self.templates = templates or _TemplateManager()
|
|
429
|
-
self.
|
|
590
|
+
self.console = console or Console()
|
|
591
|
+
self.tracker = _FileTracker()
|
|
592
|
+
self.writer = _ProjectFileWriter(self.project_dir, self.console, self.tracker)
|
|
430
593
|
|
|
431
|
-
def _validate_directory(self) -> None:
|
|
594
|
+
def _validate_directory(self, force: bool = False) -> None:
|
|
432
595
|
"""Validate that the directory is suitable for initialization.
|
|
433
596
|
|
|
597
|
+
Args:
|
|
598
|
+
force: Skip validation and proceed regardless.
|
|
599
|
+
|
|
434
600
|
Raises:
|
|
435
|
-
ProjectInitializationError: If directory
|
|
601
|
+
ProjectInitializationError: If directory contains non-VCS files and user declines.
|
|
436
602
|
"""
|
|
437
|
-
if
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
603
|
+
if force:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
existing_items = list(self.project_dir.iterdir())
|
|
607
|
+
if not existing_items:
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
# Filter out safe items
|
|
611
|
+
problematic_items = [
|
|
612
|
+
item for item in existing_items if item.name not in SAFE_DIRECTORY_ITEMS
|
|
613
|
+
]
|
|
614
|
+
|
|
615
|
+
if not problematic_items:
|
|
616
|
+
# Only safe items present - proceed without prompting
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
# Show what exists and ask for confirmation
|
|
620
|
+
items_list = "\n - ".join(item.name for item in problematic_items)
|
|
621
|
+
|
|
622
|
+
self.console.print(f"[yellow]Directory is not empty. Found:[/yellow]\n - {items_list}\n")
|
|
623
|
+
|
|
624
|
+
should_proceed = Confirm.ask(
|
|
625
|
+
f"Initialize {PKG_DISPLAY_NAME} project anyway? "
|
|
626
|
+
"This will skip existing files and create new ones",
|
|
627
|
+
default=False,
|
|
628
|
+
console=self.console,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
if not should_proceed:
|
|
632
|
+
raise ProjectInitializationError("Initialization cancelled by user.")
|
|
441
633
|
|
|
442
634
|
def _get_preset_choice(self, preset: str | None = None) -> PresetType:
|
|
443
635
|
"""Get the user's preset choice or validate the provided one.
|
|
@@ -451,8 +643,6 @@ class _ProjectInitializer:
|
|
|
451
643
|
Raises:
|
|
452
644
|
ValueError: If preset is invalid.
|
|
453
645
|
"""
|
|
454
|
-
from rich.prompt import Prompt
|
|
455
|
-
|
|
456
646
|
if preset:
|
|
457
647
|
try:
|
|
458
648
|
return PresetType(preset)
|
|
@@ -460,99 +650,340 @@ class _ProjectInitializer:
|
|
|
460
650
|
valid_presets = [p.value for p in PresetType]
|
|
461
651
|
raise ValueError(
|
|
462
652
|
f"Invalid preset '{preset}'. Must be one of: {', '.join(valid_presets)}"
|
|
463
|
-
)
|
|
653
|
+
) from None
|
|
464
654
|
|
|
465
655
|
choice = Prompt.ask(
|
|
466
656
|
"Choose a preset",
|
|
467
657
|
choices=[p.value for p in PresetType],
|
|
468
658
|
default=PresetType.DEFAULT.value,
|
|
659
|
+
console=self.console,
|
|
469
660
|
)
|
|
470
661
|
return PresetType(choice)
|
|
471
662
|
|
|
472
|
-
def
|
|
663
|
+
def _get_postgres_config_choice(self, preset: PresetType) -> bool:
|
|
664
|
+
"""Get whether to use environment variables for PostgreSQL.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
preset: The preset type (Vercel always uses env vars).
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
True if using environment variables, False for pg_service/pgpass files.
|
|
671
|
+
"""
|
|
672
|
+
# Vercel requires environment variables (no filesystem access)
|
|
673
|
+
if preset == PresetType.VERCEL:
|
|
674
|
+
return True
|
|
675
|
+
|
|
676
|
+
# For default preset, let user choose
|
|
677
|
+
self.console.print("\n[bold]PostgreSQL Configuration Method:[/bold]")
|
|
678
|
+
self.console.print(" • [cyan]Environment variables[/cyan]: Store credentials in .env file")
|
|
679
|
+
self.console.print(
|
|
680
|
+
" • [cyan]PostgreSQL service files[/cyan]: Use pg_service.conf and .pgpass"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
use_env_vars = Confirm.ask(
|
|
684
|
+
"Use environment variables for PostgreSQL configuration?",
|
|
685
|
+
default=True,
|
|
686
|
+
console=self.console,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
return use_env_vars
|
|
690
|
+
|
|
691
|
+
def _get_database_choice(
|
|
692
|
+
self, preset: PresetType, database: str | None = None
|
|
693
|
+
) -> tuple[DatabaseBackend, bool]:
|
|
694
|
+
"""Get the user's database choice and PostgreSQL config method.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
preset: The preset type (Vercel requires PostgreSQL).
|
|
698
|
+
database: Optional database to use without prompting.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Tuple of (database backend, use_env_vars_for_postgres).
|
|
702
|
+
|
|
703
|
+
Raises:
|
|
704
|
+
ValueError: If database is invalid or incompatible with preset.
|
|
705
|
+
"""
|
|
706
|
+
# Vercel preset requires PostgreSQL (no file system access on Vercel)
|
|
707
|
+
if preset == PresetType.VERCEL:
|
|
708
|
+
if database and database != DatabaseBackend.POSTGRESQL:
|
|
709
|
+
raise ValueError(f"Vercel preset requires PostgreSQL database (got: {database})")
|
|
710
|
+
return DatabaseBackend.POSTGRESQL, True # Always use env vars with Vercel
|
|
711
|
+
|
|
712
|
+
# If database explicitly provided, validate it
|
|
713
|
+
if database:
|
|
714
|
+
try:
|
|
715
|
+
db_backend = DatabaseBackend(database)
|
|
716
|
+
except ValueError:
|
|
717
|
+
valid_databases = [db.value for db in DatabaseBackend]
|
|
718
|
+
raise ValueError(
|
|
719
|
+
f"Invalid database '{database}'. Must be one of: {', '.join(valid_databases)}"
|
|
720
|
+
) from None
|
|
721
|
+
|
|
722
|
+
# Ask about PostgreSQL config method if applicable
|
|
723
|
+
use_env_vars = (
|
|
724
|
+
self._get_postgres_config_choice(preset)
|
|
725
|
+
if db_backend == DatabaseBackend.POSTGRESQL
|
|
726
|
+
else False
|
|
727
|
+
)
|
|
728
|
+
return db_backend, use_env_vars
|
|
729
|
+
|
|
730
|
+
# Interactive prompt for default preset
|
|
731
|
+
choice = Prompt.ask(
|
|
732
|
+
"Choose a database",
|
|
733
|
+
choices=[db.value for db in DatabaseBackend],
|
|
734
|
+
default=DatabaseBackend.SQLITE3.value,
|
|
735
|
+
console=self.console,
|
|
736
|
+
)
|
|
737
|
+
db_backend = DatabaseBackend(choice)
|
|
738
|
+
|
|
739
|
+
# Ask about PostgreSQL config method if applicable
|
|
740
|
+
use_env_vars = (
|
|
741
|
+
self._get_postgres_config_choice(preset)
|
|
742
|
+
if db_backend == DatabaseBackend.POSTGRESQL
|
|
743
|
+
else False
|
|
744
|
+
)
|
|
745
|
+
return db_backend, use_env_vars
|
|
746
|
+
|
|
747
|
+
def _create_core_files(
|
|
748
|
+
self, preset: PresetType, database: DatabaseBackend, use_postgres_env_vars: bool
|
|
749
|
+
) -> None:
|
|
473
750
|
"""Create core project configuration files.
|
|
474
751
|
|
|
475
752
|
Args:
|
|
476
753
|
preset: The preset type to use.
|
|
754
|
+
database: The database backend to use.
|
|
755
|
+
use_postgres_env_vars: Whether to use env vars for PostgreSQL config.
|
|
477
756
|
|
|
478
757
|
Raises:
|
|
479
758
|
IOError: If files cannot be created.
|
|
480
759
|
"""
|
|
481
|
-
dependencies = self.dependencies.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
760
|
+
dependencies = self.dependencies.get_for_config(preset, database)
|
|
761
|
+
|
|
762
|
+
# Create files only if they don't exist
|
|
763
|
+
files_to_create = {
|
|
764
|
+
"pyproject.toml": self.templates.pyproject_toml(
|
|
765
|
+
preset, database, dependencies, use_postgres_env_vars
|
|
766
|
+
),
|
|
767
|
+
".gitignore": self.templates.gitignore(),
|
|
768
|
+
"README.md": self.templates.readme(),
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
for filename, content in files_to_create.items():
|
|
772
|
+
if self.writer.write_if_not_exists(filename, content):
|
|
773
|
+
pass # File was created
|
|
774
|
+
else:
|
|
775
|
+
self.console.print(f"[dim]{filename} already exists, skipping[/dim]")
|
|
776
|
+
|
|
777
|
+
def _configure_preset_files_and_env_example(self, preset: PresetType) -> None:
|
|
488
778
|
"""Configure files based on the chosen preset.
|
|
489
779
|
|
|
490
780
|
Args:
|
|
491
781
|
preset: The preset configuration to apply.
|
|
782
|
+
|
|
783
|
+
Raises:
|
|
784
|
+
IOError: If preset-specific file generation fails.
|
|
492
785
|
"""
|
|
493
|
-
|
|
494
|
-
# Import here to avoid circular dependencies
|
|
495
|
-
from .generators import ServerFileGenerator, VercelFileGenerator
|
|
786
|
+
from subprocess import CalledProcessError, run
|
|
496
787
|
|
|
497
|
-
|
|
498
|
-
|
|
788
|
+
try:
|
|
789
|
+
match preset:
|
|
790
|
+
case PresetType.VERCEL:
|
|
791
|
+
# Only generate vercel.json if it doesn't exist
|
|
792
|
+
if not (self.project_dir / "vercel.json").exists():
|
|
793
|
+
run(
|
|
794
|
+
[PKG_NAME, "generate", "-f", "vercel", "-y"],
|
|
795
|
+
cwd=self.project_dir,
|
|
796
|
+
check=True,
|
|
797
|
+
capture_output=True,
|
|
798
|
+
)
|
|
799
|
+
# Track the created file
|
|
800
|
+
self.tracker.track(self.project_dir / "vercel.json")
|
|
801
|
+
else:
|
|
802
|
+
self.console.print("[dim]vercel.json already exists, skipping[/dim]")
|
|
803
|
+
|
|
804
|
+
# Only generate api/server.py if it doesn't exist
|
|
805
|
+
server_path = self.project_dir / "api" / "server.py"
|
|
806
|
+
if not server_path.exists():
|
|
807
|
+
run(
|
|
808
|
+
[PKG_NAME, "generate", "-f", "server", "-y"],
|
|
809
|
+
cwd=self.project_dir,
|
|
810
|
+
check=True,
|
|
811
|
+
capture_output=True,
|
|
812
|
+
)
|
|
813
|
+
# Track the created files
|
|
814
|
+
self.tracker.track(self.project_dir / "api")
|
|
815
|
+
else:
|
|
816
|
+
self.console.print("[dim]api/server.py already exists, skipping[/dim]")
|
|
817
|
+
case _:
|
|
818
|
+
# Future presets will be added here
|
|
819
|
+
pass
|
|
820
|
+
|
|
821
|
+
except CalledProcessError as e:
|
|
822
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
823
|
+
raise IOError(f"Failed to generate preset-specific files: {error_msg}") from e
|
|
824
|
+
except FileNotFoundError as e:
|
|
825
|
+
raise IOError(
|
|
826
|
+
f"Command '{PKG_NAME}' not found. Ensure {PKG_DISPLAY_NAME} is properly installed."
|
|
827
|
+
) from e
|
|
828
|
+
|
|
829
|
+
# Only generate .env.example file if it doesn't exist
|
|
830
|
+
env_example_path = self.project_dir / ".env.example"
|
|
831
|
+
if not env_example_path.exists():
|
|
832
|
+
try:
|
|
833
|
+
run(
|
|
834
|
+
[PKG_NAME, "generate", "-f", "env", "-y"],
|
|
835
|
+
cwd=self.project_dir,
|
|
836
|
+
check=True,
|
|
837
|
+
capture_output=True,
|
|
838
|
+
)
|
|
839
|
+
# Track the created file
|
|
840
|
+
self.tracker.track(env_example_path)
|
|
841
|
+
except CalledProcessError as e:
|
|
842
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
843
|
+
raise IOError(f"Failed to generate .env file: {error_msg}") from e
|
|
844
|
+
except FileNotFoundError as e:
|
|
845
|
+
raise IOError(
|
|
846
|
+
f"Command '{PKG_NAME}' not found. Ensure {PKG_DISPLAY_NAME} is properly installed."
|
|
847
|
+
) from e
|
|
848
|
+
else:
|
|
849
|
+
self.console.print("[dim].env.example already exists, skipping[/dim]")
|
|
499
850
|
|
|
500
851
|
def _create_home_app(self) -> None:
|
|
501
852
|
"""Create the default home application."""
|
|
502
|
-
home_creator = _HomeAppCreator(self.project_dir, self.writer, self.templates)
|
|
853
|
+
home_creator = _HomeAppCreator(self.project_dir, self.writer, self.templates, self.console)
|
|
503
854
|
home_creator.create()
|
|
504
855
|
|
|
505
|
-
def
|
|
856
|
+
def _show_next_steps(
|
|
857
|
+
self, preset: PresetType, database: DatabaseBackend, use_postgres_env_vars: bool
|
|
858
|
+
) -> None:
|
|
859
|
+
"""Display next steps for the user after successful initialization.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
preset: The preset that was used.
|
|
863
|
+
database: The database backend that was chosen.
|
|
864
|
+
use_postgres_env_vars: Whether using env vars for PostgreSQL.
|
|
865
|
+
"""
|
|
866
|
+
from rich.panel import Panel
|
|
867
|
+
|
|
868
|
+
next_steps = [
|
|
869
|
+
"1. Install dependencies: [bold cyan]uv sync[/bold cyan]",
|
|
870
|
+
"2. Copy [bold].env.example[/bold] to [bold].env[/bold] and configure",
|
|
871
|
+
]
|
|
872
|
+
|
|
873
|
+
step_num = 3
|
|
874
|
+
|
|
875
|
+
# Database-specific instructions
|
|
876
|
+
if database == DatabaseBackend.POSTGRESQL:
|
|
877
|
+
if use_postgres_env_vars:
|
|
878
|
+
next_steps.append(
|
|
879
|
+
f"{step_num}. Configure PostgreSQL connection in [bold].env[/bold]"
|
|
880
|
+
)
|
|
881
|
+
else:
|
|
882
|
+
next_steps.append(
|
|
883
|
+
f"{step_num}. Configure PostgreSQL connection using [bold]pg_service.conf[/bold] and [bold].pgpass[/bold] files"
|
|
884
|
+
)
|
|
885
|
+
step_num += 1
|
|
886
|
+
|
|
887
|
+
# Preset-specific instructions
|
|
888
|
+
if preset == PresetType.VERCEL:
|
|
889
|
+
next_steps.append(f"{step_num}. Configure Vercel blob token in [bold].env[/bold]")
|
|
890
|
+
step_num += 1
|
|
891
|
+
|
|
892
|
+
next_steps.append(
|
|
893
|
+
f"{step_num}. Run development server: [bold cyan]uv run djx runserver[/bold cyan]"
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
panel = Panel(
|
|
897
|
+
"\n".join(next_steps),
|
|
898
|
+
title=f"[bold green]✓ {PKG_DISPLAY_NAME} project initialized successfully![/bold green]",
|
|
899
|
+
border_style="green",
|
|
900
|
+
padding=(1, 2),
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
self.console.print("\n")
|
|
904
|
+
self.console.print(panel)
|
|
905
|
+
|
|
906
|
+
def create(
|
|
907
|
+
self, preset: str | None = None, database: str | None = None, force: bool = False
|
|
908
|
+
) -> ExitCode:
|
|
506
909
|
"""Execute the full project initialization workflow.
|
|
507
910
|
|
|
508
911
|
Args:
|
|
509
912
|
preset: Optional preset to use without prompting.
|
|
913
|
+
database: Optional database backend to use without prompting.
|
|
914
|
+
force: Skip directory validation.
|
|
510
915
|
|
|
511
916
|
Returns:
|
|
512
917
|
ExitCode indicating success or failure.
|
|
513
918
|
"""
|
|
514
|
-
from christianwhocodes.io import Text, print
|
|
515
|
-
|
|
516
919
|
try:
|
|
517
|
-
# Validate directory is empty
|
|
518
|
-
self._validate_directory()
|
|
920
|
+
# Validate directory is empty or acceptable
|
|
921
|
+
self._validate_directory(force=force)
|
|
519
922
|
|
|
520
923
|
# Get and validate preset choice
|
|
521
924
|
chosen_preset = self._get_preset_choice(preset)
|
|
522
925
|
|
|
523
|
-
#
|
|
524
|
-
self.
|
|
525
|
-
|
|
526
|
-
# Configure preset-specific files
|
|
527
|
-
self._configure_preset_files(chosen_preset)
|
|
528
|
-
|
|
529
|
-
# Create default app
|
|
530
|
-
self._create_home_app()
|
|
531
|
-
|
|
532
|
-
print(
|
|
533
|
-
f"✓ {PKG_DISPLAY_NAME} project initialized successfully!",
|
|
534
|
-
Text.SUCCESS,
|
|
926
|
+
# Get and validate database choice
|
|
927
|
+
chosen_database, use_postgres_env_vars = self._get_database_choice(
|
|
928
|
+
chosen_preset, database
|
|
535
929
|
)
|
|
930
|
+
|
|
931
|
+
with Progress(
|
|
932
|
+
SpinnerColumn(),
|
|
933
|
+
TextColumn("[progress.description]{task.description}"),
|
|
934
|
+
console=self.console,
|
|
935
|
+
transient=True,
|
|
936
|
+
) as progress:
|
|
937
|
+
# Create core configuration files
|
|
938
|
+
task = progress.add_task("Creating project files...", total=None)
|
|
939
|
+
self._create_core_files(chosen_preset, chosen_database, use_postgres_env_vars)
|
|
940
|
+
progress.update(task, completed=True)
|
|
941
|
+
|
|
942
|
+
# Configure preset-specific files
|
|
943
|
+
task = progress.add_task("Configuring preset files...", total=None)
|
|
944
|
+
self._configure_preset_files_and_env_example(chosen_preset)
|
|
945
|
+
progress.update(task, completed=True)
|
|
946
|
+
|
|
947
|
+
# Create default app
|
|
948
|
+
task = progress.add_task("Creating home app...", total=None)
|
|
949
|
+
self._create_home_app()
|
|
950
|
+
progress.update(task, completed=True)
|
|
951
|
+
|
|
952
|
+
self._show_next_steps(chosen_preset, chosen_database, use_postgres_env_vars)
|
|
536
953
|
return ExitCode.SUCCESS
|
|
537
954
|
|
|
538
955
|
except KeyboardInterrupt:
|
|
539
|
-
|
|
956
|
+
self.tracker.cleanup_all()
|
|
957
|
+
self.console.print(
|
|
958
|
+
"\n[yellow]Project initialization cancelled. Cleaned up partial files.[/yellow]"
|
|
959
|
+
)
|
|
540
960
|
return ExitCode.ERROR
|
|
541
961
|
|
|
542
962
|
except ProjectInitializationError as e:
|
|
543
|
-
|
|
963
|
+
# User declined to proceed - no cleanup needed since nothing was created
|
|
964
|
+
self.console.print(f"[yellow]{e}[/yellow]")
|
|
544
965
|
return ExitCode.ERROR
|
|
545
966
|
|
|
546
967
|
except ValueError as e:
|
|
547
|
-
|
|
968
|
+
self.tracker.cleanup_all()
|
|
969
|
+
self.console.print(
|
|
970
|
+
f"[red]Configuration error:[/red] {e}\n[yellow]Cleaned up partial files.[/yellow]"
|
|
971
|
+
)
|
|
548
972
|
return ExitCode.ERROR
|
|
549
973
|
|
|
550
974
|
except (IOError, PermissionError) as e:
|
|
551
|
-
|
|
975
|
+
self.tracker.cleanup_all()
|
|
976
|
+
self.console.print(
|
|
977
|
+
f"[red]File system error:[/red] {e}\n[yellow]Cleaned up partial files.[/yellow]"
|
|
978
|
+
)
|
|
552
979
|
return ExitCode.ERROR
|
|
553
980
|
|
|
554
981
|
except Exception as e:
|
|
555
|
-
|
|
982
|
+
self.tracker.cleanup_all()
|
|
983
|
+
self.console.print(
|
|
984
|
+
f"[red]Unexpected error during initialization:[/red] {e}\n"
|
|
985
|
+
f"[yellow]Cleaned up partial files.[/yellow]"
|
|
986
|
+
)
|
|
556
987
|
return ExitCode.ERROR
|
|
557
988
|
|
|
558
989
|
|
|
@@ -561,13 +992,29 @@ class _ProjectInitializer:
|
|
|
561
992
|
# ============================================================================
|
|
562
993
|
|
|
563
994
|
|
|
564
|
-
def initialize(
|
|
565
|
-
|
|
995
|
+
def initialize(
|
|
996
|
+
preset: str | None = None, database: str | None = None, force: bool = False
|
|
997
|
+
) -> ExitCode:
|
|
998
|
+
f"""Main entry point for project initialization.
|
|
999
|
+
|
|
1000
|
+
Creates a new project with the specified preset and database configuration.
|
|
566
1001
|
|
|
567
1002
|
Args:
|
|
568
1003
|
preset: Optional preset to use without prompting.
|
|
1004
|
+
Available presets: 'default', 'vercel'.
|
|
1005
|
+
database: Optional database backend to use without prompting.
|
|
1006
|
+
Available databases: '{DatabaseBackend.SQLITE3.value}', '{DatabaseBackend.POSTGRESQL.value}'.
|
|
1007
|
+
Note: Vercel preset requires PostgreSQL.
|
|
1008
|
+
force: Skip directory validation and proceed even if directory is not empty.
|
|
569
1009
|
|
|
570
1010
|
Returns:
|
|
571
|
-
ExitCode
|
|
1011
|
+
ExitCode.SUCCESS if initialization completed successfully,
|
|
1012
|
+
ExitCode.ERROR otherwise.
|
|
1013
|
+
|
|
1014
|
+
Example:
|
|
1015
|
+
>>> initialize(preset="vercel") # Will auto-select {DatabaseBackend.POSTGRESQL.value} due to Vercel preset requirement
|
|
1016
|
+
>>> initialize(database="{DatabaseBackend.POSTGRESQL.value}") # Default preset with {DatabaseBackend.POSTGRESQL.value}
|
|
1017
|
+
>>> initialize(preset="default", database="{DatabaseBackend.SQLITE3.value}") # Default preset with {DatabaseBackend.SQLITE3.value}
|
|
1018
|
+
>>> initialize(force=True)
|
|
572
1019
|
"""
|
|
573
|
-
return _ProjectInitializer(PROJECT_DIR).create(preset=preset)
|
|
1020
|
+
return _ProjectInitializer(PROJECT_DIR).create(preset=preset, database=database, force=force)
|