aegis-stack 0.1.0__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.

Potentially problematic release.


This version of aegis-stack might be problematic. Click here for more details.

Files changed (103) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +374 -0
  3. aegis/core/CLAUDE.md +365 -0
  4. aegis/core/__init__.py +6 -0
  5. aegis/core/components.py +115 -0
  6. aegis/core/dependency_resolver.py +119 -0
  7. aegis/core/template_generator.py +163 -0
  8. aegis/templates/CLAUDE.md +306 -0
  9. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
  10. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
  11. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  12. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
  13. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
  14. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  15. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
  16. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  17. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  18. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  19. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
  20. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
  21. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
  22. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  23. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  24. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
  25. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
  26. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
  27. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
  28. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
  29. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
  30. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  31. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  32. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  33. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  34. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  35. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  100. aegis_stack-0.1.0.dist-info/METADATA +114 -0
  101. aegis_stack-0.1.0.dist-info/RECORD +103 -0
  102. aegis_stack-0.1.0.dist-info/WHEEL +4 -0
  103. aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
aegis/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ Aegis Stack CLI - Component generation and project management tools.
3
+ """
4
+
5
+ __version__ = "0.1.0"
aegis/__main__.py ADDED
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aegis Stack CLI - Main entry point
4
+
5
+ Usage:
6
+ aegis init PROJECT_NAME
7
+ aegis components
8
+ aegis --help
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ import typer
14
+
15
+ from aegis import __version__
16
+ from aegis.core.components import (
17
+ COMPONENTS,
18
+ ComponentSpec,
19
+ ComponentType,
20
+ get_components_by_type,
21
+ )
22
+ from aegis.core.dependency_resolver import DependencyResolver
23
+ from aegis.core.template_generator import TemplateGenerator
24
+
25
+ # Create the main Typer application
26
+ app = typer.Typer(
27
+ name="aegis",
28
+ help=(
29
+ "Aegis Stack CLI - Component generation and project management. "
30
+ "Available components: redis, worker, scheduler, database"
31
+ ),
32
+ add_completion=False,
33
+ )
34
+
35
+
36
+ @app.command()
37
+ def version() -> None:
38
+ """Show the Aegis Stack CLI version."""
39
+ typer.echo(f"Aegis Stack CLI v{__version__}")
40
+
41
+
42
+ @app.command()
43
+ def components() -> None:
44
+ """List available components and their dependencies."""
45
+
46
+ typer.echo("\nšŸ“¦ CORE COMPONENTS")
47
+ typer.echo("=" * 40)
48
+ typer.echo(" backend - FastAPI backend server (always included)")
49
+ typer.echo(" frontend - Flet frontend interface (always included)")
50
+
51
+ typer.echo("\nšŸ—ļø INFRASTRUCTURE COMPONENTS")
52
+ typer.echo("=" * 40)
53
+
54
+ infra_components = get_components_by_type(ComponentType.INFRASTRUCTURE)
55
+ for name, spec in infra_components.items():
56
+ typer.echo(f" {name:12} - {spec.description}")
57
+ if spec.requires:
58
+ typer.echo(f" Requires: {', '.join(spec.requires)}")
59
+ if spec.recommends:
60
+ typer.echo(f" Recommends: {', '.join(spec.recommends)}")
61
+
62
+ typer.echo(
63
+ "\nšŸ’” Use 'aegis init PROJECT_NAME --components redis,worker' "
64
+ "to select components"
65
+ )
66
+
67
+
68
+ def validate_and_resolve_components(
69
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
70
+ ) -> list[str] | None:
71
+ """Validate and resolve component dependencies."""
72
+ if not value:
73
+ return None
74
+
75
+ # Parse comma-separated string
76
+ components_raw = [c.strip() for c in value.split(",")]
77
+
78
+ # Check for empty components before filtering
79
+ if any(not c for c in components_raw):
80
+ typer.echo("āŒ Empty component name is not allowed", err=True)
81
+ raise typer.Exit(1)
82
+
83
+ selected = [c for c in components_raw if c]
84
+
85
+ # Validate components exist
86
+ errors = DependencyResolver.validate_components(selected)
87
+ if errors:
88
+ for error in errors:
89
+ typer.echo(f"āŒ {error}", err=True)
90
+ raise typer.Exit(1)
91
+
92
+ # Resolve dependencies
93
+ resolved = DependencyResolver.resolve_dependencies(selected)
94
+
95
+ # Show dependency resolution
96
+ auto_added = DependencyResolver.get_missing_dependencies(selected)
97
+ if auto_added:
98
+ typer.echo(f"šŸ“¦ Auto-added dependencies: {', '.join(auto_added)}")
99
+
100
+ # Show recommendations
101
+ recommendations = DependencyResolver.get_recommendations(resolved)
102
+ if recommendations:
103
+ rec_list = ", ".join(recommendations)
104
+ typer.echo(f"šŸ’” Recommended: {rec_list}")
105
+ # Note: Skip interactive recommendations for now to keep it simple
106
+
107
+ return resolved
108
+
109
+
110
+ def validate_project_name(project_name: str) -> None:
111
+ """Validate project name and raise typer.Exit if invalid."""
112
+ import re
113
+
114
+ # Check for invalid characters (only allow letters, numbers, hyphens,
115
+ # underscores)
116
+ if not re.match(r"^[a-zA-Z0-9_-]+$", project_name):
117
+ typer.echo(
118
+ "āŒ Invalid project name. Only letters, numbers, hyphens, and "
119
+ "underscores are allowed.",
120
+ err=True,
121
+ )
122
+ raise typer.Exit(1)
123
+
124
+ # Check for reserved names
125
+ reserved_names = {"aegis", "aegis-stack"}
126
+ if project_name.lower() in reserved_names:
127
+ typer.echo(f"āŒ '{project_name}' is a reserved name.", err=True)
128
+ raise typer.Exit(1)
129
+
130
+ # Check length limit
131
+ if len(project_name) > 50:
132
+ typer.echo("āŒ Project name too long. Maximum 50 characters allowed.", err=True)
133
+ raise typer.Exit(1)
134
+
135
+
136
+ @app.command()
137
+ def init(
138
+ project_name: str = typer.Argument(
139
+ ..., help="Name of the new Aegis Stack project to create"
140
+ ),
141
+ components: str | None = typer.Option(
142
+ None,
143
+ "--components",
144
+ "-c",
145
+ callback=validate_and_resolve_components,
146
+ help="Comma-separated list of components (redis,worker,scheduler,database)",
147
+ ),
148
+ interactive: bool = typer.Option(
149
+ True,
150
+ "--interactive/--no-interactive",
151
+ "-i/-ni",
152
+ help="Use interactive component selection",
153
+ ),
154
+ force: bool = typer.Option(
155
+ False, "--force", "-f", help="Overwrite existing directory if it exists"
156
+ ),
157
+ output_dir: str | None = typer.Option(
158
+ None,
159
+ "--output-dir",
160
+ "-o",
161
+ help="Directory to create the project in (default: current directory)",
162
+ ),
163
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
164
+ ) -> None:
165
+ """
166
+ Initialize a new Aegis Stack project with battle-tested component combinations.
167
+
168
+ This command creates a complete project structure with your chosen components,
169
+ ensuring all dependencies and configurations are compatible and tested.
170
+
171
+ Examples:\n
172
+ - aegis init my-app\n
173
+ - aegis init my-app --components redis,worker\n
174
+ - aegis init my-app --components redis,worker,scheduler,database --no-interactive\n
175
+ """ # noqa
176
+
177
+ # Validate project name first
178
+ validate_project_name(project_name)
179
+
180
+ typer.echo("šŸ›”ļø Aegis Stack Project Initialization")
181
+ typer.echo("=" * 50)
182
+
183
+ # Determine output directory
184
+ base_output_dir = Path(output_dir) if output_dir else Path.cwd()
185
+ project_path = base_output_dir / project_name
186
+
187
+ typer.echo(f"šŸ“ Project will be created in: {project_path.resolve()}")
188
+
189
+ # Check if directory already exists
190
+ if project_path.exists():
191
+ if not force:
192
+ typer.echo(f"āŒ Directory '{project_path}' already exists", err=True)
193
+ typer.echo(
194
+ " Use --force to overwrite or choose a different name", err=True
195
+ )
196
+ raise typer.Exit(1)
197
+ else:
198
+ typer.echo(f"āš ļø Overwriting existing directory: {project_path}")
199
+
200
+ # Interactive component selection
201
+ selected_components = components if components else []
202
+
203
+ if interactive and not components:
204
+ selected_components = interactive_component_selection()
205
+
206
+ # Resolve dependencies for interactively selected components
207
+ if selected_components:
208
+ selected_components = DependencyResolver.resolve_dependencies(
209
+ selected_components
210
+ )
211
+
212
+ auto_added = DependencyResolver.get_missing_dependencies(
213
+ [c for c in selected_components if c not in ["backend", "frontend"]]
214
+ )
215
+ if auto_added:
216
+ typer.echo(f"\nšŸ“¦ Auto-added dependencies: {', '.join(auto_added)}")
217
+
218
+ # Create template generator
219
+ template_gen = TemplateGenerator(project_name, list(selected_components))
220
+
221
+ # Show selected configuration
222
+ typer.echo()
223
+ typer.echo(f"šŸ“ Project Name: {project_name}")
224
+ typer.echo("šŸ—ļø Project Structure:")
225
+ typer.echo(" āœ… Core: backend, frontend")
226
+
227
+ # Show infrastructure components
228
+ infra_components = [
229
+ name
230
+ for name in selected_components
231
+ if name in COMPONENTS and COMPONENTS[name].type == ComponentType.INFRASTRUCTURE
232
+ ]
233
+ if infra_components:
234
+ typer.echo(f" šŸ“¦ Infrastructure: {', '.join(infra_components)}")
235
+
236
+ # Show template files that will be generated
237
+ template_files = template_gen.get_template_files()
238
+ if template_files:
239
+ typer.echo("\nšŸ“„ Component Files:")
240
+ for file_path in template_files:
241
+ typer.echo(f" • {file_path}")
242
+
243
+ # Show entrypoints that will be created
244
+ entrypoints = template_gen.get_entrypoints()
245
+ if entrypoints:
246
+ typer.echo("\nšŸš€ Entrypoints:")
247
+ for entrypoint in entrypoints:
248
+ typer.echo(f" • {entrypoint}")
249
+
250
+ # Show worker queues that will be created
251
+ worker_queues = template_gen.get_worker_queues()
252
+ if worker_queues:
253
+ typer.echo("\nšŸ‘· Worker Queues:")
254
+ for queue in worker_queues:
255
+ typer.echo(f" • {queue}")
256
+
257
+ # Show dependency information using template generator
258
+ deps = template_gen._get_pyproject_deps()
259
+ if deps:
260
+ typer.echo("\nšŸ“¦ Dependencies to be installed:")
261
+ for dep in deps:
262
+ typer.echo(f" • {dep}")
263
+
264
+ # Confirm before proceeding
265
+ typer.echo()
266
+ if not yes and not typer.confirm("šŸš€ Create this project?"):
267
+ typer.echo("āŒ Project creation cancelled")
268
+ raise typer.Exit(0)
269
+
270
+ # Create project using cookiecutter
271
+ typer.echo()
272
+ typer.echo(f"šŸ”§ Creating project: {project_name}")
273
+
274
+ try:
275
+ from cookiecutter.main import cookiecutter
276
+
277
+ # Get the template path
278
+ template_path = (
279
+ Path(__file__).parent / "templates" / "cookiecutter-aegis-project"
280
+ )
281
+
282
+ # Use template generator for context
283
+ extra_context = template_gen.get_template_context()
284
+
285
+ # Generate project with cookiecutter
286
+ cookiecutter(
287
+ str(template_path),
288
+ extra_context=extra_context,
289
+ output_dir=str(base_output_dir),
290
+ no_input=True, # Don't prompt user, use our context
291
+ overwrite_if_exists=force,
292
+ )
293
+
294
+ typer.echo("āœ… Project created successfully!")
295
+
296
+ # Show next steps
297
+ typer.echo()
298
+ typer.echo("šŸ“‹ Next steps:")
299
+ typer.echo(f" cd {project_path.resolve()}")
300
+ typer.echo(" uv sync")
301
+ typer.echo(" cp .env.example .env")
302
+ typer.echo(" make run-local")
303
+
304
+ except ImportError:
305
+ typer.echo("āŒ Error: cookiecutter not installed", err=True)
306
+ raise typer.Exit(1)
307
+ except Exception as e:
308
+ typer.echo(f"āŒ Error creating project: {e}", err=True)
309
+ raise typer.Exit(1)
310
+
311
+
312
+ def get_interactive_infrastructure_components() -> list[ComponentSpec]:
313
+ """Get infrastructure components available for interactive selection."""
314
+ # Get all infrastructure components
315
+ infra_components = []
316
+ for component_spec in COMPONENTS.values():
317
+ if component_spec.type == ComponentType.INFRASTRUCTURE:
318
+ infra_components.append(component_spec)
319
+
320
+ # Sort by name for consistent ordering
321
+ return sorted(infra_components, key=lambda x: x.name)
322
+
323
+
324
+ def interactive_component_selection() -> list[str]:
325
+ """Interactive component selection with dependency awareness."""
326
+
327
+ typer.echo("šŸŽÆ Component Selection")
328
+ typer.echo("=" * 40)
329
+ typer.echo("āœ… Core components (backend + frontend) included automatically\n")
330
+
331
+ selected = []
332
+
333
+ # Get all infrastructure components from registry
334
+ infra_components = get_interactive_infrastructure_components()
335
+
336
+ typer.echo("šŸ—ļø Infrastructure Components:")
337
+
338
+ # Process components in a specific order to handle dependencies
339
+ component_order = ["redis", "worker", "scheduler", "database"]
340
+
341
+ for component_name in component_order:
342
+ # Find the component spec
343
+ component_spec = next(
344
+ (c for c in infra_components if c.name == component_name), None
345
+ )
346
+ if not component_spec:
347
+ continue # Skip if component doesn't exist in registry
348
+
349
+ # Handle special worker dependency logic
350
+ if component_name == "worker":
351
+ if "redis" in selected:
352
+ # Redis already selected, simple worker prompt
353
+ prompt = f" Add {component_spec.description.lower()}?"
354
+ if typer.confirm(prompt):
355
+ selected.append("worker")
356
+ else:
357
+ # Redis not selected, offer to add both
358
+ prompt = (
359
+ f" Add {component_spec.description.lower()}? (will auto-add Redis)"
360
+ )
361
+ if typer.confirm(prompt):
362
+ selected.extend(["redis", "worker"])
363
+ else:
364
+ # Standard prompt for other components
365
+ prompt = f" Add {component_spec.description}?"
366
+ if typer.confirm(prompt):
367
+ selected.append(component_name)
368
+
369
+ return selected
370
+
371
+
372
+ # This is what runs when you do: aegis
373
+ if __name__ == "__main__":
374
+ app()