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.
Files changed (172) hide show
  1. coregen.py +1585 -0
  2. coregen_cli-3.1.6.dist-info/METADATA +1211 -0
  3. coregen_cli-3.1.6.dist-info/RECORD +172 -0
  4. coregen_cli-3.1.6.dist-info/WHEEL +5 -0
  5. coregen_cli-3.1.6.dist-info/entry_points.txt +2 -0
  6. coregen_cli-3.1.6.dist-info/top_level.txt +2 -0
  7. generators/__init__.py +1 -0
  8. generators/assets/__init__.py +1 -0
  9. generators/assets/assets.py +30 -0
  10. generators/assets/main.py +4 -0
  11. generators/config/__init__.py +1 -0
  12. generators/config/analisy_options.py +42 -0
  13. generators/config/main.py +11 -0
  14. generators/config/pubspec.py +139 -0
  15. generators/cursor/__init__.py +3 -0
  16. generators/cursor/setup.py +87 -0
  17. generators/helpers/__init__.py +56 -0
  18. generators/helpers/component.py +1030 -0
  19. generators/helpers/config.py +255 -0
  20. generators/helpers/data_source.py +180 -0
  21. generators/helpers/domain.py +469 -0
  22. generators/helpers/feature.py +654 -0
  23. generators/helpers/navigation.py +274 -0
  24. generators/helpers/page.py +63 -0
  25. generators/helpers/project.py +33 -0
  26. generators/helpers/utils.py +338 -0
  27. generators/helpers/validation.py +364 -0
  28. generators/initializator.py +68 -0
  29. generators/main.py +81 -0
  30. generators/static/assets/logo.png +0 -0
  31. generators/static/assets/mock/auth_user.json +5 -0
  32. generators/static/assets/svgs/apple.svg +4 -0
  33. generators/static/assets/svgs/google.svg +2 -0
  34. generators/static/cursor/AGENTS.md.jinja +37 -0
  35. generators/static/cursor/agents/doc-writer.md +20 -0
  36. generators/static/cursor/agents/epic-orchestrator.md +28 -0
  37. generators/static/cursor/agents/feature-implementer.md +29 -0
  38. generators/static/cursor/agents/integration-wiring.md +30 -0
  39. generators/static/cursor/agents/layer-application.md +21 -0
  40. generators/static/cursor/agents/layer-guardian.md +21 -0
  41. generators/static/cursor/agents/layer-infrastructure.md +31 -0
  42. generators/static/cursor/agents/layer-model.md +24 -0
  43. generators/static/cursor/agents/layer-presentation.md +33 -0
  44. generators/static/cursor/docs/architecture/APIS_AND_INTEGRATION.md.jinja +42 -0
  45. generators/static/cursor/docs/architecture/CARAVAGGIO_COMPONENTS.md.jinja +317 -0
  46. generators/static/cursor/docs/architecture/DDD_LAYERS.md.jinja +32 -0
  47. generators/static/cursor/docs/architecture/FILE_TEMPLATES.md.jinja +76 -0
  48. generators/static/cursor/docs/architecture/MOCK_AND_REMOTE_DATA.md.jinja +87 -0
  49. generators/static/cursor/docs/architecture/REFERENCE_IMPLEMENTATIONS.md.jinja +48 -0
  50. generators/static/cursor/docs/architecture/WIDGETS_AND_CARAVAGGIO.md.jinja +76 -0
  51. generators/static/cursor/docs/epics/README.md +17 -0
  52. generators/static/cursor/lib/domain/AGENTS.md +23 -0
  53. generators/static/cursor/lib/features/AGENTS.md +23 -0
  54. generators/static/cursor/lib/widgets/AGENTS.md +21 -0
  55. generators/static/cursor/rules/apis-layer.mdc.jinja +21 -0
  56. generators/static/cursor/rules/application-layer.mdc.jinja +12 -0
  57. generators/static/cursor/rules/architecture-core.mdc.jinja +43 -0
  58. generators/static/cursor/rules/dart-conventions.mdc.jinja +14 -0
  59. generators/static/cursor/rules/domain-layer.mdc.jinja +24 -0
  60. generators/static/cursor/rules/login-context.mdc.jinja +16 -0
  61. generators/static/cursor/rules/presentation-layer.mdc.jinja +15 -0
  62. generators/static/cursor/rules/quality-gate.mdc.jinja +14 -0
  63. generators/static/cursor/rules/ui-caravaggio.mdc.jinja +22 -0
  64. generators/static/cursor/skills/caravaggio-ui/SKILL.md +138 -0
  65. generators/static/cursor/skills/epic-delivery/SKILL.md +74 -0
  66. generators/static/cursor/skills/epic-delivery/templates/epic-status.md +14 -0
  67. generators/static/cursor/skills/epic-delivery/templates/epic.md +36 -0
  68. generators/static/templates/apis/common/constants_template.jinja +48 -0
  69. generators/static/templates/apis/common/data_source_config_template.jinja +47 -0
  70. generators/static/templates/apis/core/api_injectable_module_template.jinja +14 -0
  71. generators/static/templates/apis/interceptors/api_logger_template.jinja +135 -0
  72. generators/static/templates/apis/interceptors/auth_interceptor_template.jinja +207 -0
  73. generators/static/templates/auth/application/auth_bloc_template.jinja +36 -0
  74. generators/static/templates/auth/application/auth_event_template.jinja +7 -0
  75. generators/static/templates/auth/application/auth_state_template.jinja +8 -0
  76. generators/static/templates/auth/infrastructure/auth_facade_module_template.jinja +16 -0
  77. generators/static/templates/auth/infrastructure/auth_facade_template.jinja +288 -0
  78. generators/static/templates/auth/infrastructure/mock_auth_facade_template.jinja +106 -0
  79. generators/static/templates/auth/model/auth_failure_template.jinja +11 -0
  80. generators/static/templates/auth/model/i_auth_facade_template.jinja +24 -0
  81. generators/static/templates/auth/model/user_template.jinja +34 -0
  82. generators/static/templates/auth/model/value_objects_template.jinja +54 -0
  83. generators/static/templates/auth/presentation/login_screen_template.jinja +24 -0
  84. generators/static/templates/auth/sign_in_form/application/sign_in_form_bloc_template.jinja +139 -0
  85. generators/static/templates/auth/sign_in_form/application/sign_in_form_event_template.jinja +15 -0
  86. generators/static/templates/auth/sign_in_form/application/sign_in_form_state_template.jinja +26 -0
  87. generators/static/templates/auth/sign_in_form/presentation/apple_form_template.jinja +16 -0
  88. generators/static/templates/auth/sign_in_form/presentation/email_password_form_template_INACTIVE.jinja +94 -0
  89. generators/static/templates/auth/sign_in_form/presentation/google_form_template.jinja +15 -0
  90. generators/static/templates/auth/sign_in_form/presentation/sign_in_form_template.jinja +381 -0
  91. generators/static/templates/component/component_bloc_template.jinja +49 -0
  92. generators/static/templates/component/component_event_template.jinja +7 -0
  93. generators/static/templates/component/component_form_bloc_template.jinja +56 -0
  94. generators/static/templates/component/component_form_event_template.jinja +7 -0
  95. generators/static/templates/component/component_form_state_template.jinja +18 -0
  96. generators/static/templates/component/component_form_widget_template.jinja +130 -0
  97. generators/static/templates/component/component_list_widget_template.jinja +95 -0
  98. generators/static/templates/component/component_state_template.jinja +9 -0
  99. generators/static/templates/component/component_widget_template.jinja +113 -0
  100. generators/static/templates/core/bloc/base_form_bloc_template.jinja +32 -0
  101. generators/static/templates/core/errors/error_localizer_template.jinja +61 -0
  102. generators/static/templates/core/infrastructure/base_mapper_template.jinja +21 -0
  103. generators/static/templates/core/infrastructure/base_repository_mixin_template.jinja +78 -0
  104. generators/static/templates/core/infrastructure/firebase_injectable_module_template.jinja +11 -0
  105. generators/static/templates/core/infrastructure/firestore_helpers_template.jinja +22 -0
  106. generators/static/templates/core/infrastructure/repository_error_handler_template.jinja +44 -0
  107. generators/static/templates/core/model/common_interfaces_template.jinja +3 -0
  108. generators/static/templates/core/model/entity_template.jinja +5 -0
  109. generators/static/templates/core/model/errors_template.jinja +15 -0
  110. generators/static/templates/core/model/failures_template.jinja +97 -0
  111. generators/static/templates/core/model/value_objects_template.jinja +135 -0
  112. generators/static/templates/core/model/value_validators_template.jinja +105 -0
  113. generators/static/templates/core/presentation/app_drawer_template.jinja +52 -0
  114. generators/static/templates/core/presentation/app_widget_template.jinja +41 -0
  115. generators/static/templates/core/presentation/bottom_nav_bar_template.jinja +32 -0
  116. generators/static/templates/domain/domain_mapper_template.jinja +39 -0
  117. generators/static/templates/domain/domain_remote_service_template.jinja +46 -0
  118. generators/static/templates/domain/domain_repository_template.jinja +90 -0
  119. generators/static/templates/domain/domain_service_module_template.jinja +23 -0
  120. generators/static/templates/domain/enum_template.jinja +3 -0
  121. generators/static/templates/domain/i_domain_service_template.jinja +18 -0
  122. generators/static/templates/domain/mock_domain_service_template.jinja +73 -0
  123. generators/static/templates/feature/feature_bloc_template.jinja +104 -0
  124. generators/static/templates/feature/feature_dto_template.jinja +26 -0
  125. generators/static/templates/feature/feature_entity_template.jinja +23 -0
  126. generators/static/templates/feature/feature_event_template.jinja +20 -0
  127. generators/static/templates/feature/feature_extensions_template.jinja +25 -0
  128. generators/static/templates/feature/feature_failure_template.jinja +11 -0
  129. generators/static/templates/feature/feature_page_template.jinja +113 -0
  130. generators/static/templates/feature/feature_repository_template.jinja +112 -0
  131. generators/static/templates/feature/feature_state_template.jinja +20 -0
  132. generators/static/templates/feature/i_feature_repository_template.jinja +21 -0
  133. generators/static/templates/feature/value_object_field_template.jinja +0 -0
  134. generators/static/templates/feature/value_object_template.jinja +0 -0
  135. generators/static/templates/feature/value_validators_template.jinja +57 -0
  136. generators/static/templates/home/home_screen_template.jinja +68 -0
  137. generators/static/templates/home/presentation/home_screen_template.jinja +52 -0
  138. generators/static/templates/home/screen_template.jinja +12 -0
  139. generators/static/templates/injection_template.jinja +8 -0
  140. generators/static/templates/logging/analytics_logging_template.jinja +42 -0
  141. generators/static/templates/logging/console_template.jinja +50 -0
  142. generators/static/templates/logging/logger_injectable_module_template.jinja +24 -0
  143. generators/static/templates/logging/logger_template.jinja +118 -0
  144. generators/static/templates/main_template.jinja +43 -0
  145. generators/static/templates/page_template.jinja +29 -0
  146. generators/static/templates/router_template.jinja +26 -0
  147. generators/static/templates/splash/presentation/splash_screen_auth_template.jinja +97 -0
  148. generators/static/templates/splash/presentation/splash_screen_template.jinja +51 -0
  149. generators/static/templates/storage/storage_repository_template.jinja +80 -0
  150. generators/static/templates/user_profile/infrastructure/user_profile_dto_template.jinja +27 -0
  151. generators/static/templates/user_profile/infrastructure/user_profile_mapper_template.jinja +43 -0
  152. generators/static/templates/user_profile/infrastructure/user_profile_repository_template.jinja +82 -0
  153. generators/static/templates/user_profile/infrastructure/user_profile_service_template.jinja +27 -0
  154. generators/static/templates/user_profile/model/i_user_profile_repository_template.jinja +34 -0
  155. generators/static/templates/user_profile/model/user_profile_failure_template.jinja +11 -0
  156. generators/static/templates/user_profile/model/user_profile_template.jinja +31 -0
  157. generators/static/templates/widgets/common/custom_scaffold_template.jinja +52 -0
  158. generators/static/templates/widgets/common/error_widget_template.jinja +22 -0
  159. generators/static/templates/widgets/common/loading_widget_template.jinja +14 -0
  160. generators/static/templates/widgets/common/unknown_state_widget_template.jinja +23 -0
  161. generators/templates/__init__.py +1 -0
  162. generators/templates/_core/core_generator.py +121 -0
  163. generators/templates/apis/apis_generator.py +50 -0
  164. generators/templates/auth/auth_generator.py +40 -0
  165. generators/templates/auth/sign_in_form_generator.py +22 -0
  166. generators/templates/copier.py +51 -0
  167. generators/templates/home/home_generator.py +26 -0
  168. generators/templates/lib/lib_generator.py +40 -0
  169. generators/templates/logging/logging_generator.py +28 -0
  170. generators/templates/main.py +75 -0
  171. generators/templates/splash/splash_generator.py +20 -0
  172. 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()