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
@@ -0,0 +1,654 @@
1
+ """Feature generation functions"""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional, List, Dict
6
+ from generators.templates.copier import generate_file
7
+ from .utils import map_field_type, map_field_type_to_dto, to_pascal_case, to_pascal_case_preserve
8
+
9
+
10
+ def create_feature_layers(feature_dir: Path, feature_name: str, field_list: list[dict], project_name: str, folder: Optional[str], lib_path: Optional[Path] = None, domain_folder: str = "domain") -> None:
11
+ """Create all layers for a feature"""
12
+ # Build the import path prefix
13
+ if folder:
14
+ import_prefix = f"{folder.replace('/', '/')}/{feature_name}"
15
+ else:
16
+ import_prefix = feature_name
17
+
18
+ # Discover enums for enum-aware generation
19
+ known_enums: set = set()
20
+ enums_info: Dict[str, dict] = {}
21
+ _lib = lib_path or (feature_dir.parent.parent if feature_dir.parent.exists() else None)
22
+ if _lib and (_lib / domain_folder / "enums").exists():
23
+ enums_info = find_enums_with_info(_lib, domain_folder)
24
+ known_enums = set(enums_info.keys())
25
+
26
+ # Model layer
27
+ model_dir = feature_dir / "model"
28
+ model_dir.mkdir(exist_ok=True)
29
+
30
+ # Create value objects and validators
31
+ generate_value_objects_and_validators(import_prefix, field_list, model_dir, project_name, known_enums=known_enums)
32
+
33
+ # Create entity (domain model)
34
+ entity_fields = []
35
+ enum_imports = []
36
+ for field in field_list:
37
+ field_name = field['name']
38
+ field_type = field['type']
39
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
40
+ if field_name == 'id':
41
+ entity_fields.append(f" required UniqueId id,")
42
+ elif base_ft in known_enums:
43
+ entity_fields.append(f" required {field_type} {field_name},")
44
+ info = enums_info.get(base_ft)
45
+ if info:
46
+ ei = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
47
+ if ei not in enum_imports:
48
+ enum_imports.append(ei)
49
+ else:
50
+ entity_fields.append(f" required {to_pascal_case_preserve(field_name)} {field_name},")
51
+
52
+ entity_import = f"import 'package:{project_name}/{import_prefix}/model/value_objects.dart';"
53
+ if enum_imports:
54
+ entity_import = entity_import + "\n" + "\n".join(enum_imports)
55
+
56
+ entity_fields_str = "\n".join(entity_fields)
57
+ generate_file(project_name, model_dir, "feature/feature_entity_template.jinja", f"{feature_name}.dart", {
58
+ "feature_name": feature_name,
59
+ "fields": entity_fields_str,
60
+ "entity_import": entity_import
61
+ })
62
+
63
+ # Create failure
64
+ generate_file(project_name, model_dir, "feature/feature_failure_template.jinja", f"{feature_name}_failure.dart", {"feature_name": feature_name})
65
+
66
+ # Infrastructure layer
67
+ infra_dir = feature_dir / "infrastructure"
68
+ infra_dir.mkdir(exist_ok=True)
69
+
70
+ # Create DTO (enum -> String in DTO)
71
+ dto_fields = ",\n".join([f" required {map_field_type_to_dto(field['type'], known_enums=known_enums) if known_enums else map_field_type(field['type'])} {field['name']}" for field in field_list])
72
+ generate_file(project_name, infra_dir, "feature/feature_dto_template.jinja", f"{feature_name}_dto.dart", {"feature_name": feature_name, "fields": dto_fields})
73
+
74
+ # Create extensions for DTO-Domain conversions
75
+ generate_extensions(feature_name, field_list, infra_dir, project_name, import_prefix, known_enums=known_enums, enums_info=enums_info)
76
+
77
+ i_repo_import = f"""import 'package:{project_name}/{import_prefix}/model/{feature_name}.dart';
78
+ import 'package:{project_name}/{import_prefix}/model/{feature_name}_failure.dart';
79
+ """
80
+
81
+ # Create repository interface
82
+ generate_file(project_name, model_dir, "feature/i_feature_repository_template.jinja", f"i_{feature_name}_repository.dart", {
83
+ "feature_name": feature_name,
84
+ "i_repo_import": i_repo_import
85
+ })
86
+
87
+ repo_import = f"""import 'package:{project_name}/{import_prefix}/infrastructure/{feature_name}_extensions.dart';
88
+ import 'package:{project_name}/{import_prefix}/model/{feature_name}.dart';
89
+ import 'package:{project_name}/{import_prefix}/infrastructure/{feature_name}_dto.dart';
90
+ import 'package:{project_name}/{import_prefix}/model/{feature_name}_failure.dart';
91
+ import 'package:{project_name}/{import_prefix}/model/i_{feature_name}_repository.dart';
92
+ """
93
+
94
+ # Create repository implementation
95
+ generate_file(project_name, infra_dir, "feature/feature_repository_template.jinja", f"{feature_name}_repository.dart", {
96
+ "feature_name": feature_name,
97
+ "repo_import": repo_import
98
+ })
99
+
100
+ # Application layer
101
+ app_dir = feature_dir / "application"
102
+ app_dir.mkdir(exist_ok=True)
103
+
104
+ # Create BLoC files
105
+ generate_file(project_name, app_dir, "feature/feature_event_template.jinja", f"{feature_name}_event.dart", {"feature_name": feature_name})
106
+ generate_file(project_name, app_dir, "feature/feature_state_template.jinja", f"{feature_name}_state.dart", {"feature_name": feature_name})
107
+ generate_file(project_name, app_dir, "feature/feature_bloc_template.jinja", f"{feature_name}_bloc.dart", {
108
+ "feature_name": feature_name,
109
+ "feature_import_prefix": f"{project_name}/{import_prefix}"
110
+ })
111
+
112
+ # Presentation layer
113
+ presentation_dir = feature_dir / "presentation"
114
+ presentation_dir.mkdir(exist_ok=True)
115
+
116
+ # Create page
117
+ generate_file(project_name, presentation_dir, "feature/feature_page_template.jinja", f"{feature_name}_page.dart", {
118
+ "feature_name": feature_name,
119
+ "model_import_prefix": f"{project_name}/{import_prefix}",
120
+ "model_file_stem": feature_name,
121
+ "model_class_pascal": to_pascal_case_preserve(feature_name),
122
+ })
123
+
124
+
125
+ def generate_value_objects_and_validators(import_prefix: str, field_list: list[dict], model_dir: Path, project_name: str, known_enums: Optional[set] = None, skip_local_validators: bool = False) -> None:
126
+ """Generate consolidated value objects and optionally validators.
127
+
128
+ When skip_local_validators is True (e.g. for domain entities), value_validators.dart
129
+ is not generated; value_objects use right(input) with a TODO pointing to core validators.
130
+ """
131
+ value_objects_content = generate_consolidated_value_objects(
132
+ import_prefix, field_list, project_name,
133
+ known_enums=known_enums,
134
+ use_placeholder_validation=skip_local_validators,
135
+ )
136
+ (model_dir / "value_objects.dart").write_text(value_objects_content)
137
+
138
+ if not skip_local_validators:
139
+ generate_value_validators(field_list, model_dir, project_name, known_enums=known_enums)
140
+
141
+
142
+ def generate_consolidated_value_objects(import_prefix: str, field_list: list[dict], project_name: str, known_enums: Optional[set] = None, use_placeholder_validation: bool = False) -> str:
143
+ """Generate consolidated value objects file.
144
+
145
+ When use_placeholder_validation is True, value_objects do not import local
146
+ value_validators; each factory uses right(input) with a TODO to core validators.
147
+ """
148
+ imports = f"import 'package:dartz/dartz.dart';\n"
149
+ imports += f"import 'package:{project_name}/core/model/failures.dart';\n"
150
+ imports += f"import 'package:{project_name}/core/model/value_objects.dart';\n"
151
+ if not use_placeholder_validation:
152
+ imports += f"import 'package:{project_name}/{import_prefix}/model/value_validators.dart';\n"
153
+ imports += "\n"
154
+
155
+ _enums = known_enums or set()
156
+
157
+ field_vos = []
158
+ for field in field_list:
159
+ field_name = field['name']
160
+ field_type = field['type']
161
+
162
+ if field_name == 'id':
163
+ continue
164
+
165
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
166
+
167
+ if base_ft in _enums:
168
+ continue
169
+
170
+ if '<' in base_ft or (base_ft and base_ft[0].isupper() and base_ft not in ['String', 'DateTime']):
171
+ continue
172
+
173
+ mapped_field_type = map_field_type(base_ft)
174
+ capitalized_name = to_pascal_case_preserve(field_name)
175
+
176
+ if use_placeholder_validation:
177
+ factory_body = (
178
+ f" // TODO: Validate input here. See core/model/value_validators.dart for examples.\n"
179
+ f" return {capitalized_name}._(right(input));"
180
+ )
181
+ else:
182
+ factory_body = f" return {capitalized_name}._(validate{capitalized_name}(input));"
183
+
184
+ field_vo = f"""
185
+ class {capitalized_name} extends ValueObject<{mapped_field_type}> {{
186
+ @override
187
+ final Either<ValueFailure<{mapped_field_type}>, {mapped_field_type}> value;
188
+
189
+ factory {capitalized_name}({mapped_field_type} input) {{
190
+ {factory_body}
191
+ }}
192
+
193
+ const {capitalized_name}._(this.value);
194
+ }}
195
+ """
196
+ field_vos.append(field_vo)
197
+
198
+ return imports + "\n".join(field_vos)
199
+
200
+
201
+ def generate_value_validators(field_list: list[dict], model_dir: Path, project_name: str, known_enums: Optional[set] = None) -> None:
202
+ """Generate value validators file using Jinja template"""
203
+ _enums = known_enums or set()
204
+ # Pre-process field_list: add pascal_name and strip nullable suffix so
205
+ # the template matches base types (e.g., String not String?)
206
+ processed_field_list = []
207
+ for field in field_list:
208
+ base_ft = field['type'][:-1] if field['type'].endswith('?') else field['type']
209
+ # Skip enums — they don't need validators
210
+ if base_ft in _enums:
211
+ continue
212
+ field_dict = dict(field) # Create a copy
213
+ field_dict['pascal_name'] = to_pascal_case_preserve(field['name'])
214
+ if field_dict['type'].endswith('?'):
215
+ field_dict['type'] = field_dict['type'][:-1]
216
+ processed_field_list.append(field_dict)
217
+
218
+ generate_file(project_name, model_dir, "feature/value_validators_template.jinja", "value_validators.dart", {
219
+ "project_name": project_name,
220
+ "field_list": processed_field_list
221
+ })
222
+
223
+
224
+ def generate_extensions(feature_name: str, field_list: list[dict], infra_dir: Path, project_name: str, import_prefix: str, known_enums: Optional[set] = None, enums_info: Optional[Dict[str, dict]] = None) -> None:
225
+ """Generate DTO-Domain conversion extensions"""
226
+ _enums = known_enums or set()
227
+ _enums_info = enums_info or {}
228
+
229
+ # Generate toDto() method
230
+ to_dto_fields = []
231
+ for field in field_list:
232
+ field_name = field['name']
233
+ field_type = field['type']
234
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
235
+ if field_name == 'id':
236
+ to_dto_fields.append(f" id: id.getOrCrash()")
237
+ elif base_ft in _enums:
238
+ to_dto_fields.append(f" {field_name}: {field_name}.name")
239
+ else:
240
+ to_dto_fields.append(f" {field_name}: {field_name}.getOrCrash()")
241
+
242
+ # Generate fromDto() method
243
+ from_dto_fields = []
244
+ for field in field_list:
245
+ field_name = field['name']
246
+ field_type = field['type']
247
+ base_ft = field_type[:-1] if field_type.endswith('?') else field_type
248
+ if field_name == 'id':
249
+ from_dto_fields.append(f" id: UniqueId.fromUniqueString(id)")
250
+ elif base_ft in _enums:
251
+ from_dto_fields.append(f" {field_name}: {base_ft}.values.byName({field_name})")
252
+ else:
253
+ capitalized_name = to_pascal_case_preserve(field_name)
254
+ from_dto_fields.append(f" {field_name}: {capitalized_name}({field_name})")
255
+
256
+ # Build enum imports for extensions
257
+ enum_ext_imports = []
258
+ for field in field_list:
259
+ ft = field['type']
260
+ base_ft = ft[:-1] if ft.endswith('?') else ft
261
+ if base_ft in _enums:
262
+ info = _enums_info.get(base_ft)
263
+ if info:
264
+ ei = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
265
+ if ei not in enum_ext_imports:
266
+ enum_ext_imports.append(ei)
267
+
268
+ extra_imports = "\n".join(enum_ext_imports)
269
+ if extra_imports:
270
+ extra_imports = "\n" + extra_imports
271
+
272
+ extension_content = f"""import 'package:{project_name}/core/model/value_objects.dart';
273
+ import 'package:{project_name}/{import_prefix}/model/{feature_name}.dart';
274
+ import 'package:{project_name}/{import_prefix}/model/value_objects.dart';
275
+ import 'package:{project_name}/{import_prefix}/infrastructure/{feature_name}_dto.dart';{extra_imports}
276
+
277
+ extension {feature_name.capitalize()}DtoX on {feature_name.capitalize()}Dto {{
278
+ {feature_name.capitalize()} toDomain() {{
279
+ return {feature_name.capitalize()}(
280
+ {','.join(from_dto_fields)}
281
+ );
282
+ }}
283
+ }}
284
+
285
+ extension {feature_name.capitalize()}DomainX on {feature_name.capitalize()} {{
286
+ {feature_name.capitalize()}Dto toDto() {{
287
+ return {feature_name.capitalize()}Dto(
288
+ {','.join(to_dto_fields)}
289
+ );
290
+ }}
291
+ }}
292
+
293
+ """
294
+ (infra_dir / f"{feature_name}_extensions.dart").write_text(extension_content)
295
+
296
+
297
+ def _is_entity_file(file_path: Path) -> bool:
298
+ """Check if a .dart file contains a freezed entity class definition.
299
+
300
+ Excludes failure files, interfaces, value objects, validators,
301
+ and generated files (.freezed.dart, .g.dart).
302
+ """
303
+ name = file_path.name
304
+ if name.endswith('.freezed.dart') or name.endswith('.g.dart'):
305
+ return False
306
+ if name.startswith('i_') or name.endswith('_failure.dart'):
307
+ return False
308
+ if name in ('value_objects.dart', 'value_validators.dart', 'common_interfaces.dart'):
309
+ return False
310
+
311
+ try:
312
+ content = file_path.read_text()
313
+ return bool(re.search(r'abstract class (\w+)\s+with\s+_\$', content))
314
+ except Exception:
315
+ return False
316
+
317
+
318
+ def _get_class_name_from_file(file_path: Path) -> Optional[str]:
319
+ """Extract the freezed class name from a .dart entity file."""
320
+ try:
321
+ content = file_path.read_text()
322
+ match = re.search(r'abstract class (\w+)\s+with', content)
323
+ if match:
324
+ return match.group(1)
325
+ except Exception:
326
+ pass
327
+ return None
328
+
329
+
330
+ def find_domain_models(lib_path: Path, domain_folder: str) -> List[str]:
331
+ """Find all available domain models in the domain folder.
332
+
333
+ Scans lib/{domain_folder}/ subdirectories for model/ folders containing
334
+ entity .dart files. Discovers entities even when the file name differs
335
+ from the containing folder (e.g., user_profile.dart inside auth/).
336
+
337
+ Args:
338
+ lib_path: Path to lib/ directory
339
+ domain_folder: Name of domain folder (e.g., 'domain')
340
+
341
+ Returns:
342
+ List of model file stems (e.g., ['test', 'user_profile'])
343
+ """
344
+ models_map = find_domain_models_with_class_names(lib_path, domain_folder)
345
+ return sorted(models_map.keys())
346
+
347
+
348
+ def get_domain_model_class_name(lib_path: Path, domain_folder: str, folder_name: str) -> Optional[str]:
349
+ """Get the class name (PascalCase) from a domain model entity file.
350
+
351
+ First checks for an entity file matching folder_name, then scans
352
+ all entity files in the folder's model/ directory.
353
+
354
+ Args:
355
+ lib_path: Path to lib/ directory
356
+ domain_folder: Name of domain folder (e.g., 'domain')
357
+ folder_name: Folder name or file stem of the model (snake_case)
358
+
359
+ Returns:
360
+ Class name in PascalCase (e.g., 'TodoItem') or None if not found
361
+ """
362
+ domain_path = lib_path / domain_folder
363
+
364
+ # Try exact match first: folder_name is the file stem
365
+ for item in domain_path.iterdir():
366
+ if item.is_dir():
367
+ candidate = item / "model" / f"{folder_name}.dart"
368
+ if candidate.exists():
369
+ return _get_class_name_from_file(candidate)
370
+
371
+ return None
372
+
373
+
374
+ def find_domain_models_with_class_names(lib_path: Path, domain_folder: str) -> Dict[str, dict]:
375
+ """Find all available domain models with their class names and folder info.
376
+
377
+ Scans all model/ subdirectories for entity files, including entities
378
+ whose file name differs from the containing folder.
379
+
380
+ Args:
381
+ lib_path: Path to lib/ directory
382
+ domain_folder: Name of domain folder (e.g., 'domain')
383
+
384
+ Returns:
385
+ Dictionary mapping file stems to model info dicts:
386
+ {
387
+ 'test': {'class_name': 'Test', 'folder': 'test'},
388
+ 'user_profile': {'class_name': 'UserProfile', 'folder': 'auth'},
389
+ }
390
+ """
391
+ domain_path = lib_path / domain_folder
392
+
393
+ if not domain_path.exists():
394
+ return {}
395
+
396
+ models_map = {}
397
+ for item in domain_path.iterdir():
398
+ if item.is_dir():
399
+ model_dir = item / "model"
400
+ if not (model_dir.exists() and model_dir.is_dir()):
401
+ continue
402
+ for dart_file in model_dir.iterdir():
403
+ if dart_file.suffix == '.dart' and _is_entity_file(dart_file):
404
+ class_name = _get_class_name_from_file(dart_file)
405
+ if class_name:
406
+ file_stem = dart_file.stem
407
+ models_map[file_stem] = {
408
+ 'class_name': class_name,
409
+ 'folder': item.name,
410
+ }
411
+
412
+ return models_map
413
+
414
+
415
+ def find_enums(lib_path: Path, domain_folder: str) -> List[str]:
416
+ """Find all available enum names in lib/<domain_folder>/enums/.
417
+
418
+ Args:
419
+ lib_path: Path to lib/ directory
420
+ domain_folder: Name of domain folder (e.g., 'domain')
421
+
422
+ Returns:
423
+ Sorted list of enum names (PascalCase, e.g., ['EventStatus', 'Priority'])
424
+ """
425
+ enums_info = find_enums_with_info(lib_path, domain_folder)
426
+ return sorted(enums_info.keys())
427
+
428
+
429
+ def find_enums_with_info(lib_path: Path, domain_folder: str) -> Dict[str, dict]:
430
+ """Find all enums with their file stems and folder info.
431
+
432
+ Scans lib/<domain_folder>/enums/ for .dart files containing ``enum Name {``.
433
+
434
+ Args:
435
+ lib_path: Path to lib/ directory
436
+ domain_folder: Name of domain folder (e.g., 'domain')
437
+
438
+ Returns:
439
+ Dictionary mapping enum name to info dict::
440
+
441
+ {
442
+ 'EventStatus': {
443
+ 'file_stem': 'event_status',
444
+ 'folder': 'domain/enums',
445
+ },
446
+ }
447
+ """
448
+ enums_dir = lib_path / domain_folder / "enums"
449
+
450
+ if not enums_dir.exists():
451
+ return {}
452
+
453
+ enums_map: Dict[str, dict] = {}
454
+ for dart_file in enums_dir.iterdir():
455
+ if dart_file.suffix != '.dart':
456
+ continue
457
+ try:
458
+ content = dart_file.read_text()
459
+ except Exception:
460
+ continue
461
+ for match in re.finditer(r'enum\s+(\w+)\s*\{', content):
462
+ enum_name = match.group(1)
463
+ enums_map[enum_name] = {
464
+ 'file_stem': dart_file.stem,
465
+ 'folder': f"{domain_folder}/enums",
466
+ }
467
+
468
+ return enums_map
469
+
470
+
471
+ def create_presentation_feature_layers(feature_dir: Path, feature_name: str, domain_model_name: str, domain_folder: str, project_name: str, folder: Optional[str], domain_model_folder: Optional[str] = None) -> None:
472
+ """Create only application and presentation layers for a feature.
473
+
474
+ This function creates a feature that uses an existing domain model.
475
+ It does NOT create model or infrastructure layers - those should
476
+ already exist in the domain folder.
477
+
478
+ Args:
479
+ feature_dir: Path to feature directory
480
+ feature_name: Name of the feature
481
+ domain_model_name: Name of the domain model to use (file stem)
482
+ domain_folder: Name of domain folder (e.g., 'domain')
483
+ project_name: Name of the project
484
+ folder: Optional folder path for the feature
485
+ domain_model_folder: Containing folder for the model. Defaults to domain_model_name.
486
+ """
487
+ if domain_model_folder is None:
488
+ domain_model_folder = domain_model_name
489
+
490
+ # Build import paths
491
+ if folder:
492
+ feature_import_prefix = f"{folder.replace('/', '/')}/{feature_name}"
493
+ else:
494
+ feature_import_prefix = feature_name
495
+
496
+ domain_import_prefix = f"{domain_folder}/{domain_model_folder}"
497
+
498
+ # Application layer
499
+ app_dir = feature_dir / "application"
500
+ app_dir.mkdir(exist_ok=True)
501
+
502
+ # Get PascalCase names
503
+ # Use to_pascal_case_preserve to handle camelCase names like "todoPage" -> "TodoPage"
504
+ feature_pascal = to_pascal_case_preserve(feature_name)
505
+ domain_model_pascal = to_pascal_case_preserve(domain_model_name)
506
+
507
+ # Build Freezed mixin names correctly (e.g., _$TodoPageEvent)
508
+ freezed_mixin_event = "_$" + feature_pascal + "Event"
509
+ freezed_mixin_state = "_$" + feature_pascal + "State"
510
+
511
+ # Create event file that uses domain model for item types
512
+ event_content = f"""/*
513
+ * BLoC events template for feature operations
514
+ * Defines immutable events using Freezed that trigger BLoC actions:
515
+ * - LoadRequested: Fetch all items
516
+ * - CreateRequested: Create new item
517
+ * - UpdateRequested: Update existing item
518
+ * - DeleteRequested: Remove item by ID
519
+ *
520
+ * Events represent user intentions and trigger state changes
521
+ */
522
+
523
+ part of '{feature_name}_bloc.dart';
524
+
525
+ @freezed
526
+ abstract class {feature_pascal}Event with {freezed_mixin_event} {{
527
+ const factory {feature_pascal}Event.loadRequested() = LoadRequested;
528
+ const factory {feature_pascal}Event.createRequested({domain_model_pascal} item) = CreateRequested;
529
+ const factory {feature_pascal}Event.updateRequested({domain_model_pascal} item) = UpdateRequested;
530
+ const factory {feature_pascal}Event.deleteRequested(String id) = DeleteRequested;
531
+ }}
532
+ """
533
+ (app_dir / f"{feature_name}_event.dart").write_text(event_content)
534
+
535
+ # Create state file that uses domain model for list types
536
+ state_content = f"""/*
537
+ * BLoC states template for feature UI representation
538
+ * Defines immutable states using Freezed for different UI states:
539
+ * - Initial: App just started, no data loaded
540
+ * - Loading: Data is being fetched/processed
541
+ * - Loaded: Data successfully retrieved with items list
542
+ * - Error: Something went wrong with error message
543
+ *
544
+ * States represent the current condition of the feature
545
+ */
546
+
547
+ part of '{feature_name}_bloc.dart';
548
+
549
+ @freezed
550
+ abstract class {feature_pascal}State with {freezed_mixin_state} {{
551
+ const factory {feature_pascal}State.initial() = Initial;
552
+ const factory {feature_pascal}State.loading() = Loading;
553
+ const factory {feature_pascal}State.loaded(List<{domain_model_pascal}> items) = Loaded;
554
+ const factory {feature_pascal}State.error(String message) = Error;
555
+ }}
556
+ """
557
+ (app_dir / f"{feature_name}_state.dart").write_text(state_content)
558
+
559
+ # Create BLoC that uses domain repository
560
+ bloc_content = f"""import 'package:bloc/bloc.dart';
561
+ import 'package:dartz/dartz.dart';
562
+ import 'package:freezed_annotation/freezed_annotation.dart';
563
+ import 'package:injectable/injectable.dart';
564
+ import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}.dart';
565
+ import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}_failure.dart';
566
+ import 'package:{project_name}/{domain_import_prefix}/model/i_{domain_model_name}_repository.dart';
567
+
568
+ part '{feature_name}_bloc.freezed.dart';
569
+ part '{feature_name}_event.dart';
570
+ part '{feature_name}_state.dart';
571
+
572
+ class {feature_pascal}Bloc extends Bloc<{feature_pascal}Event, {feature_pascal}State> {{
573
+ final I{domain_model_pascal}Repository _repository;
574
+
575
+ {feature_pascal}Bloc(this._repository) : super(const {feature_pascal}State.initial()) {{
576
+ on<LoadRequested>(_onLoadRequested);
577
+ on<CreateRequested>(_onCreateRequested);
578
+ on<UpdateRequested>(_onUpdateRequested);
579
+ on<DeleteRequested>(_onDeleteRequested);
580
+ }}
581
+
582
+ Future<void> _onLoadRequested(LoadRequested event, Emitter<{feature_pascal}State> emit) async {{
583
+ emit(const {feature_pascal}State.loading());
584
+ final Either<{domain_model_pascal}Failure, List<{domain_model_pascal}>> result =
585
+ await _repository.getAll();
586
+ result.fold(
587
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
588
+ (List<{domain_model_pascal}> items) => emit({feature_pascal}State.loaded(items)),
589
+ );
590
+ }}
591
+
592
+ Future<void> _onCreateRequested(CreateRequested event, Emitter<{feature_pascal}State> emit) async {{
593
+ emit(const {feature_pascal}State.loading());
594
+ final Either<{domain_model_pascal}Failure, Unit> result = await _repository.create(event.item);
595
+ result.fold(
596
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
597
+ (Unit _) async {{
598
+ final Either<{domain_model_pascal}Failure, List<{domain_model_pascal}>> itemsResult =
599
+ await _repository.getAll();
600
+ itemsResult.fold(
601
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
602
+ (List<{domain_model_pascal}> items) => emit({feature_pascal}State.loaded(items)),
603
+ );
604
+ }},
605
+ );
606
+ }}
607
+
608
+ Future<void> _onUpdateRequested(UpdateRequested event, Emitter<{feature_pascal}State> emit) async {{
609
+ emit(const {feature_pascal}State.loading());
610
+ final Either<{domain_model_pascal}Failure, Unit> result = await _repository.update(event.item);
611
+ result.fold(
612
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
613
+ (Unit _) async {{
614
+ final Either<{domain_model_pascal}Failure, List<{domain_model_pascal}>> itemsResult =
615
+ await _repository.getAll();
616
+ itemsResult.fold(
617
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
618
+ (List<{domain_model_pascal}> items) => emit({feature_pascal}State.loaded(items)),
619
+ );
620
+ }},
621
+ );
622
+ }}
623
+
624
+ Future<void> _onDeleteRequested(DeleteRequested event, Emitter<{feature_pascal}State> emit) async {{
625
+ emit(const {feature_pascal}State.loading());
626
+ final Either<{domain_model_pascal}Failure, Unit> result = await _repository.delete(event.id);
627
+ result.fold(
628
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
629
+ (Unit _) async {{
630
+ final Either<{domain_model_pascal}Failure, List<{domain_model_pascal}>> itemsResult =
631
+ await _repository.getAll();
632
+ itemsResult.fold(
633
+ ({domain_model_pascal}Failure failure) => emit({feature_pascal}State.error(failure.toString())),
634
+ (List<{domain_model_pascal}> items) => emit({feature_pascal}State.loaded(items)),
635
+ );
636
+ }},
637
+ );
638
+ }}
639
+ }}
640
+ """
641
+ (app_dir / f"{feature_name}_bloc.dart").write_text(bloc_content)
642
+
643
+ # Presentation layer
644
+ presentation_dir = feature_dir / "presentation"
645
+ presentation_dir.mkdir(exist_ok=True)
646
+
647
+ # Create page
648
+ generate_file(project_name, presentation_dir, "feature/feature_page_template.jinja", f"{feature_name}_page.dart", {
649
+ "feature_name": feature_name,
650
+ "model_import_prefix": f"{project_name}/{domain_import_prefix}",
651
+ "model_file_stem": domain_model_name,
652
+ "model_class_pascal": domain_model_pascal,
653
+ })
654
+