coregen-cli 3.1.6__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.
- coregen.py +1585 -0
- coregen_cli-3.1.6.dist-info/METADATA +1211 -0
- coregen_cli-3.1.6.dist-info/RECORD +172 -0
- coregen_cli-3.1.6.dist-info/WHEEL +5 -0
- coregen_cli-3.1.6.dist-info/entry_points.txt +2 -0
- coregen_cli-3.1.6.dist-info/top_level.txt +2 -0
- generators/__init__.py +1 -0
- generators/assets/__init__.py +1 -0
- generators/assets/assets.py +30 -0
- generators/assets/main.py +4 -0
- generators/config/__init__.py +1 -0
- generators/config/analisy_options.py +42 -0
- generators/config/main.py +11 -0
- generators/config/pubspec.py +139 -0
- generators/cursor/__init__.py +3 -0
- generators/cursor/setup.py +87 -0
- generators/helpers/__init__.py +56 -0
- generators/helpers/component.py +1030 -0
- generators/helpers/config.py +255 -0
- generators/helpers/data_source.py +180 -0
- generators/helpers/domain.py +469 -0
- generators/helpers/feature.py +654 -0
- generators/helpers/navigation.py +274 -0
- generators/helpers/page.py +63 -0
- generators/helpers/project.py +33 -0
- generators/helpers/utils.py +338 -0
- generators/helpers/validation.py +364 -0
- generators/initializator.py +68 -0
- generators/main.py +81 -0
- generators/static/assets/logo.png +0 -0
- generators/static/assets/mock/auth_user.json +5 -0
- generators/static/assets/svgs/apple.svg +4 -0
- generators/static/assets/svgs/google.svg +2 -0
- generators/static/cursor/AGENTS.md.jinja +37 -0
- generators/static/cursor/agents/doc-writer.md +20 -0
- generators/static/cursor/agents/epic-orchestrator.md +28 -0
- generators/static/cursor/agents/feature-implementer.md +29 -0
- generators/static/cursor/agents/integration-wiring.md +30 -0
- generators/static/cursor/agents/layer-application.md +21 -0
- generators/static/cursor/agents/layer-guardian.md +21 -0
- generators/static/cursor/agents/layer-infrastructure.md +31 -0
- generators/static/cursor/agents/layer-model.md +24 -0
- generators/static/cursor/agents/layer-presentation.md +33 -0
- generators/static/cursor/docs/architecture/APIS_AND_INTEGRATION.md.jinja +42 -0
- generators/static/cursor/docs/architecture/CARAVAGGIO_COMPONENTS.md.jinja +317 -0
- generators/static/cursor/docs/architecture/DDD_LAYERS.md.jinja +32 -0
- generators/static/cursor/docs/architecture/FILE_TEMPLATES.md.jinja +76 -0
- generators/static/cursor/docs/architecture/MOCK_AND_REMOTE_DATA.md.jinja +87 -0
- generators/static/cursor/docs/architecture/REFERENCE_IMPLEMENTATIONS.md.jinja +48 -0
- generators/static/cursor/docs/architecture/WIDGETS_AND_CARAVAGGIO.md.jinja +76 -0
- generators/static/cursor/docs/epics/README.md +17 -0
- generators/static/cursor/lib/domain/AGENTS.md +23 -0
- generators/static/cursor/lib/features/AGENTS.md +23 -0
- generators/static/cursor/lib/widgets/AGENTS.md +21 -0
- generators/static/cursor/rules/apis-layer.mdc.jinja +21 -0
- generators/static/cursor/rules/application-layer.mdc.jinja +12 -0
- generators/static/cursor/rules/architecture-core.mdc.jinja +43 -0
- generators/static/cursor/rules/dart-conventions.mdc.jinja +14 -0
- generators/static/cursor/rules/domain-layer.mdc.jinja +24 -0
- generators/static/cursor/rules/login-context.mdc.jinja +16 -0
- generators/static/cursor/rules/presentation-layer.mdc.jinja +15 -0
- generators/static/cursor/rules/quality-gate.mdc.jinja +14 -0
- generators/static/cursor/rules/ui-caravaggio.mdc.jinja +22 -0
- generators/static/cursor/skills/caravaggio-ui/SKILL.md +138 -0
- generators/static/cursor/skills/epic-delivery/SKILL.md +74 -0
- generators/static/cursor/skills/epic-delivery/templates/epic-status.md +14 -0
- generators/static/cursor/skills/epic-delivery/templates/epic.md +36 -0
- generators/static/templates/apis/common/constants_template.jinja +48 -0
- generators/static/templates/apis/common/data_source_config_template.jinja +47 -0
- generators/static/templates/apis/core/api_injectable_module_template.jinja +14 -0
- generators/static/templates/apis/interceptors/api_logger_template.jinja +135 -0
- generators/static/templates/apis/interceptors/auth_interceptor_template.jinja +207 -0
- generators/static/templates/auth/application/auth_bloc_template.jinja +36 -0
- generators/static/templates/auth/application/auth_event_template.jinja +7 -0
- generators/static/templates/auth/application/auth_state_template.jinja +8 -0
- generators/static/templates/auth/infrastructure/auth_facade_module_template.jinja +16 -0
- generators/static/templates/auth/infrastructure/auth_facade_template.jinja +288 -0
- generators/static/templates/auth/infrastructure/mock_auth_facade_template.jinja +106 -0
- generators/static/templates/auth/model/auth_failure_template.jinja +11 -0
- generators/static/templates/auth/model/i_auth_facade_template.jinja +24 -0
- generators/static/templates/auth/model/user_template.jinja +34 -0
- generators/static/templates/auth/model/value_objects_template.jinja +54 -0
- generators/static/templates/auth/presentation/login_screen_template.jinja +24 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_bloc_template.jinja +139 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_event_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_state_template.jinja +26 -0
- generators/static/templates/auth/sign_in_form/presentation/apple_form_template.jinja +16 -0
- generators/static/templates/auth/sign_in_form/presentation/email_password_form_template_INACTIVE.jinja +94 -0
- generators/static/templates/auth/sign_in_form/presentation/google_form_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/presentation/sign_in_form_template.jinja +381 -0
- generators/static/templates/component/component_bloc_template.jinja +49 -0
- generators/static/templates/component/component_event_template.jinja +7 -0
- generators/static/templates/component/component_form_bloc_template.jinja +56 -0
- generators/static/templates/component/component_form_event_template.jinja +7 -0
- generators/static/templates/component/component_form_state_template.jinja +18 -0
- generators/static/templates/component/component_form_widget_template.jinja +130 -0
- generators/static/templates/component/component_list_widget_template.jinja +95 -0
- generators/static/templates/component/component_state_template.jinja +9 -0
- generators/static/templates/component/component_widget_template.jinja +113 -0
- generators/static/templates/core/bloc/base_form_bloc_template.jinja +32 -0
- generators/static/templates/core/errors/error_localizer_template.jinja +61 -0
- generators/static/templates/core/infrastructure/base_mapper_template.jinja +21 -0
- generators/static/templates/core/infrastructure/base_repository_mixin_template.jinja +78 -0
- generators/static/templates/core/infrastructure/firebase_injectable_module_template.jinja +11 -0
- generators/static/templates/core/infrastructure/firestore_helpers_template.jinja +22 -0
- generators/static/templates/core/infrastructure/repository_error_handler_template.jinja +44 -0
- generators/static/templates/core/model/common_interfaces_template.jinja +3 -0
- generators/static/templates/core/model/entity_template.jinja +5 -0
- generators/static/templates/core/model/errors_template.jinja +15 -0
- generators/static/templates/core/model/failures_template.jinja +97 -0
- generators/static/templates/core/model/value_objects_template.jinja +135 -0
- generators/static/templates/core/model/value_validators_template.jinja +105 -0
- generators/static/templates/core/presentation/app_drawer_template.jinja +52 -0
- generators/static/templates/core/presentation/app_widget_template.jinja +41 -0
- generators/static/templates/core/presentation/bottom_nav_bar_template.jinja +32 -0
- generators/static/templates/domain/domain_mapper_template.jinja +39 -0
- generators/static/templates/domain/domain_remote_service_template.jinja +46 -0
- generators/static/templates/domain/domain_repository_template.jinja +90 -0
- generators/static/templates/domain/domain_service_module_template.jinja +23 -0
- generators/static/templates/domain/enum_template.jinja +3 -0
- generators/static/templates/domain/i_domain_service_template.jinja +18 -0
- generators/static/templates/domain/mock_domain_service_template.jinja +73 -0
- generators/static/templates/feature/feature_bloc_template.jinja +104 -0
- generators/static/templates/feature/feature_dto_template.jinja +26 -0
- generators/static/templates/feature/feature_entity_template.jinja +23 -0
- generators/static/templates/feature/feature_event_template.jinja +20 -0
- generators/static/templates/feature/feature_extensions_template.jinja +25 -0
- generators/static/templates/feature/feature_failure_template.jinja +11 -0
- generators/static/templates/feature/feature_page_template.jinja +113 -0
- generators/static/templates/feature/feature_repository_template.jinja +112 -0
- generators/static/templates/feature/feature_state_template.jinja +20 -0
- generators/static/templates/feature/i_feature_repository_template.jinja +21 -0
- generators/static/templates/feature/value_object_field_template.jinja +0 -0
- generators/static/templates/feature/value_object_template.jinja +0 -0
- generators/static/templates/feature/value_validators_template.jinja +57 -0
- generators/static/templates/home/home_screen_template.jinja +68 -0
- generators/static/templates/home/presentation/home_screen_template.jinja +52 -0
- generators/static/templates/home/screen_template.jinja +12 -0
- generators/static/templates/injection_template.jinja +8 -0
- generators/static/templates/logging/analytics_logging_template.jinja +42 -0
- generators/static/templates/logging/console_template.jinja +50 -0
- generators/static/templates/logging/logger_injectable_module_template.jinja +24 -0
- generators/static/templates/logging/logger_template.jinja +118 -0
- generators/static/templates/main_template.jinja +43 -0
- generators/static/templates/page_template.jinja +29 -0
- generators/static/templates/router_template.jinja +26 -0
- generators/static/templates/splash/presentation/splash_screen_auth_template.jinja +97 -0
- generators/static/templates/splash/presentation/splash_screen_template.jinja +51 -0
- generators/static/templates/storage/storage_repository_template.jinja +80 -0
- generators/static/templates/user_profile/infrastructure/user_profile_dto_template.jinja +27 -0
- generators/static/templates/user_profile/infrastructure/user_profile_mapper_template.jinja +43 -0
- generators/static/templates/user_profile/infrastructure/user_profile_repository_template.jinja +82 -0
- generators/static/templates/user_profile/infrastructure/user_profile_service_template.jinja +27 -0
- generators/static/templates/user_profile/model/i_user_profile_repository_template.jinja +34 -0
- generators/static/templates/user_profile/model/user_profile_failure_template.jinja +11 -0
- generators/static/templates/user_profile/model/user_profile_template.jinja +31 -0
- generators/static/templates/widgets/common/custom_scaffold_template.jinja +52 -0
- generators/static/templates/widgets/common/error_widget_template.jinja +22 -0
- generators/static/templates/widgets/common/loading_widget_template.jinja +14 -0
- generators/static/templates/widgets/common/unknown_state_widget_template.jinja +23 -0
- generators/templates/__init__.py +1 -0
- generators/templates/_core/core_generator.py +121 -0
- generators/templates/apis/apis_generator.py +50 -0
- generators/templates/auth/auth_generator.py +40 -0
- generators/templates/auth/sign_in_form_generator.py +22 -0
- generators/templates/copier.py +51 -0
- generators/templates/home/home_generator.py +26 -0
- generators/templates/lib/lib_generator.py +40 -0
- generators/templates/logging/logging_generator.py +28 -0
- generators/templates/main.py +75 -0
- generators/templates/splash/splash_generator.py +20 -0
- generators/templates/storage/storage_generator.py +5 -0
coregen.py
ADDED
|
@@ -0,0 +1,1585 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CoreGen - CLI to create and manage Flutter projects with custom structure
|
|
4
|
+
|
|
5
|
+
Created by Lorenzo Busi @ GetAutomation
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.tree import Tree
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich import print as rprint
|
|
19
|
+
|
|
20
|
+
from generators import init
|
|
21
|
+
from generators.helpers import (
|
|
22
|
+
get_project_name,
|
|
23
|
+
validate_flutter_project,
|
|
24
|
+
generate_page_file,
|
|
25
|
+
update_router,
|
|
26
|
+
create_feature_layers,
|
|
27
|
+
create_presentation_feature_layers,
|
|
28
|
+
create_domain_entity_layers,
|
|
29
|
+
find_domain_models,
|
|
30
|
+
find_domain_models_with_class_names,
|
|
31
|
+
find_enums,
|
|
32
|
+
find_enums_with_info,
|
|
33
|
+
get_model_fields_from_domain,
|
|
34
|
+
create_drawer_page,
|
|
35
|
+
update_home_page_with_drawer,
|
|
36
|
+
create_drawer_widget,
|
|
37
|
+
create_bottom_nav_page,
|
|
38
|
+
update_home_page_with_bottom_nav,
|
|
39
|
+
create_bottom_nav_widget,
|
|
40
|
+
create_component_layers,
|
|
41
|
+
create_component_form_layers,
|
|
42
|
+
create_component_list_layers,
|
|
43
|
+
# Validation
|
|
44
|
+
validate_entity_name,
|
|
45
|
+
validate_field_name,
|
|
46
|
+
validate_field_type,
|
|
47
|
+
parse_fields_string,
|
|
48
|
+
pascal_case_to_snake_case,
|
|
49
|
+
to_pascal_case_preserve,
|
|
50
|
+
pascal_case_to_camel_case,
|
|
51
|
+
# Configuration
|
|
52
|
+
CoreGenConfig,
|
|
53
|
+
load_config,
|
|
54
|
+
apply_cli_overrides,
|
|
55
|
+
create_default_config,
|
|
56
|
+
show_config,
|
|
57
|
+
PROJECT_CONFIG_FILE,
|
|
58
|
+
)
|
|
59
|
+
from generators.helpers.utils import to_camel_case
|
|
60
|
+
|
|
61
|
+
# Version
|
|
62
|
+
VERSION = "3.1.6"
|
|
63
|
+
|
|
64
|
+
# Rich console for colored output
|
|
65
|
+
console = Console()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_success(message: str) -> None:
|
|
69
|
+
"""Print success message in green"""
|
|
70
|
+
console.print(f"[bold green]✅ {message}[/bold green]")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def print_error(message: str) -> None:
|
|
74
|
+
"""Print error message in red"""
|
|
75
|
+
console.print(f"[bold red]❌ {message}[/bold red]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def print_warning(message: str) -> None:
|
|
79
|
+
"""Print warning message in yellow"""
|
|
80
|
+
console.print(f"[bold yellow]⚠️ {message}[/bold yellow]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def print_info(message: str) -> None:
|
|
84
|
+
"""Print info message in blue"""
|
|
85
|
+
console.print(f"[bold blue]ℹ️ {message}[/bold blue]")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def print_step(message: str) -> None:
|
|
89
|
+
"""Print step message"""
|
|
90
|
+
console.print(f"[cyan]→ {message}[/cyan]")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_dry_run_header() -> None:
|
|
94
|
+
"""Print dry-run mode header with rich panel"""
|
|
95
|
+
console.print()
|
|
96
|
+
console.print(Panel.fit(
|
|
97
|
+
"[bold yellow]🔍 DRY-RUN MODE[/bold yellow]\n[dim]No files will be created[/dim]",
|
|
98
|
+
border_style="yellow"
|
|
99
|
+
))
|
|
100
|
+
console.print()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def print_dry_run_tree(base_path: str, structure: list[tuple[str, list[str]]]) -> None:
|
|
104
|
+
"""Print a tree structure for dry-run output using rich Tree"""
|
|
105
|
+
tree = Tree(f"[bold blue]📁 {base_path}/[/bold blue]")
|
|
106
|
+
|
|
107
|
+
for folder, files in structure:
|
|
108
|
+
folder_branch = tree.add(f"[blue]📁 {folder}/[/blue]")
|
|
109
|
+
for file in files:
|
|
110
|
+
folder_branch.add(f"[green]📄 {file}[/green]")
|
|
111
|
+
|
|
112
|
+
console.print(tree)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_dry_run_footer() -> None:
|
|
116
|
+
"""Print dry-run mode footer"""
|
|
117
|
+
console.print()
|
|
118
|
+
console.print("[dim]─" * 50 + "[/dim]")
|
|
119
|
+
print_info("Run without --dry-run to create these files")
|
|
120
|
+
console.print()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def print_created_structure(name: str, structure: list[tuple[str, list[str]]], updated_files: list[str] = None) -> None:
|
|
124
|
+
"""Print the structure of created files"""
|
|
125
|
+
tree = Tree(f"[bold green]📦 Created: {name}[/bold green]")
|
|
126
|
+
|
|
127
|
+
for folder, files in structure:
|
|
128
|
+
folder_branch = tree.add(f"[blue]📁 {folder}/[/blue]")
|
|
129
|
+
for file in files:
|
|
130
|
+
folder_branch.add(f"[green]✅ {file}[/green]")
|
|
131
|
+
|
|
132
|
+
console.print(tree)
|
|
133
|
+
|
|
134
|
+
if updated_files:
|
|
135
|
+
console.print()
|
|
136
|
+
console.print("[bold]📝 Updated files:[/bold]")
|
|
137
|
+
for file in updated_files:
|
|
138
|
+
console.print(f" [cyan]→ {file}[/cyan]")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def prompt_select_form_model_fields(all_fields: list[dict]) -> list[dict]:
|
|
142
|
+
"""Ask which domain model fields to include in the form; keeps model field order."""
|
|
143
|
+
if not all_fields:
|
|
144
|
+
return all_fields
|
|
145
|
+
|
|
146
|
+
console.print("[bold cyan]Quali campi del modello vuoi mostrare nel form?[/bold cyan]")
|
|
147
|
+
for i, field in enumerate(all_fields, start=1):
|
|
148
|
+
console.print(f" {i}. [green]{field['name']}[/green] : [magenta]{field['type']}[/magenta]")
|
|
149
|
+
console.print(" [dim](Invio senza numeri = tutti i campi)[/dim]")
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
raw = click.prompt(
|
|
154
|
+
"Indici separati da virgola (es. 1,3) oppure Invio per tutti",
|
|
155
|
+
default="",
|
|
156
|
+
show_default=False,
|
|
157
|
+
)
|
|
158
|
+
raw = (raw or "").strip()
|
|
159
|
+
if not raw:
|
|
160
|
+
return list(all_fields)
|
|
161
|
+
|
|
162
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
163
|
+
if not parts:
|
|
164
|
+
return list(all_fields)
|
|
165
|
+
|
|
166
|
+
indices = []
|
|
167
|
+
parse_ok = True
|
|
168
|
+
for p in parts:
|
|
169
|
+
try:
|
|
170
|
+
n = int(p)
|
|
171
|
+
except ValueError:
|
|
172
|
+
console.print("[red]Inserisci solo numeri interi separati da virgola.[/red]")
|
|
173
|
+
parse_ok = False
|
|
174
|
+
break
|
|
175
|
+
if n < 1 or n > len(all_fields):
|
|
176
|
+
console.print(
|
|
177
|
+
f"[red]Indice non valido: {n}. Usa un numero tra 1 e {len(all_fields)}.[/red]"
|
|
178
|
+
)
|
|
179
|
+
parse_ok = False
|
|
180
|
+
break
|
|
181
|
+
indices.append(n - 1)
|
|
182
|
+
if not parse_ok:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
seen = set()
|
|
186
|
+
ordered = []
|
|
187
|
+
for idx in indices:
|
|
188
|
+
if idx not in seen:
|
|
189
|
+
seen.add(idx)
|
|
190
|
+
ordered.append(idx)
|
|
191
|
+
|
|
192
|
+
selected = [all_fields[i] for i in ordered]
|
|
193
|
+
if not selected:
|
|
194
|
+
console.print("[red]Seleziona almeno un campo.[/red]")
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
return selected
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run_flutter_commands(project_path: Path) -> None:
|
|
201
|
+
"""Run flutter pub get and build_runner build after project modifications"""
|
|
202
|
+
try:
|
|
203
|
+
print_step("Running flutter pub get...")
|
|
204
|
+
subprocess.run(["flutter", "pub", "get"], cwd=project_path, check=True, capture_output=True)
|
|
205
|
+
|
|
206
|
+
# Check if build_runner is available before running it
|
|
207
|
+
try:
|
|
208
|
+
print_step("Running build_runner build...")
|
|
209
|
+
subprocess.run(["dart", "run", "build_runner", "build", "--delete-conflicting-outputs"], cwd=project_path, check=True, capture_output=True)
|
|
210
|
+
except subprocess.CalledProcessError:
|
|
211
|
+
print_warning("build_runner not available or failed. You may need to add it as a dev dependency.")
|
|
212
|
+
|
|
213
|
+
print_success("Dependencies updated!")
|
|
214
|
+
except subprocess.CalledProcessError as e:
|
|
215
|
+
print_warning(f"Could not run flutter commands: {e}")
|
|
216
|
+
print_info("You may need to run 'flutter pub get' manually.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@click.group()
|
|
220
|
+
@click.version_option(version=VERSION, prog_name="CoreGen", message="%(prog)s %(version)s")
|
|
221
|
+
def cli():
|
|
222
|
+
"""
|
|
223
|
+
🚀 CoreGen - CLI to create and manage Flutter projects with DDD architecture.
|
|
224
|
+
|
|
225
|
+
\b
|
|
226
|
+
Quick Start:
|
|
227
|
+
coregen create --name my_app
|
|
228
|
+
cd my_app
|
|
229
|
+
coregen add-domain --name todo --fields "title:string,done:bool"
|
|
230
|
+
coregen add-component --name todo_list --type list
|
|
231
|
+
|
|
232
|
+
\b
|
|
233
|
+
Available Commands:
|
|
234
|
+
create Create a new Flutter project
|
|
235
|
+
add-page Add a simple page
|
|
236
|
+
add-domain Add a domain entity (model + infrastructure only)
|
|
237
|
+
add-enum Add a Dart enum to the domain
|
|
238
|
+
add-component Add a reusable component (form, list, or single)
|
|
239
|
+
list List pages and domain models
|
|
240
|
+
config Manage configuration
|
|
241
|
+
|
|
242
|
+
\b
|
|
243
|
+
Tips:
|
|
244
|
+
• Use --dry-run to preview changes before creating files
|
|
245
|
+
• Use --no-build to skip flutter pub get
|
|
246
|
+
• Create coregen.yaml for project-specific defaults
|
|
247
|
+
|
|
248
|
+
Created by Lorenzo Busi @ GetAutomation
|
|
249
|
+
"""
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@cli.command()
|
|
254
|
+
@click.option('--project-path', default='.', help='Path to the Flutter project (default: current directory)')
|
|
255
|
+
@click.option('--init', 'init_config', is_flag=True, help='Create a new coregen.yaml config file')
|
|
256
|
+
@click.option('--show', is_flag=True, help='Show current configuration')
|
|
257
|
+
def config(project_path, init_config, show):
|
|
258
|
+
"""
|
|
259
|
+
Manage CoreGen configuration.
|
|
260
|
+
|
|
261
|
+
Configuration is loaded from (highest to lowest priority):
|
|
262
|
+
1. CLI flags
|
|
263
|
+
2. Project coregen.yaml
|
|
264
|
+
3. Global ~/.coregenrc
|
|
265
|
+
4. Built-in defaults
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
|
|
269
|
+
coregen config --show
|
|
270
|
+
|
|
271
|
+
coregen config --init
|
|
272
|
+
"""
|
|
273
|
+
project_dir = Path(project_path)
|
|
274
|
+
|
|
275
|
+
if init_config:
|
|
276
|
+
# Check if pubspec.yaml exists (it's a Flutter project)
|
|
277
|
+
if not (project_dir / "pubspec.yaml").exists():
|
|
278
|
+
print_error("Not a Flutter project. pubspec.yaml not found.")
|
|
279
|
+
print_info("Run this command from a Flutter project directory.")
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
config_path = project_dir / PROJECT_CONFIG_FILE
|
|
283
|
+
if config_path.exists():
|
|
284
|
+
if not click.confirm(f"⚠️ {PROJECT_CONFIG_FILE} already exists. Overwrite?"):
|
|
285
|
+
print_info("Aborted.")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
project_name = get_project_name(project_dir)
|
|
289
|
+
create_default_config(project_dir, project_name)
|
|
290
|
+
print_success(f"Created {PROJECT_CONFIG_FILE}")
|
|
291
|
+
console.print(f" [dim]Edit this file to customize CoreGen behavior[/dim]")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if show or (not init_config and not show):
|
|
295
|
+
# Show current configuration
|
|
296
|
+
cfg = load_config(project_dir if (project_dir / "pubspec.yaml").exists() else None)
|
|
297
|
+
show_config(cfg)
|
|
298
|
+
|
|
299
|
+
# Show where config was loaded from
|
|
300
|
+
console.print()
|
|
301
|
+
config_file = project_dir / PROJECT_CONFIG_FILE
|
|
302
|
+
if config_file.exists():
|
|
303
|
+
console.print(f"[dim]📄 Project config: {config_file}[/dim]")
|
|
304
|
+
else:
|
|
305
|
+
console.print(f"[dim]📄 No project config found. Run 'coregen config --init' to create one.[/dim]")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@cli.command()
|
|
309
|
+
@click.option('--name', default=None, help='Project name (lowercase, underscores allowed)')
|
|
310
|
+
@click.option(
|
|
311
|
+
'--login/--no-login',
|
|
312
|
+
default=None,
|
|
313
|
+
help='Include login/auth. Omit to be asked interactively after the project name.',
|
|
314
|
+
)
|
|
315
|
+
@click.option(
|
|
316
|
+
'--no-cursor',
|
|
317
|
+
is_flag=True,
|
|
318
|
+
default=False,
|
|
319
|
+
help='Skip generating .cursor/ rules, agents, skills, and docs/architecture.',
|
|
320
|
+
)
|
|
321
|
+
def create(name, login, no_cursor):
|
|
322
|
+
"""
|
|
323
|
+
Create a new Flutter project with DDD architecture.
|
|
324
|
+
|
|
325
|
+
\b
|
|
326
|
+
Creates a complete project structure with:
|
|
327
|
+
• Core layer (app widget, router, injection)
|
|
328
|
+
• Home screen with scaffold
|
|
329
|
+
• BLoC pattern ready
|
|
330
|
+
• Injectable setup
|
|
331
|
+
|
|
332
|
+
\b
|
|
333
|
+
Examples:
|
|
334
|
+
# Basic project
|
|
335
|
+
coregen create --name my_app
|
|
336
|
+
|
|
337
|
+
# Project with authentication
|
|
338
|
+
coregen create --name my_app --login
|
|
339
|
+
|
|
340
|
+
# Non-interactive mode
|
|
341
|
+
coregen create --name my_app --no-login
|
|
342
|
+
"""
|
|
343
|
+
if name is None:
|
|
344
|
+
name = click.prompt('Project name')
|
|
345
|
+
if login is None:
|
|
346
|
+
login = click.confirm('Does the project have login?', default=False)
|
|
347
|
+
|
|
348
|
+
# Project name validation
|
|
349
|
+
if not name.replace('_', '').replace('-', '').isalnum():
|
|
350
|
+
print_error("The project name must contain only letters, numbers, _ and -")
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
# Convert name for Flutter (lowercase with underscore)
|
|
354
|
+
flutter_name = name.lower().replace('-', '_')
|
|
355
|
+
|
|
356
|
+
console.print(Panel.fit(
|
|
357
|
+
f"[bold cyan]🚀 Creating project: {flutter_name}[/bold cyan]",
|
|
358
|
+
border_style="cyan"
|
|
359
|
+
))
|
|
360
|
+
|
|
361
|
+
init(flutter_name, login, cursor_setup=not no_cursor)
|
|
362
|
+
|
|
363
|
+
# Run flutter commands after project creation
|
|
364
|
+
run_flutter_commands(Path(flutter_name))
|
|
365
|
+
|
|
366
|
+
console.print()
|
|
367
|
+
print_success(f"Project '{flutter_name}' created successfully!")
|
|
368
|
+
console.print()
|
|
369
|
+
console.print("[bold]Next steps:[/bold]")
|
|
370
|
+
console.print(f" [cyan]cd {flutter_name}[/cyan]")
|
|
371
|
+
console.print(f" [cyan]flutter run[/cyan]")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@cli.command()
|
|
375
|
+
@click.option('--name', prompt='Page name', help='Page name (e.g., profile, settings, about)')
|
|
376
|
+
@click.option('--project-path', default='.', help='Path to Flutter project')
|
|
377
|
+
@click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
378
|
+
@click.option('--no-build', is_flag=True, help='Skip flutter pub get')
|
|
379
|
+
def add_page(name, project_path, dry_run, no_build):
|
|
380
|
+
"""
|
|
381
|
+
Add a simple page to an existing Flutter project.
|
|
382
|
+
|
|
383
|
+
\b
|
|
384
|
+
Creates:
|
|
385
|
+
• lib/<feature_folder>/<name>/<name>_page.dart (feature_folder from config, default: "features")
|
|
386
|
+
• Updates lib/router.dart with new route
|
|
387
|
+
|
|
388
|
+
\b
|
|
389
|
+
Use this for simple pages without business logic.
|
|
390
|
+
For pages with state management, use add-component --type list instead.
|
|
391
|
+
|
|
392
|
+
\b
|
|
393
|
+
Examples:
|
|
394
|
+
# Add a profile page
|
|
395
|
+
coregen add-page --name profile
|
|
396
|
+
|
|
397
|
+
# Preview what will be created
|
|
398
|
+
coregen add-page --name settings --dry-run
|
|
399
|
+
|
|
400
|
+
# Skip flutter pub get
|
|
401
|
+
coregen add-page --name about --no-build
|
|
402
|
+
"""
|
|
403
|
+
project_dir = Path(project_path)
|
|
404
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
405
|
+
|
|
406
|
+
# Load configuration
|
|
407
|
+
cfg = load_config(project_dir)
|
|
408
|
+
|
|
409
|
+
# Convert name to appropriate format
|
|
410
|
+
page_name = name.lower().replace(' ', '_')
|
|
411
|
+
|
|
412
|
+
# Determine feature folder path (use config, default to "features")
|
|
413
|
+
feature_folder = cfg.feature_folder if cfg.feature_folder else ""
|
|
414
|
+
|
|
415
|
+
# Dry-run mode: show what would be created
|
|
416
|
+
if dry_run:
|
|
417
|
+
print_dry_run_header()
|
|
418
|
+
console.print(f"[bold]📄 Would add page:[/bold] [cyan]{page_name}[/cyan]")
|
|
419
|
+
console.print()
|
|
420
|
+
# Build path for dry-run display
|
|
421
|
+
if feature_folder:
|
|
422
|
+
dry_run_path = f"lib/{feature_folder}/{page_name}"
|
|
423
|
+
else:
|
|
424
|
+
dry_run_path = f"lib/{page_name}"
|
|
425
|
+
print_dry_run_tree(dry_run_path, [
|
|
426
|
+
("", [f"{page_name}_page.dart"])
|
|
427
|
+
])
|
|
428
|
+
console.print()
|
|
429
|
+
console.print("[bold]📝 Would update:[/bold] [cyan]lib/router.dart[/cyan]")
|
|
430
|
+
print_dry_run_footer()
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
console.print(f"[bold cyan]📄 Adding page: {page_name}[/bold cyan]")
|
|
434
|
+
|
|
435
|
+
# Create page directory structure inside feature folder (from config)
|
|
436
|
+
if feature_folder:
|
|
437
|
+
features_dir = lib_path / feature_folder
|
|
438
|
+
else:
|
|
439
|
+
features_dir = lib_path
|
|
440
|
+
features_dir.mkdir(exist_ok=True, parents=True)
|
|
441
|
+
page_dir = features_dir / page_name
|
|
442
|
+
page_dir.mkdir(exist_ok=True)
|
|
443
|
+
|
|
444
|
+
# Generate page file directly in page directory (no presentation folder)
|
|
445
|
+
generate_page_file(page_name, page_dir, project_name)
|
|
446
|
+
|
|
447
|
+
# Update router with feature folder (from config)
|
|
448
|
+
update_router(project_dir, page_name, project_name, folder=feature_folder if feature_folder else None)
|
|
449
|
+
|
|
450
|
+
# Show created structure
|
|
451
|
+
print_created_structure(page_name, [
|
|
452
|
+
("", [f"{page_name}_page.dart"])
|
|
453
|
+
], ["lib/router.dart"])
|
|
454
|
+
|
|
455
|
+
# Run flutter commands (respecting --no-build and config)
|
|
456
|
+
if not no_build and cfg.auto_run_build_runner:
|
|
457
|
+
run_flutter_commands(project_dir)
|
|
458
|
+
elif no_build:
|
|
459
|
+
print_info("Skipping flutter pub get and build_runner (--no-build)")
|
|
460
|
+
|
|
461
|
+
print_success(f"Page '{page_name}' added successfully!")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@cli.command()
|
|
465
|
+
@click.option('--name', help='Domain entity name (e.g., todo, user, product)')
|
|
466
|
+
@click.option(
|
|
467
|
+
'--fields',
|
|
468
|
+
help=(
|
|
469
|
+
'Fields as name:type,name:type (e.g., "title:string,done:bool"). '
|
|
470
|
+
'Nullable: string?, String?, int?, List<T>?, Map<K,V>?. '
|
|
471
|
+
'In zsh, quote the value if types contain ? (otherwise the shell reports "no matches found").'
|
|
472
|
+
),
|
|
473
|
+
)
|
|
474
|
+
@click.option('--folder', help='Domain folder (default from config)')
|
|
475
|
+
@click.option('--project-path', default='.', help='Path to Flutter project')
|
|
476
|
+
@click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
477
|
+
@click.option('--no-build', is_flag=True, help='Skip flutter pub get')
|
|
478
|
+
@click.option(
|
|
479
|
+
'--non-interactive',
|
|
480
|
+
is_flag=True,
|
|
481
|
+
help='No field prompts; use --fields or only auto-added id field (for tools/CI)',
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
'--no-repo',
|
|
485
|
+
is_flag=True,
|
|
486
|
+
help='Skip repository interface, Retrofit service, and repository (DTO + mapper only)',
|
|
487
|
+
)
|
|
488
|
+
def add_domain(name, fields, folder, project_path, dry_run, no_build, non_interactive, no_repo):
|
|
489
|
+
"""
|
|
490
|
+
Add a domain entity (model + infrastructure only).
|
|
491
|
+
|
|
492
|
+
\b
|
|
493
|
+
Creates:
|
|
494
|
+
• lib/<domain_folder>/<name>/model/ - Entity, failure, repository interface (unless --no-repo)
|
|
495
|
+
• lib/<domain_folder>/<name>/infrastructure/ - DTO, mapper; with repo also service + repository
|
|
496
|
+
|
|
497
|
+
\b
|
|
498
|
+
Domain entities are shared business entities that can be used by multiple features.
|
|
499
|
+
They do NOT include application or presentation layers.
|
|
500
|
+
|
|
501
|
+
\b
|
|
502
|
+
Examples:
|
|
503
|
+
# Domain entity with fields
|
|
504
|
+
coregen add-domain --name todo --fields "title:string,done:bool,priority:int"
|
|
505
|
+
|
|
506
|
+
# Interactive mode (will prompt for fields)
|
|
507
|
+
coregen add-domain --name user
|
|
508
|
+
|
|
509
|
+
# Preview what will be created
|
|
510
|
+
coregen add-domain --name product --fields "name:string,price:double" --dry-run
|
|
511
|
+
|
|
512
|
+
# Custom domain folder
|
|
513
|
+
coregen add-domain --name note --fields "title:string" --folder shared/domain
|
|
514
|
+
|
|
515
|
+
# Nested/deserialization-only entity (no API repository)
|
|
516
|
+
coregen add-domain --name address --fields "street:string,city:string" --no-repo
|
|
517
|
+
"""
|
|
518
|
+
project_dir = Path(project_path)
|
|
519
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
520
|
+
|
|
521
|
+
# Load configuration
|
|
522
|
+
cfg = load_config(project_dir)
|
|
523
|
+
|
|
524
|
+
# Interactive mode - ask for missing parameters (skip if dry-run)
|
|
525
|
+
if not name:
|
|
526
|
+
if dry_run:
|
|
527
|
+
print_error("--name is required with --dry-run")
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
name = click.prompt("Domain entity name")
|
|
530
|
+
|
|
531
|
+
# Validate entity name
|
|
532
|
+
is_valid, error_msg = validate_entity_name(name)
|
|
533
|
+
if not is_valid:
|
|
534
|
+
print_error(error_msg)
|
|
535
|
+
sys.exit(1)
|
|
536
|
+
|
|
537
|
+
# Handle entity name: support PascalCase (e.g., "NoteItem") and snake_case (e.g., "note_item")
|
|
538
|
+
# - PascalCase names: use snake_case for folder, PascalCase for class
|
|
539
|
+
# - snake_case names: use snake_case for folder, PascalCase for class
|
|
540
|
+
if name and name[0].isupper() and '_' not in name:
|
|
541
|
+
# PascalCase input (e.g., "NoteItem")
|
|
542
|
+
entity_folder_name = pascal_case_to_snake_case(name)
|
|
543
|
+
entity_class_name = name # Keep original PascalCase
|
|
544
|
+
else:
|
|
545
|
+
# snake_case or other format
|
|
546
|
+
entity_folder_name = name.lower().replace(' ', '_').replace('-', '_')
|
|
547
|
+
entity_class_name = to_pascal_case_preserve(entity_folder_name)
|
|
548
|
+
|
|
549
|
+
# Use folder from CLI or config
|
|
550
|
+
if folder is None:
|
|
551
|
+
folder = cfg.domain_folder if cfg.domain_folder else "domain"
|
|
552
|
+
|
|
553
|
+
# Parse and validate fields
|
|
554
|
+
field_list = []
|
|
555
|
+
if fields:
|
|
556
|
+
try:
|
|
557
|
+
parsed_fields = parse_fields_string(fields)
|
|
558
|
+
for field_name, field_type in parsed_fields:
|
|
559
|
+
# Validate field name
|
|
560
|
+
is_valid_name, name_error = validate_field_name(field_name)
|
|
561
|
+
if not is_valid_name:
|
|
562
|
+
print_error(f"Invalid field name '{field_name}': {name_error}")
|
|
563
|
+
sys.exit(1)
|
|
564
|
+
|
|
565
|
+
# Validate field type
|
|
566
|
+
is_valid_type, type_error, normalized_type = validate_field_type(field_type, lib_path, folder)
|
|
567
|
+
if not is_valid_type:
|
|
568
|
+
print_error(f"Invalid field type '{field_type}' for field '{field_name}': {type_error}")
|
|
569
|
+
sys.exit(1)
|
|
570
|
+
|
|
571
|
+
field_list.append({"name": field_name, "type": normalized_type})
|
|
572
|
+
except ValueError as e:
|
|
573
|
+
print_error(str(e))
|
|
574
|
+
sys.exit(1)
|
|
575
|
+
elif non_interactive and not dry_run:
|
|
576
|
+
if not fields:
|
|
577
|
+
print_info("No --fields in non-interactive mode; entity will use default id field only.")
|
|
578
|
+
elif not dry_run:
|
|
579
|
+
# Interactive mode for fields
|
|
580
|
+
console.print("[bold cyan]Adding fields interactively. Type 'done' when finished.[/bold cyan]")
|
|
581
|
+
console.print("[dim]Tip: Use PascalCase for model names (e.g., NoteItem), List<ModelName> for lists, Type? for nullable (e.g., String?, int?)[/dim]")
|
|
582
|
+
while True:
|
|
583
|
+
field_name = click.prompt("Field name (or 'done')", default="done")
|
|
584
|
+
if field_name.lower() == 'done':
|
|
585
|
+
break
|
|
586
|
+
|
|
587
|
+
# Validate field name
|
|
588
|
+
is_valid_name, name_error = validate_field_name(field_name)
|
|
589
|
+
if not is_valid_name:
|
|
590
|
+
print_error(f"Invalid field name: {name_error}")
|
|
591
|
+
continue
|
|
592
|
+
|
|
593
|
+
field_type = click.prompt("Field type (e.g., string, int, String?, List<ModelName>)", default="string")
|
|
594
|
+
|
|
595
|
+
# Validate field type
|
|
596
|
+
is_valid_type, type_error, normalized_type = validate_field_type(field_type, lib_path, folder)
|
|
597
|
+
if not is_valid_type:
|
|
598
|
+
print_error(f"Invalid field type: {type_error}")
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
field_list.append({"name": field_name, "type": normalized_type})
|
|
602
|
+
|
|
603
|
+
# Ensure id field exists (add if not present)
|
|
604
|
+
has_id = any(field['name'] == 'id' for field in field_list)
|
|
605
|
+
if not has_id:
|
|
606
|
+
field_list.insert(0, {"name": "id", "type": "string"})
|
|
607
|
+
|
|
608
|
+
# Build base path for display (use folder name for paths)
|
|
609
|
+
base_path = f"lib/{folder}/{entity_folder_name}"
|
|
610
|
+
|
|
611
|
+
model_files = [
|
|
612
|
+
f"{entity_folder_name}.dart",
|
|
613
|
+
f"{entity_folder_name}_failure.dart",
|
|
614
|
+
"value_objects.dart",
|
|
615
|
+
"value_validators.dart",
|
|
616
|
+
]
|
|
617
|
+
if not no_repo:
|
|
618
|
+
model_files.insert(2, f"i_{entity_folder_name}_repository.dart")
|
|
619
|
+
|
|
620
|
+
infra_files = [
|
|
621
|
+
f"{entity_folder_name}_dto.dart",
|
|
622
|
+
f"{entity_folder_name}_mapper.dart",
|
|
623
|
+
]
|
|
624
|
+
if not no_repo:
|
|
625
|
+
infra_files[1:1] = [
|
|
626
|
+
f"i_{entity_folder_name}_service.dart",
|
|
627
|
+
f"{entity_folder_name}_remote_service.dart",
|
|
628
|
+
f"mock_{entity_folder_name}_service.dart",
|
|
629
|
+
f"{entity_folder_name}_service_module.dart",
|
|
630
|
+
]
|
|
631
|
+
infra_files.append(f"{entity_folder_name}_repository.dart")
|
|
632
|
+
|
|
633
|
+
# Dry-run mode: show what would be created
|
|
634
|
+
if dry_run:
|
|
635
|
+
print_dry_run_header()
|
|
636
|
+
console.print(f"[bold]📦 Would add domain entity:[/bold] [cyan]{entity_class_name}[/cyan]")
|
|
637
|
+
console.print(f" [dim]Domain folder:[/dim] [blue]{folder}[/blue]")
|
|
638
|
+
console.print(f" [dim]Class name:[/dim] [blue]{entity_class_name}[/blue]")
|
|
639
|
+
console.print(f" [dim]Folder name:[/dim] [blue]{entity_folder_name}[/blue]")
|
|
640
|
+
if field_list:
|
|
641
|
+
fields_str = ', '.join([f"[green]{field['name']}[/green]:[magenta]{field['type']}[/magenta]" for field in field_list])
|
|
642
|
+
console.print(f" [dim]Fields:[/dim] {fields_str}")
|
|
643
|
+
console.print()
|
|
644
|
+
|
|
645
|
+
print_dry_run_tree(base_path, [
|
|
646
|
+
("model", model_files),
|
|
647
|
+
("infrastructure", infra_files),
|
|
648
|
+
])
|
|
649
|
+
print_dry_run_footer()
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
console.print(f"[bold cyan]📦 Adding domain entity: {entity_class_name}[/bold cyan]")
|
|
653
|
+
console.print(f" [dim]Domain folder:[/dim] [blue]{folder}[/blue]")
|
|
654
|
+
console.print(f" [dim]Class name:[/dim] [blue]{entity_class_name}[/blue]")
|
|
655
|
+
console.print(f" [dim]Folder name:[/dim] [blue]{entity_folder_name}[/blue]")
|
|
656
|
+
if field_list:
|
|
657
|
+
fields_str = ', '.join([f"[green]{field['name']}[/green]:[magenta]{field['type']}[/magenta]" for field in field_list])
|
|
658
|
+
console.print(f" [dim]Fields:[/dim] {fields_str}")
|
|
659
|
+
|
|
660
|
+
# Create domain directory structure (use folder name for directory)
|
|
661
|
+
domain_dir = lib_path / folder / entity_folder_name
|
|
662
|
+
domain_dir.mkdir(parents=True, exist_ok=True)
|
|
663
|
+
|
|
664
|
+
# Create domain entity layers (model + infrastructure only)
|
|
665
|
+
# Pass both folder_name (for paths) and class_name (for class names)
|
|
666
|
+
create_domain_entity_layers(
|
|
667
|
+
domain_dir,
|
|
668
|
+
entity_folder_name,
|
|
669
|
+
entity_class_name,
|
|
670
|
+
field_list,
|
|
671
|
+
project_name,
|
|
672
|
+
folder,
|
|
673
|
+
no_repo=no_repo,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
from generators.templates._core.core_generator import generate_error_localizer, infer_has_login
|
|
677
|
+
|
|
678
|
+
if not no_repo:
|
|
679
|
+
from generators.helpers.data_source import generate_mock_json, regenerate_data_source_config
|
|
680
|
+
from generators.helpers.feature import find_enums_with_info
|
|
681
|
+
|
|
682
|
+
enums_info = find_enums_with_info(lib_path, folder) if lib_path.exists() else {}
|
|
683
|
+
generate_mock_json(
|
|
684
|
+
project_dir,
|
|
685
|
+
entity_folder_name,
|
|
686
|
+
field_list,
|
|
687
|
+
known_enums=enums_info,
|
|
688
|
+
)
|
|
689
|
+
regenerate_data_source_config(
|
|
690
|
+
project_name,
|
|
691
|
+
lib_path,
|
|
692
|
+
domain_folder=folder,
|
|
693
|
+
has_login=infer_has_login(lib_path),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Regenerate error_localizer with the newly added domain failure
|
|
697
|
+
|
|
698
|
+
generate_error_localizer(
|
|
699
|
+
project_name,
|
|
700
|
+
lib_path,
|
|
701
|
+
domain_folder=folder,
|
|
702
|
+
has_login=infer_has_login(lib_path),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Show created structure
|
|
706
|
+
print_created_structure(entity_folder_name, [
|
|
707
|
+
("model", model_files),
|
|
708
|
+
("infrastructure", infra_files),
|
|
709
|
+
])
|
|
710
|
+
|
|
711
|
+
# Run Flutter commands (respecting --no-build and config)
|
|
712
|
+
if not no_build and cfg.auto_run_build_runner:
|
|
713
|
+
run_flutter_commands(project_dir)
|
|
714
|
+
elif no_build:
|
|
715
|
+
print_info("Skipping flutter pub get and build_runner (--no-build)")
|
|
716
|
+
|
|
717
|
+
print_success(f"Domain entity '{entity_class_name}' added successfully!")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@cli.command()
|
|
721
|
+
@click.option('--name', help='Enum name in PascalCase (e.g., EventStatus, Priority)')
|
|
722
|
+
@click.option('--values', help='Comma-separated enum values (e.g., "pending,active,done")')
|
|
723
|
+
@click.option('--folder', help='Domain folder (default from config)')
|
|
724
|
+
@click.option('--project-path', default='.', help='Path to Flutter project')
|
|
725
|
+
@click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
726
|
+
@click.option('--force', is_flag=True, help='Overwrite existing enum file without prompting')
|
|
727
|
+
def add_enum(name, values, folder, project_path, dry_run, force):
|
|
728
|
+
"""
|
|
729
|
+
Add a Dart enum to the domain.
|
|
730
|
+
|
|
731
|
+
\b
|
|
732
|
+
Creates a single enum file in lib/<domain_folder>/enums/.
|
|
733
|
+
Once created, enum types can be used as field types in add-domain.
|
|
734
|
+
|
|
735
|
+
\b
|
|
736
|
+
Examples:
|
|
737
|
+
coregen add-enum --name EventStatus --values "pending,active,done"
|
|
738
|
+
|
|
739
|
+
coregen add-enum --name Priority --values "low,medium,high"
|
|
740
|
+
|
|
741
|
+
coregen add-enum --name EventStatus --values "pending,active,done" --dry-run
|
|
742
|
+
"""
|
|
743
|
+
from generators.templates.copier import generate_file as _gen_file
|
|
744
|
+
|
|
745
|
+
project_dir = Path(project_path)
|
|
746
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
747
|
+
|
|
748
|
+
# Load configuration
|
|
749
|
+
cfg = load_config(project_dir)
|
|
750
|
+
|
|
751
|
+
# Interactive mode - ask for missing parameters
|
|
752
|
+
if not name:
|
|
753
|
+
if dry_run:
|
|
754
|
+
print_error("--name is required with --dry-run")
|
|
755
|
+
sys.exit(1)
|
|
756
|
+
name = click.prompt("Enum name (PascalCase)")
|
|
757
|
+
|
|
758
|
+
# Validate enum name
|
|
759
|
+
is_valid, error_msg = validate_entity_name(name)
|
|
760
|
+
if not is_valid:
|
|
761
|
+
print_error(error_msg)
|
|
762
|
+
sys.exit(1)
|
|
763
|
+
|
|
764
|
+
# Normalise: PascalCase → snake_case for file, preserve PascalCase for class
|
|
765
|
+
if name and name[0].isupper() and '_' not in name:
|
|
766
|
+
enum_file_stem = pascal_case_to_snake_case(name)
|
|
767
|
+
enum_class_name = name
|
|
768
|
+
else:
|
|
769
|
+
enum_file_stem = name.lower().replace(' ', '_').replace('-', '_')
|
|
770
|
+
enum_class_name = to_pascal_case_preserve(enum_file_stem)
|
|
771
|
+
|
|
772
|
+
# Use folder from CLI or config
|
|
773
|
+
if folder is None:
|
|
774
|
+
folder = cfg.domain_folder if cfg.domain_folder else "domain"
|
|
775
|
+
|
|
776
|
+
# Parse values
|
|
777
|
+
if not values:
|
|
778
|
+
if dry_run:
|
|
779
|
+
print_error("--values is required with --dry-run")
|
|
780
|
+
sys.exit(1)
|
|
781
|
+
values = click.prompt("Enum values (comma-separated, e.g. pending,active,done)")
|
|
782
|
+
|
|
783
|
+
raw_values = [v.strip() for v in values.split(',') if v.strip()]
|
|
784
|
+
|
|
785
|
+
if not raw_values:
|
|
786
|
+
print_error("At least one enum value is required.")
|
|
787
|
+
sys.exit(1)
|
|
788
|
+
|
|
789
|
+
# Validate and normalise each value
|
|
790
|
+
normalised_values = []
|
|
791
|
+
seen_lower = set()
|
|
792
|
+
for val in raw_values:
|
|
793
|
+
norm = to_camel_case(val) if '_' in val else val
|
|
794
|
+
is_valid_val, val_err = validate_field_name(norm)
|
|
795
|
+
if not is_valid_val:
|
|
796
|
+
print_error(f"Invalid enum value '{val}': {val_err}")
|
|
797
|
+
sys.exit(1)
|
|
798
|
+
if norm.lower() in seen_lower:
|
|
799
|
+
print_error(f"Duplicate enum value '{norm}'.")
|
|
800
|
+
sys.exit(1)
|
|
801
|
+
seen_lower.add(norm.lower())
|
|
802
|
+
normalised_values.append(norm)
|
|
803
|
+
|
|
804
|
+
values_str = ", ".join(normalised_values)
|
|
805
|
+
enums_dir = lib_path / folder / "enums"
|
|
806
|
+
output_file = enums_dir / f"{enum_file_stem}.dart"
|
|
807
|
+
relative_path = f"lib/{folder}/enums/{enum_file_stem}.dart"
|
|
808
|
+
|
|
809
|
+
# Check for existing file
|
|
810
|
+
if output_file.exists() and not dry_run and not force:
|
|
811
|
+
if not click.confirm(f"⚠️ {relative_path} already exists. Overwrite?"):
|
|
812
|
+
print_info("Aborted.")
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
# Dry-run mode
|
|
816
|
+
if dry_run:
|
|
817
|
+
print_dry_run_header()
|
|
818
|
+
console.print(f"[bold]📦 Would create enum:[/bold] [cyan]{enum_class_name}[/cyan]")
|
|
819
|
+
console.print(f" [dim]File:[/dim] [blue]{relative_path}[/blue]")
|
|
820
|
+
console.print(f" [dim]Values:[/dim] [green]{values_str}[/green]")
|
|
821
|
+
console.print()
|
|
822
|
+
print_dry_run_tree(f"lib/{folder}/enums", [
|
|
823
|
+
("", [f"{enum_file_stem}.dart"])
|
|
824
|
+
])
|
|
825
|
+
print_dry_run_footer()
|
|
826
|
+
return
|
|
827
|
+
|
|
828
|
+
# Create file
|
|
829
|
+
enums_dir.mkdir(parents=True, exist_ok=True)
|
|
830
|
+
_gen_file(project_name, enums_dir, "domain/enum_template.jinja", f"{enum_file_stem}.dart", {
|
|
831
|
+
"enum_name": enum_class_name,
|
|
832
|
+
"values": values_str,
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
console.print(f"[bold cyan]📦 Created enum: {enum_class_name}[/bold cyan]")
|
|
836
|
+
console.print(f" [dim]File:[/dim] [blue]{relative_path}[/blue]")
|
|
837
|
+
console.print(f" [dim]Values:[/dim] [green]{values_str}[/green]")
|
|
838
|
+
print_success(f"Enum '{enum_class_name}' added successfully! You can now use it as a field type in add-domain.")
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
# @cli.command() # Disabled - use add-domain + add-component --type list instead
|
|
842
|
+
def add_feature(name=None, folder=None, fields=None, project_path='.', dry_run=False, no_build=False, domain=False, presentation=False):
|
|
843
|
+
"""
|
|
844
|
+
[DEPRECATED] This command has been removed.
|
|
845
|
+
|
|
846
|
+
Use the following instead:
|
|
847
|
+
- For domain entities: `coregen add-domain --name <model_name>`
|
|
848
|
+
- For list components: `coregen add-component --name <name> --type list`
|
|
849
|
+
- For single components: `coregen add-component --name <name> --type single`
|
|
850
|
+
- For form components: `coregen add-component --name <name> --type form`
|
|
851
|
+
|
|
852
|
+
This function is kept for backward compatibility only.
|
|
853
|
+
"""
|
|
854
|
+
# Early exit with error message
|
|
855
|
+
print_error("The 'add-feature' command has been removed.")
|
|
856
|
+
print_error("Use 'coregen add-domain --name <model_name>' to create domain entities.")
|
|
857
|
+
print_error("Use 'coregen add-component --name <name> --type list' to create list components.")
|
|
858
|
+
print_error("Use 'coregen add-component --name <name> --type single' to create single item components.")
|
|
859
|
+
print_error("Use 'coregen add-component --name <name> --type form' to create form components.")
|
|
860
|
+
sys.exit(1)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
# DEPRECATED: This command has been removed from the CLI but code is kept for retrocompatibilità
|
|
864
|
+
# Use add-page instead for simple pages, or add-component for more complex navigation needs
|
|
865
|
+
# @cli.command()
|
|
866
|
+
# @click.option('--name', prompt='Drawer item name', help='Item name (e.g., settings, profile)')
|
|
867
|
+
# @click.option('--project-path', default='.', help='Path to Flutter project')
|
|
868
|
+
# @click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
869
|
+
# @click.option('--no-build', is_flag=True, help='Skip flutter pub get')
|
|
870
|
+
def add_drawer_item(name, project_path, dry_run, no_build):
|
|
871
|
+
"""
|
|
872
|
+
Add a drawer navigation item to the home screen.
|
|
873
|
+
|
|
874
|
+
\b
|
|
875
|
+
Creates/Updates:
|
|
876
|
+
• lib/features/<name>/<name>_page.dart
|
|
877
|
+
• lib/core/presentation/app_drawer.dart
|
|
878
|
+
• lib/features/home/home_page.dart (adds drawer)
|
|
879
|
+
• lib/router.dart (new route)
|
|
880
|
+
|
|
881
|
+
\b
|
|
882
|
+
Examples:
|
|
883
|
+
# Add settings drawer item
|
|
884
|
+
coregen add-drawer-item --name settings
|
|
885
|
+
|
|
886
|
+
# Add profile with preview
|
|
887
|
+
coregen add-drawer-item --name profile --dry-run
|
|
888
|
+
|
|
889
|
+
\b
|
|
890
|
+
Note: Creates drawer widget on first use, adds items on subsequent calls.
|
|
891
|
+
"""
|
|
892
|
+
project_dir = Path(project_path)
|
|
893
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
894
|
+
|
|
895
|
+
# Convert name to appropriate format
|
|
896
|
+
drawer_item_name = name.lower().replace(' ', '_')
|
|
897
|
+
|
|
898
|
+
# Dry-run mode: show what would be created
|
|
899
|
+
if dry_run:
|
|
900
|
+
print_dry_run_header()
|
|
901
|
+
console.print(f"[bold]📱 Would add drawer item:[/bold] [cyan]{drawer_item_name}[/cyan]")
|
|
902
|
+
console.print()
|
|
903
|
+
print_dry_run_tree(f"lib/{drawer_item_name}", [
|
|
904
|
+
("presentation", [f"{drawer_item_name}_page.dart"])
|
|
905
|
+
])
|
|
906
|
+
console.print()
|
|
907
|
+
console.print("[bold]📝 Would update/create:[/bold]")
|
|
908
|
+
console.print(" [cyan]├── lib/router.dart[/cyan]")
|
|
909
|
+
console.print(" [cyan]├── lib/features/home/home_page.dart[/cyan]")
|
|
910
|
+
console.print(" [cyan]└── lib/core/presentation/app_drawer.dart[/cyan]")
|
|
911
|
+
print_dry_run_footer()
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
console.print(f"[bold cyan]📱 Adding drawer item: {drawer_item_name}[/bold cyan]")
|
|
915
|
+
|
|
916
|
+
# Check if home screen exists
|
|
917
|
+
home_dir = lib_path / "features" / "home"
|
|
918
|
+
if not home_dir.exists():
|
|
919
|
+
print_error("Home directory not found. Make sure this is a CoreGen project.")
|
|
920
|
+
sys.exit(1)
|
|
921
|
+
|
|
922
|
+
# Create page for the drawer item
|
|
923
|
+
create_drawer_page(project_dir, drawer_item_name, project_name)
|
|
924
|
+
|
|
925
|
+
# Update home screen to include drawer
|
|
926
|
+
update_home_page_with_drawer(project_dir, project_name)
|
|
927
|
+
|
|
928
|
+
# Create drawer widget if it doesn't exist
|
|
929
|
+
create_drawer_widget(project_dir, drawer_item_name, project_name)
|
|
930
|
+
|
|
931
|
+
print_created_structure(drawer_item_name, [
|
|
932
|
+
("presentation", [f"{drawer_item_name}_page.dart"])
|
|
933
|
+
], ["lib/router.dart", "lib/features/home/home_page.dart", "lib/core/presentation/app_drawer.dart"])
|
|
934
|
+
|
|
935
|
+
print_success(f"Drawer item '{drawer_item_name}' added successfully!")
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
# DEPRECATED: This command has been removed from the CLI but code is kept for retrocompatibilità
|
|
939
|
+
# Use add-page instead for simple pages, or add-component for more complex navigation needs
|
|
940
|
+
# @cli.command()
|
|
941
|
+
# @click.option('--name', prompt='Bottom nav item name', help='Tab name (e.g., search, favorites)')
|
|
942
|
+
# @click.option('--project-path', default='.', help='Path to Flutter project')
|
|
943
|
+
# @click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
944
|
+
# @click.option('--no-build', is_flag=True, help='Skip flutter pub get')
|
|
945
|
+
def add_bottom_nav_item(name, project_path, dry_run, no_build):
|
|
946
|
+
"""
|
|
947
|
+
Add a bottom navigation tab to the home screen.
|
|
948
|
+
|
|
949
|
+
\b
|
|
950
|
+
Creates/Updates:
|
|
951
|
+
• lib/features/home/<name>_screen.dart
|
|
952
|
+
• lib/core/presentation/bottom_nav_bar.dart
|
|
953
|
+
• lib/features/home/home_page.dart (adds bottom nav)
|
|
954
|
+
|
|
955
|
+
\b
|
|
956
|
+
Examples:
|
|
957
|
+
# Add search tab
|
|
958
|
+
coregen add-bottom-nav-item --name search
|
|
959
|
+
|
|
960
|
+
# Add favorites tab with preview
|
|
961
|
+
coregen add-bottom-nav-item --name favorites --dry-run
|
|
962
|
+
|
|
963
|
+
\b
|
|
964
|
+
Note: Creates bottom navigation on first use, adds tabs on subsequent calls.
|
|
965
|
+
"""
|
|
966
|
+
project_dir = Path(project_path)
|
|
967
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
968
|
+
|
|
969
|
+
# Load configuration
|
|
970
|
+
cfg = load_config(project_dir)
|
|
971
|
+
|
|
972
|
+
# Convert name to appropriate format
|
|
973
|
+
bottom_nav_item_name = name.lower().replace(' ', '_')
|
|
974
|
+
|
|
975
|
+
# Dry-run mode: show what would be created
|
|
976
|
+
if dry_run:
|
|
977
|
+
print_dry_run_header()
|
|
978
|
+
console.print(f"[bold]📱 Would add bottom nav item:[/bold] [cyan]{bottom_nav_item_name}[/cyan]")
|
|
979
|
+
console.print()
|
|
980
|
+
tree = Tree(f"[bold blue]📁 lib/features/home/[/bold blue]")
|
|
981
|
+
tree.add(f"[green]📄 {bottom_nav_item_name}_screen.dart[/green]")
|
|
982
|
+
console.print(tree)
|
|
983
|
+
console.print()
|
|
984
|
+
console.print("[bold]📝 Would update/create:[/bold]")
|
|
985
|
+
console.print(" [cyan]├── lib/features/home/home_page.dart[/cyan]")
|
|
986
|
+
console.print(" [cyan]└── lib/core/presentation/bottom_nav_bar.dart[/cyan]")
|
|
987
|
+
print_dry_run_footer()
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
console.print(f"[bold cyan]📱 Adding bottom nav item: {bottom_nav_item_name}[/bold cyan]")
|
|
991
|
+
|
|
992
|
+
# Check if home screen exists
|
|
993
|
+
home_dir = lib_path / "features" / "home"
|
|
994
|
+
if not home_dir.exists():
|
|
995
|
+
print_error("Home directory not found. Make sure this is a CoreGen project.")
|
|
996
|
+
sys.exit(1)
|
|
997
|
+
|
|
998
|
+
# Create page for the bottom nav item
|
|
999
|
+
create_bottom_nav_page(project_dir, bottom_nav_item_name)
|
|
1000
|
+
|
|
1001
|
+
# Update home screen to include bottom navigation
|
|
1002
|
+
update_home_page_with_bottom_nav(project_dir, bottom_nav_item_name, project_name)
|
|
1003
|
+
|
|
1004
|
+
# Create bottom navigation widget if it doesn't exist
|
|
1005
|
+
create_bottom_nav_widget(project_dir, bottom_nav_item_name)
|
|
1006
|
+
|
|
1007
|
+
# Run Flutter commands (respecting --no-build and config)
|
|
1008
|
+
if not no_build and cfg.auto_run_build_runner:
|
|
1009
|
+
run_flutter_commands(project_dir)
|
|
1010
|
+
elif no_build:
|
|
1011
|
+
print_info("Skipping flutter pub get and build_runner (--no-build)")
|
|
1012
|
+
|
|
1013
|
+
console.print()
|
|
1014
|
+
console.print("[bold]📝 Updated files:[/bold]")
|
|
1015
|
+
console.print(" [cyan]→ lib/features/home/home_page.dart[/cyan]")
|
|
1016
|
+
console.print(" [cyan]→ lib/core/presentation/bottom_nav_bar.dart[/cyan]")
|
|
1017
|
+
console.print(f" [green]✅ lib/features/home/{bottom_nav_item_name}_screen.dart[/green]")
|
|
1018
|
+
|
|
1019
|
+
print_success(f"Bottom nav item '{bottom_nav_item_name}' added successfully!")
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@cli.command()
|
|
1023
|
+
@click.option('--name', help='Component name (e.g., user_card, login_form)')
|
|
1024
|
+
@click.option(
|
|
1025
|
+
'--fields',
|
|
1026
|
+
help=(
|
|
1027
|
+
'Form fields as name:type,name:type (only for --type form). '
|
|
1028
|
+
'Nullable: string?, String?, List<T>?, etc. Quote the whole argument in zsh when using ?.'
|
|
1029
|
+
),
|
|
1030
|
+
)
|
|
1031
|
+
@click.option('--type', type=click.Choice(['form', 'list', 'single'], case_sensitive=False), help='Component type: form, list, or single')
|
|
1032
|
+
@click.option('--folder', help='Target folder (e.g., components, shared)')
|
|
1033
|
+
@click.option('--project-path', default='.', help='Path to Flutter project')
|
|
1034
|
+
@click.option('--dry-run', is_flag=True, help='Preview without creating files')
|
|
1035
|
+
@click.option('--no-build', is_flag=True, help='Skip flutter pub get')
|
|
1036
|
+
@click.option(
|
|
1037
|
+
'--domain-model',
|
|
1038
|
+
'domain_model_opt',
|
|
1039
|
+
default=None,
|
|
1040
|
+
help='Skip model prompt: domain entity file stem, or "none" for no model (non-interactive)',
|
|
1041
|
+
)
|
|
1042
|
+
@click.option(
|
|
1043
|
+
'--use-all-model-fields',
|
|
1044
|
+
'use_all_model_fields',
|
|
1045
|
+
is_flag=True,
|
|
1046
|
+
help='With --type form and a domain model, include all model fields (skip field selection prompt)',
|
|
1047
|
+
)
|
|
1048
|
+
def add_component(
|
|
1049
|
+
name,
|
|
1050
|
+
fields,
|
|
1051
|
+
type,
|
|
1052
|
+
folder,
|
|
1053
|
+
project_path,
|
|
1054
|
+
dry_run,
|
|
1055
|
+
no_build,
|
|
1056
|
+
domain_model_opt,
|
|
1057
|
+
use_all_model_fields,
|
|
1058
|
+
):
|
|
1059
|
+
"""
|
|
1060
|
+
Add a reusable component with optional BLoC.
|
|
1061
|
+
|
|
1062
|
+
\b
|
|
1063
|
+
Three types available:
|
|
1064
|
+
|
|
1065
|
+
SINGLE COMPONENT (--type single or default):
|
|
1066
|
+
Creates a component that displays a single item loaded by ID.
|
|
1067
|
+
• application/ - BLoC, events, states
|
|
1068
|
+
• presentation/ - Widget
|
|
1069
|
+
|
|
1070
|
+
LIST COMPONENT (--type list):
|
|
1071
|
+
Creates a component that displays a list of items with full CRUD operations.
|
|
1072
|
+
• application/ - BLoC with getAll, create, update, delete
|
|
1073
|
+
• presentation/ - Widget with ListView
|
|
1074
|
+
|
|
1075
|
+
FORM COMPONENT (--type form):
|
|
1076
|
+
Creates a form with field validation and submission handling.
|
|
1077
|
+
With a domain model, you are asked which model fields to include in the form.
|
|
1078
|
+
With no model and no --fields, you get an empty form scaffold (Submit only) to fill in manually.
|
|
1079
|
+
• application/ - Form BLoC, events, states
|
|
1080
|
+
• presentation/ - Form widget
|
|
1081
|
+
|
|
1082
|
+
\b
|
|
1083
|
+
Examples:
|
|
1084
|
+
# Single component (default)
|
|
1085
|
+
coregen add-component --name user_card
|
|
1086
|
+
|
|
1087
|
+
# List component
|
|
1088
|
+
coregen add-component --name todo_list --type list
|
|
1089
|
+
|
|
1090
|
+
# Form component with fields
|
|
1091
|
+
coregen add-component --name login --type form \\
|
|
1092
|
+
--fields "email:string,password:string"
|
|
1093
|
+
|
|
1094
|
+
# Component in specific folder (default is features/components)
|
|
1095
|
+
coregen add-component --name search_bar --folder shared/widgets
|
|
1096
|
+
|
|
1097
|
+
# Preview component
|
|
1098
|
+
coregen add-component --name register --type list --dry-run
|
|
1099
|
+
|
|
1100
|
+
\b
|
|
1101
|
+
Default folder: features/components (can be overridden with --folder or coregen.yaml)
|
|
1102
|
+
"""
|
|
1103
|
+
project_dir = Path(project_path)
|
|
1104
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
1105
|
+
|
|
1106
|
+
# Load configuration
|
|
1107
|
+
cfg = load_config(project_dir)
|
|
1108
|
+
|
|
1109
|
+
# Validate fields option
|
|
1110
|
+
if fields is not None and type and type.lower() != 'form':
|
|
1111
|
+
print_error("The --fields option can only be used with --type form.")
|
|
1112
|
+
sys.exit(1)
|
|
1113
|
+
|
|
1114
|
+
# Interactive mode - always ask for missing parameters (skip if dry-run)
|
|
1115
|
+
if not name:
|
|
1116
|
+
if dry_run:
|
|
1117
|
+
print_error("--name is required with --dry-run")
|
|
1118
|
+
sys.exit(1)
|
|
1119
|
+
name = click.prompt("Component name")
|
|
1120
|
+
|
|
1121
|
+
component_name = name.lower().replace(' ', '_')
|
|
1122
|
+
|
|
1123
|
+
# Select feature/folder for component
|
|
1124
|
+
if folder is None:
|
|
1125
|
+
# Find available features/folders in lib/features
|
|
1126
|
+
features_dir = lib_path / "features"
|
|
1127
|
+
available_features = []
|
|
1128
|
+
if features_dir.exists():
|
|
1129
|
+
# Get all directories in features/
|
|
1130
|
+
for item in features_dir.iterdir():
|
|
1131
|
+
if item.is_dir():
|
|
1132
|
+
feature_name = item.name
|
|
1133
|
+
available_features.append(feature_name)
|
|
1134
|
+
|
|
1135
|
+
# Sort features and put "components" first if it exists
|
|
1136
|
+
available_features = sorted(available_features, key=lambda x: (x != "components", x))
|
|
1137
|
+
|
|
1138
|
+
if not dry_run and available_features:
|
|
1139
|
+
console.print("[bold cyan]Select feature/folder for component:[/bold cyan]")
|
|
1140
|
+
for i, feat in enumerate(available_features):
|
|
1141
|
+
default_mark = " (default)" if feat == "components" else ""
|
|
1142
|
+
console.print(f" {i}. {feat}{default_mark}")
|
|
1143
|
+
console.print()
|
|
1144
|
+
|
|
1145
|
+
while True:
|
|
1146
|
+
try:
|
|
1147
|
+
choice_str = click.prompt(f"Feature (0-{len(available_features)-1})", default="0", type=str)
|
|
1148
|
+
choice = int(choice_str) if choice_str else 0
|
|
1149
|
+
if 0 <= choice < len(available_features):
|
|
1150
|
+
selected_feature = available_features[choice]
|
|
1151
|
+
folder = f"features/{selected_feature}"
|
|
1152
|
+
break
|
|
1153
|
+
else:
|
|
1154
|
+
console.print(f"[red]Invalid choice. Please select 0-{len(available_features)-1}.[/red]")
|
|
1155
|
+
except ValueError:
|
|
1156
|
+
console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
1157
|
+
else:
|
|
1158
|
+
# Default to features/components if not specified or in dry-run
|
|
1159
|
+
if cfg.component_folder:
|
|
1160
|
+
folder = cfg.component_folder
|
|
1161
|
+
else:
|
|
1162
|
+
folder = "features/components"
|
|
1163
|
+
|
|
1164
|
+
# Determine component type - use parameter if provided via CLI, otherwise ask interactively
|
|
1165
|
+
if type:
|
|
1166
|
+
component_type = type.lower()
|
|
1167
|
+
elif dry_run:
|
|
1168
|
+
component_type = 'single' # Default to single component in dry-run
|
|
1169
|
+
else:
|
|
1170
|
+
console.print("[bold cyan]Select component type:[/bold cyan]")
|
|
1171
|
+
console.print(" 1. Single item (loads one item by ID)")
|
|
1172
|
+
console.print(" 2. List (shows all items with CRUD operations)")
|
|
1173
|
+
console.print(" 3. Form (form with validation)")
|
|
1174
|
+
console.print()
|
|
1175
|
+
while True:
|
|
1176
|
+
choice = click.prompt("Type (1-3)", type=int)
|
|
1177
|
+
if choice == 1:
|
|
1178
|
+
component_type = 'single'
|
|
1179
|
+
break
|
|
1180
|
+
elif choice == 2:
|
|
1181
|
+
component_type = 'list'
|
|
1182
|
+
break
|
|
1183
|
+
elif choice == 3:
|
|
1184
|
+
component_type = 'form'
|
|
1185
|
+
break
|
|
1186
|
+
console.print("[red]Invalid choice. Please select 1, 2, or 3.[/red]")
|
|
1187
|
+
|
|
1188
|
+
# Select domain model
|
|
1189
|
+
models_info = find_domain_models_with_class_names(lib_path, cfg.domain_folder)
|
|
1190
|
+
available_models = sorted(models_info.keys())
|
|
1191
|
+
domain_model_name = None
|
|
1192
|
+
domain_model_folder = None
|
|
1193
|
+
if domain_model_opt is not None:
|
|
1194
|
+
raw = domain_model_opt.strip()
|
|
1195
|
+
if raw.lower() in ('', 'none'):
|
|
1196
|
+
domain_model_name = None
|
|
1197
|
+
domain_model_folder = None
|
|
1198
|
+
else:
|
|
1199
|
+
match_key = None
|
|
1200
|
+
for k in available_models:
|
|
1201
|
+
if k == raw or k.lower() == raw.lower():
|
|
1202
|
+
match_key = k
|
|
1203
|
+
break
|
|
1204
|
+
if match_key is None:
|
|
1205
|
+
known = ', '.join(available_models) if available_models else '(none)'
|
|
1206
|
+
print_error(f"Unknown domain model '{raw}'. Known: {known}")
|
|
1207
|
+
sys.exit(1)
|
|
1208
|
+
domain_model_name = match_key
|
|
1209
|
+
domain_model_folder = models_info[match_key]['folder']
|
|
1210
|
+
elif not dry_run:
|
|
1211
|
+
console.print(f"[bold cyan]Select domain model:[/bold cyan]")
|
|
1212
|
+
console.print(f" 0. [dim](Vuoto) - componente senza modello[/dim]")
|
|
1213
|
+
for i, model_key in enumerate(available_models, 1):
|
|
1214
|
+
info = models_info[model_key]
|
|
1215
|
+
console.print(f" {i}. {info['class_name']} ({model_key})")
|
|
1216
|
+
console.print()
|
|
1217
|
+
|
|
1218
|
+
while True:
|
|
1219
|
+
choice = click.prompt(f"Select domain model (0-{len(available_models)})", type=int)
|
|
1220
|
+
if choice == 0:
|
|
1221
|
+
domain_model_name = None
|
|
1222
|
+
domain_model_folder = None
|
|
1223
|
+
break
|
|
1224
|
+
elif 1 <= choice <= len(available_models):
|
|
1225
|
+
domain_model_name = available_models[choice - 1]
|
|
1226
|
+
domain_model_folder = models_info[domain_model_name]['folder']
|
|
1227
|
+
break
|
|
1228
|
+
console.print(f"[red]Invalid choice. Please select 0-{len(available_models)}.[/red]")
|
|
1229
|
+
else:
|
|
1230
|
+
# Dry run: use first available model if any, otherwise use empty (Vuoto)
|
|
1231
|
+
if available_models:
|
|
1232
|
+
domain_model_name = available_models[0]
|
|
1233
|
+
domain_model_folder = models_info[domain_model_name]['folder']
|
|
1234
|
+
else:
|
|
1235
|
+
domain_model_name = None
|
|
1236
|
+
domain_model_folder = None
|
|
1237
|
+
|
|
1238
|
+
# Build base path for display
|
|
1239
|
+
base_path = f"lib/{folder}/{component_name}" if folder else f"lib/{component_name}"
|
|
1240
|
+
|
|
1241
|
+
# Get fields for form components (from domain model, or --fields when no model)
|
|
1242
|
+
field_list = []
|
|
1243
|
+
domain_folder_for_field_types = cfg.domain_folder if cfg.domain_folder else "domain"
|
|
1244
|
+
if component_type == 'form' and domain_model_name is not None:
|
|
1245
|
+
try:
|
|
1246
|
+
field_list = get_model_fields_from_domain(lib_path, cfg.domain_folder, domain_model_name, domain_model_folder)
|
|
1247
|
+
except Exception as e:
|
|
1248
|
+
print_error(f"Error reading domain model: {e}")
|
|
1249
|
+
sys.exit(1)
|
|
1250
|
+
if not dry_run and field_list and not use_all_model_fields:
|
|
1251
|
+
field_list = prompt_select_form_model_fields(field_list)
|
|
1252
|
+
elif component_type == 'form' and fields:
|
|
1253
|
+
try:
|
|
1254
|
+
parsed_fields = parse_fields_string(fields)
|
|
1255
|
+
for field_name, field_type in parsed_fields:
|
|
1256
|
+
is_valid_name, name_error = validate_field_name(field_name)
|
|
1257
|
+
if not is_valid_name:
|
|
1258
|
+
print_error(f"Invalid field name '{field_name}': {name_error}")
|
|
1259
|
+
sys.exit(1)
|
|
1260
|
+
is_valid_type, type_error, normalized_type = validate_field_type(
|
|
1261
|
+
field_type, lib_path, domain_folder_for_field_types
|
|
1262
|
+
)
|
|
1263
|
+
if not is_valid_type:
|
|
1264
|
+
print_error(f"Invalid field type '{field_type}' for field '{field_name}': {type_error}")
|
|
1265
|
+
sys.exit(1)
|
|
1266
|
+
field_list.append({"name": field_name, "type": normalized_type})
|
|
1267
|
+
except ValueError as e:
|
|
1268
|
+
print_error(str(e))
|
|
1269
|
+
sys.exit(1)
|
|
1270
|
+
|
|
1271
|
+
# Dry-run mode: show what would be created
|
|
1272
|
+
if dry_run:
|
|
1273
|
+
print_dry_run_header()
|
|
1274
|
+
console.print(f"[bold]🔧 Would add component:[/bold] [cyan]{component_name}[/cyan]")
|
|
1275
|
+
model_label = domain_model_name if domain_model_name else "(Vuoto)"
|
|
1276
|
+
console.print(f" [dim]Using domain model:[/dim] [blue]{model_label}[/blue]")
|
|
1277
|
+
console.print(f" [dim]Type:[/dim] [magenta]{component_type.capitalize()} component[/magenta]")
|
|
1278
|
+
if component_type == 'form' and field_list:
|
|
1279
|
+
fields_str = ', '.join([f"[green]{field['name']}[/green]:[magenta]{field['type']}[/magenta]" for field in field_list])
|
|
1280
|
+
console.print(f" [dim]Fields:[/dim] {fields_str}")
|
|
1281
|
+
if folder:
|
|
1282
|
+
console.print(f" [dim]Folder:[/dim] [blue]{folder}[/blue]")
|
|
1283
|
+
console.print()
|
|
1284
|
+
|
|
1285
|
+
if component_type == 'form':
|
|
1286
|
+
print_dry_run_tree(base_path, [
|
|
1287
|
+
("application", [
|
|
1288
|
+
f"{component_name}_form_bloc.dart",
|
|
1289
|
+
f"{component_name}_form_event.dart",
|
|
1290
|
+
f"{component_name}_form_state.dart"
|
|
1291
|
+
]),
|
|
1292
|
+
("presentation", [
|
|
1293
|
+
f"{component_name}_component.dart"
|
|
1294
|
+
])
|
|
1295
|
+
])
|
|
1296
|
+
elif component_type == 'list':
|
|
1297
|
+
print_dry_run_tree(base_path, [
|
|
1298
|
+
("application", [
|
|
1299
|
+
f"{component_name}_bloc.dart",
|
|
1300
|
+
f"{component_name}_event.dart",
|
|
1301
|
+
f"{component_name}_state.dart"
|
|
1302
|
+
]),
|
|
1303
|
+
("presentation", [
|
|
1304
|
+
f"{component_name}_component.dart"
|
|
1305
|
+
])
|
|
1306
|
+
])
|
|
1307
|
+
else: # single
|
|
1308
|
+
print_dry_run_tree(base_path, [
|
|
1309
|
+
("application", [
|
|
1310
|
+
f"{component_name}_bloc.dart",
|
|
1311
|
+
f"{component_name}_event.dart",
|
|
1312
|
+
f"{component_name}_state.dart"
|
|
1313
|
+
]),
|
|
1314
|
+
("presentation", [
|
|
1315
|
+
f"{component_name}_component.dart"
|
|
1316
|
+
])
|
|
1317
|
+
])
|
|
1318
|
+
print_dry_run_footer()
|
|
1319
|
+
return
|
|
1320
|
+
|
|
1321
|
+
console.print(f"[bold cyan]🔧 Adding {component_type} component: {component_name}[/bold cyan]")
|
|
1322
|
+
model_label = domain_model_name if domain_model_name else "(Vuoto)"
|
|
1323
|
+
console.print(f" [dim]Using domain model:[/dim] [blue]{model_label}[/blue]")
|
|
1324
|
+
if component_type == 'form' and field_list:
|
|
1325
|
+
fields_str = ', '.join([f"[green]{field['name']}[/green]:[magenta]{field['type']}[/magenta]" for field in field_list])
|
|
1326
|
+
console.print(f" [dim]Fields:[/dim] {fields_str}")
|
|
1327
|
+
|
|
1328
|
+
if folder:
|
|
1329
|
+
console.print(f" [dim]Folder:[/dim] [blue]{folder}[/blue]")
|
|
1330
|
+
|
|
1331
|
+
# Create component directory structure
|
|
1332
|
+
if folder:
|
|
1333
|
+
# Create nested folder structure
|
|
1334
|
+
folder_path = lib_path
|
|
1335
|
+
for folder_part in folder.split('/'):
|
|
1336
|
+
folder_path = folder_path / folder_part
|
|
1337
|
+
component_dir = folder_path / component_name
|
|
1338
|
+
else:
|
|
1339
|
+
component_dir = lib_path / component_name
|
|
1340
|
+
|
|
1341
|
+
component_dir.mkdir(parents=True, exist_ok=True)
|
|
1342
|
+
|
|
1343
|
+
if component_type == 'form':
|
|
1344
|
+
# Create all layers with domain model fields
|
|
1345
|
+
create_component_form_layers(component_dir, component_name, field_list, project_name, folder, domain_model_name, cfg.domain_folder, domain_model_folder, lib_path=lib_path)
|
|
1346
|
+
# Show created structure
|
|
1347
|
+
print_created_structure(component_name, [
|
|
1348
|
+
("application", [f"{component_name}_form_bloc.dart", f"{component_name}_form_event.dart", f"{component_name}_form_state.dart"]),
|
|
1349
|
+
("presentation", [f"{component_name}_component.dart"])
|
|
1350
|
+
])
|
|
1351
|
+
elif component_type == 'list':
|
|
1352
|
+
# Create all layers with list functionality (CRUD operations)
|
|
1353
|
+
create_component_list_layers(component_dir, component_name, project_name, folder, domain_model_name, cfg.domain_folder, domain_model_folder, lib_path)
|
|
1354
|
+
# Show created structure
|
|
1355
|
+
print_created_structure(component_name, [
|
|
1356
|
+
("application", [f"{component_name}_bloc.dart", f"{component_name}_event.dart", f"{component_name}_state.dart"]),
|
|
1357
|
+
("presentation", [f"{component_name}_component.dart"])
|
|
1358
|
+
])
|
|
1359
|
+
else: # single
|
|
1360
|
+
# Create all layers with domain model reference
|
|
1361
|
+
create_component_layers(component_dir, component_name, project_name, folder, domain_model_name, cfg.domain_folder, domain_model_folder, lib_path)
|
|
1362
|
+
# Show created structure
|
|
1363
|
+
print_created_structure(component_name, [
|
|
1364
|
+
("application", [f"{component_name}_bloc.dart", f"{component_name}_event.dart", f"{component_name}_state.dart"]),
|
|
1365
|
+
("presentation", [f"{component_name}_component.dart"])
|
|
1366
|
+
])
|
|
1367
|
+
|
|
1368
|
+
# Run Flutter commands (respecting --no-build and config)
|
|
1369
|
+
if not no_build and cfg.auto_run_build_runner:
|
|
1370
|
+
run_flutter_commands(project_dir)
|
|
1371
|
+
elif no_build:
|
|
1372
|
+
print_info("Skipping flutter pub get and build_runner (--no-build)")
|
|
1373
|
+
|
|
1374
|
+
print_success(f"Component '{component_name}' added successfully!")
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
@cli.command(name='list')
|
|
1378
|
+
@click.option('--project-path', default='.', help='Path to Flutter project')
|
|
1379
|
+
def list_resources(project_path):
|
|
1380
|
+
"""
|
|
1381
|
+
List pages and domain models in the Flutter project.
|
|
1382
|
+
|
|
1383
|
+
\b
|
|
1384
|
+
Shows:
|
|
1385
|
+
pages - All pages from router.dart
|
|
1386
|
+
models - All domain models from domain/ folder
|
|
1387
|
+
|
|
1388
|
+
\b
|
|
1389
|
+
Examples:
|
|
1390
|
+
# List pages and models
|
|
1391
|
+
coregen list
|
|
1392
|
+
"""
|
|
1393
|
+
project_dir = Path(project_path)
|
|
1394
|
+
lib_path, project_name = validate_flutter_project(project_dir)
|
|
1395
|
+
|
|
1396
|
+
# Load configuration
|
|
1397
|
+
cfg = load_config(project_dir)
|
|
1398
|
+
|
|
1399
|
+
console.print(Panel.fit(
|
|
1400
|
+
f"[bold cyan]📋 Project: {project_name}[/bold cyan]",
|
|
1401
|
+
border_style="cyan"
|
|
1402
|
+
))
|
|
1403
|
+
|
|
1404
|
+
# List pages from router.dart
|
|
1405
|
+
_list_pages_from_router(project_dir, project_name)
|
|
1406
|
+
|
|
1407
|
+
# List domain models
|
|
1408
|
+
_list_domain_models(lib_path, cfg.domain_folder if cfg.domain_folder else "domain")
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def _list_pages_from_router(project_dir: Path, project_name: str) -> None:
|
|
1412
|
+
"""List all pages by parsing router.dart."""
|
|
1413
|
+
router_path = project_dir / "lib" / "router.dart"
|
|
1414
|
+
|
|
1415
|
+
if not router_path.exists():
|
|
1416
|
+
console.print()
|
|
1417
|
+
console.print("[dim]📄 No router.dart found[/dim]")
|
|
1418
|
+
return
|
|
1419
|
+
|
|
1420
|
+
pages = []
|
|
1421
|
+
try:
|
|
1422
|
+
with open(router_path, 'r') as f:
|
|
1423
|
+
content = f.read()
|
|
1424
|
+
|
|
1425
|
+
import re
|
|
1426
|
+
|
|
1427
|
+
# Extract imports for pages
|
|
1428
|
+
# Pattern: import 'package:{project_name}/features/{name}/{name}_page.dart';
|
|
1429
|
+
# Pattern: import 'package:{project_name}/features/{name}/{name}_screen.dart';
|
|
1430
|
+
import_pattern = rf"import\s+['\"]package:{re.escape(project_name)}/features/(\w+)/(\w+)_(?:page|screen)\.dart['\"];"
|
|
1431
|
+
|
|
1432
|
+
import_matches = re.finditer(import_pattern, content)
|
|
1433
|
+
page_classes = {} # class_name -> {page_name, file_path}
|
|
1434
|
+
|
|
1435
|
+
for match in import_matches:
|
|
1436
|
+
page_folder = match.group(1)
|
|
1437
|
+
page_file = match.group(2)
|
|
1438
|
+
file_type = 'screen' if 'screen' in match.group(0) else 'page'
|
|
1439
|
+
|
|
1440
|
+
# Determine class name from file name
|
|
1441
|
+
# home_page.dart -> HomePage, settings_page.dart -> SettingsPage
|
|
1442
|
+
class_name = ''.join(word.capitalize() for word in page_file.split('_')) + ('Screen' if file_type == 'screen' else 'Page')
|
|
1443
|
+
|
|
1444
|
+
page_classes[class_name] = {
|
|
1445
|
+
'page_name': page_folder,
|
|
1446
|
+
'file_path': f"lib/features/{page_folder}/{page_file}_{file_type}.dart",
|
|
1447
|
+
'file_type': file_type
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
# Extract routes from GoRoute
|
|
1451
|
+
# Pattern: GoRoute(path: HomePage.routeName, builder: ... => const HomePage(),)
|
|
1452
|
+
# Pattern: GoRoute(path: '/path', builder: ... => const ClassName(),)
|
|
1453
|
+
builder_pattern = r"builder:.*?const\s+(\w+)\s*\("
|
|
1454
|
+
|
|
1455
|
+
# Find all GoRoute blocks - need to handle nested parentheses
|
|
1456
|
+
# Strategy: find GoRoute( and then find matching closing paren
|
|
1457
|
+
go_route_starts = list(re.finditer(r"GoRoute\s*\(", content))
|
|
1458
|
+
|
|
1459
|
+
for start_match in go_route_starts:
|
|
1460
|
+
start_pos = start_match.end() - 1 # Position of opening paren
|
|
1461
|
+
# Find matching closing paren
|
|
1462
|
+
paren_count = 0
|
|
1463
|
+
end_pos = start_pos
|
|
1464
|
+
for i, char in enumerate(content[start_pos:], start_pos):
|
|
1465
|
+
if char == '(':
|
|
1466
|
+
paren_count += 1
|
|
1467
|
+
elif char == ')':
|
|
1468
|
+
paren_count -= 1
|
|
1469
|
+
if paren_count == 0:
|
|
1470
|
+
end_pos = i + 1
|
|
1471
|
+
break
|
|
1472
|
+
|
|
1473
|
+
if end_pos > start_pos:
|
|
1474
|
+
block_content = content[start_match.start():end_pos]
|
|
1475
|
+
|
|
1476
|
+
# Extract class name from builder
|
|
1477
|
+
class_match = re.search(builder_pattern, block_content, re.DOTALL)
|
|
1478
|
+
if not class_match:
|
|
1479
|
+
continue
|
|
1480
|
+
|
|
1481
|
+
class_name = class_match.group(1)
|
|
1482
|
+
|
|
1483
|
+
if class_name not in page_classes:
|
|
1484
|
+
continue
|
|
1485
|
+
|
|
1486
|
+
# Extract path
|
|
1487
|
+
path = None
|
|
1488
|
+
|
|
1489
|
+
# Try pattern: path: ClassName.routeName
|
|
1490
|
+
route_name_pattern = rf"path:\s*{re.escape(class_name)}\.routeName"
|
|
1491
|
+
if re.search(route_name_pattern, block_content):
|
|
1492
|
+
# Need to read the page file to get routeName value
|
|
1493
|
+
page_info = page_classes[class_name]
|
|
1494
|
+
page_file_path = project_dir / page_info['file_path']
|
|
1495
|
+
|
|
1496
|
+
if page_file_path.exists():
|
|
1497
|
+
try:
|
|
1498
|
+
page_content = page_file_path.read_text()
|
|
1499
|
+
route_name_match = re.search(r"static\s+const\s+String\s+routeName\s*=\s*['\"]([^'\"]+)['\"]", page_content)
|
|
1500
|
+
if route_name_match:
|
|
1501
|
+
path = route_name_match.group(1)
|
|
1502
|
+
except Exception:
|
|
1503
|
+
pass
|
|
1504
|
+
|
|
1505
|
+
# Try pattern: path: '/literal_path'
|
|
1506
|
+
if not path:
|
|
1507
|
+
literal_path_pattern = r"path:\s*['\"]([^'\"]+)['\"]"
|
|
1508
|
+
literal_match = re.search(literal_path_pattern, block_content)
|
|
1509
|
+
if literal_match:
|
|
1510
|
+
path = literal_match.group(1)
|
|
1511
|
+
|
|
1512
|
+
# Fallback: infer from class name
|
|
1513
|
+
if not path:
|
|
1514
|
+
page_info = page_classes[class_name]
|
|
1515
|
+
page_name = page_info['page_name']
|
|
1516
|
+
if page_name == 'home':
|
|
1517
|
+
path = '/home'
|
|
1518
|
+
elif page_name == 'splash':
|
|
1519
|
+
path = '/'
|
|
1520
|
+
else:
|
|
1521
|
+
path = f"/{page_name}"
|
|
1522
|
+
|
|
1523
|
+
page_info = page_classes[class_name]
|
|
1524
|
+
pages.append({
|
|
1525
|
+
'name': page_info['page_name'],
|
|
1526
|
+
'path': path,
|
|
1527
|
+
'class': class_name,
|
|
1528
|
+
'file_path': page_info['file_path']
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
except Exception as e:
|
|
1532
|
+
console.print(f"[yellow]⚠️ Could not parse router.dart: {e}[/yellow]")
|
|
1533
|
+
return
|
|
1534
|
+
|
|
1535
|
+
if pages:
|
|
1536
|
+
console.print()
|
|
1537
|
+
console.print("[bold blue]📄 Pages:[/bold blue]")
|
|
1538
|
+
for page in sorted(pages, key=lambda x: x['path']):
|
|
1539
|
+
console.print(f" [green]{page['path']:<20}[/green] → [cyan]{page['class']:<20}[/cyan] [dim]({page['file_path']})[/dim]")
|
|
1540
|
+
else:
|
|
1541
|
+
console.print()
|
|
1542
|
+
console.print("[dim]📄 No pages found in router.dart[/dim]")
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _list_domain_models(lib_path: Path, domain_folder: str) -> None:
|
|
1546
|
+
"""List all domain models."""
|
|
1547
|
+
models_info = find_domain_models_with_class_names(lib_path, domain_folder)
|
|
1548
|
+
|
|
1549
|
+
if models_info:
|
|
1550
|
+
console.print()
|
|
1551
|
+
console.print("[bold blue]📦 Domain Models:[/bold blue]")
|
|
1552
|
+
for file_stem in sorted(models_info.keys()):
|
|
1553
|
+
info = models_info[file_stem]
|
|
1554
|
+
model_path = f"lib/{domain_folder}/{info['folder']}/model/{file_stem}.dart"
|
|
1555
|
+
console.print(f" [cyan]{info['class_name']:<20}[/cyan] [dim]({model_path})[/dim]")
|
|
1556
|
+
else:
|
|
1557
|
+
console.print()
|
|
1558
|
+
console.print(f"[dim]📦 No domain models found in {domain_folder}/ folder[/dim]")
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
# Obsolete functions - kept for reference but not used anymore
|
|
1562
|
+
# The list command now only shows pages (from router.dart) and domain models
|
|
1563
|
+
|
|
1564
|
+
def _list_features(lib_path: Path) -> None:
|
|
1565
|
+
"""[OBSOLETE] List all features in the project - no longer used."""
|
|
1566
|
+
pass
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _list_pages(lib_path: Path) -> None:
|
|
1570
|
+
"""[OBSOLETE] List all simple pages - no longer used. Use _list_pages_from_router instead."""
|
|
1571
|
+
pass
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def _list_components(lib_path: Path) -> None:
|
|
1575
|
+
"""[OBSOLETE] List all components - no longer used."""
|
|
1576
|
+
pass
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _list_routes(project_dir: Path, project_name: str) -> None:
|
|
1580
|
+
"""[OBSOLETE] List all routes from router.dart - functionality merged into _list_pages_from_router."""
|
|
1581
|
+
pass
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
if __name__ == "__main__":
|
|
1585
|
+
cli()
|