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/core/CLAUDE.md ADDED
@@ -0,0 +1,365 @@
1
+ # CLI Core Development Guide
2
+
3
+ This guide covers CLI development patterns for Aegis Stack's core command system.
4
+
5
+ ## Component System Architecture
6
+
7
+ ### Component Registry (`components.py`)
8
+ The component system uses a centralized registry with typed component specifications:
9
+
10
+ ```python
11
+ @dataclass
12
+ class ComponentSpec:
13
+ """Specification for a component."""
14
+ name: str
15
+ description: str
16
+ type: ComponentType
17
+ requires: list[str] = field(default_factory=list)
18
+ recommends: list[str] = field(default_factory=list)
19
+ conflicts: list[str] = field(default_factory=list)
20
+
21
+ # Component registry
22
+ COMPONENTS: dict[str, ComponentSpec] = {
23
+ "redis": ComponentSpec(
24
+ name="redis",
25
+ description="Redis server for caching and message queues",
26
+ type=ComponentType.INFRASTRUCTURE,
27
+ ),
28
+ "worker": ComponentSpec(
29
+ name="worker",
30
+ description="Background task processing with arq",
31
+ type=ComponentType.INFRASTRUCTURE,
32
+ requires=["redis"],
33
+ ),
34
+ "scheduler": ComponentSpec(
35
+ name="scheduler",
36
+ description="Scheduled task execution with APScheduler",
37
+ type=ComponentType.INFRASTRUCTURE,
38
+ ),
39
+ }
40
+ ```
41
+
42
+ ### Component Types
43
+ ```python
44
+ class ComponentType(Enum):
45
+ """Component types for organization."""
46
+ INFRASTRUCTURE = "infrastructure" # Redis, workers, databases
47
+ SERVICE = "service" # Future: API services, microservices
48
+ INTEGRATION = "integration" # Future: External integrations
49
+ ```
50
+
51
+ ## Dependency Resolution Patterns
52
+
53
+ ### Dependency Resolver (`dependency_resolver.py`)
54
+ The dependency resolver handles component relationships and validation:
55
+
56
+ ```python
57
+ class DependencyResolver:
58
+ """Resolves component dependencies and validates selections."""
59
+
60
+ @staticmethod
61
+ def validate_components(components: list[str]) -> list[str]:
62
+ """Validate component names and return errors."""
63
+ errors = []
64
+ for component in components:
65
+ if component not in COMPONENTS:
66
+ available = list(COMPONENTS.keys())
67
+ # Suggest similar components
68
+ suggestion = DependencyResolver._suggest_component(component, available)
69
+ if suggestion:
70
+ errors.append(f"Unknown component '{component}'. Did you mean '{suggestion}'?")
71
+ else:
72
+ errors.append(f"Unknown component '{component}'. Available: {', '.join(available)}")
73
+ return errors
74
+
75
+ @staticmethod
76
+ def resolve_dependencies(selected: list[str]) -> list[str]:
77
+ """Resolve all dependencies for selected components."""
78
+ resolved = set(selected)
79
+
80
+ # Add required dependencies
81
+ queue = list(selected)
82
+ while queue:
83
+ component = queue.pop(0)
84
+ if component in COMPONENTS:
85
+ for required in COMPONENTS[component].requires:
86
+ if required not in resolved:
87
+ resolved.add(required)
88
+ queue.append(required)
89
+
90
+ return sorted(resolved)
91
+ ```
92
+
93
+ ### Validation Patterns
94
+ ```python
95
+ def validate_project_name(project_name: str) -> None:
96
+ """Validate project name and raise typer.Exit if invalid."""
97
+ import re
98
+
99
+ # Check for invalid characters
100
+ if not re.match(r"^[a-zA-Z0-9_-]+$", project_name):
101
+ typer.echo("❌ Invalid project name. Only letters, numbers, hyphens, and underscores are allowed.", err=True)
102
+ raise typer.Exit(1)
103
+
104
+ # Check for reserved names
105
+ reserved_names = {"aegis", "aegis-stack"}
106
+ if project_name.lower() in reserved_names:
107
+ typer.echo(f"❌ '{project_name}' is a reserved name.", err=True)
108
+ raise typer.Exit(1)
109
+
110
+ # Check length limit
111
+ if len(project_name) > 50:
112
+ typer.echo("❌ Project name too long. Maximum 50 characters allowed.", err=True)
113
+ raise typer.Exit(1)
114
+ ```
115
+
116
+ ## Template Generation Logic
117
+
118
+ ### Template Generator (`template_generator.py`)
119
+ The template generator converts component selections into cookiecutter context:
120
+
121
+ ```python
122
+ class TemplateGenerator:
123
+ """Generates template context from component selections."""
124
+
125
+ def __init__(self, project_name: str, components: list[str]):
126
+ self.project_name = project_name
127
+ self.project_slug = self._generate_slug(project_name)
128
+ self.components = components
129
+
130
+ def get_template_context(self) -> dict[str, Any]:
131
+ """Generate cookiecutter context from component selections."""
132
+ return {
133
+ "project_name": self.project_name,
134
+ "project_slug": self.project_slug,
135
+ "project_description": f"A production-ready Python application",
136
+ "author_name": "Developer",
137
+ "author_email": "dev@example.com",
138
+ "version": "0.1.0",
139
+ "python_version": "3.11",
140
+ "include_scheduler": "yes" if "scheduler" in self.components else "no",
141
+ "include_worker": "yes" if "worker" in self.components else "no",
142
+ "include_database": "yes" if "database" in self.components else "no",
143
+ "include_cache": "yes" if "cache" in self.components else "no",
144
+ }
145
+
146
+ def get_template_files(self) -> list[str]:
147
+ """Get list of template files that will be generated."""
148
+ files = []
149
+
150
+ # Core files (always included)
151
+ files.extend([
152
+ "app/components/backend/main.py",
153
+ "app/components/frontend/main.py",
154
+ "app/core/config.py",
155
+ "app/integrations/main.py",
156
+ ])
157
+
158
+ # Component-specific files
159
+ if "scheduler" in self.components:
160
+ files.extend([
161
+ "app/components/scheduler/main.py",
162
+ "app/entrypoints/scheduler.py",
163
+ "tests/components/test_scheduler.py",
164
+ ])
165
+
166
+ if "worker" in self.components:
167
+ files.extend([
168
+ "app/components/worker/queues/system.py",
169
+ "app/components/worker/tasks/system_tasks.py",
170
+ "app/services/load_test.py",
171
+ "tests/services/test_worker_health_registration.py",
172
+ ])
173
+
174
+ return sorted(files)
175
+ ```
176
+
177
+ ## CLI Command Structure
178
+
179
+ ### Command Definitions (`__main__.py`)
180
+ Commands use Typer with proper validation and error handling:
181
+
182
+ ```python
183
+ @app.command()
184
+ def init(
185
+ project_name: str = typer.Argument(..., help="Name of the new Aegis Stack project to create"),
186
+ components: str | None = typer.Option(
187
+ None,
188
+ "--components", "-c",
189
+ callback=validate_and_resolve_components,
190
+ help="Comma-separated list of components (redis,worker,scheduler)",
191
+ ),
192
+ interactive: bool = typer.Option(
193
+ True, "--interactive/--no-interactive", "-i/-ni",
194
+ help="Use interactive component selection"
195
+ ),
196
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing directory if it exists"),
197
+ output_dir: str | None = typer.Option(None, "--output-dir", "-o", help="Directory to create the project in"),
198
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
199
+ ) -> None:
200
+ """Initialize a new Aegis Stack project with battle-tested component combinations."""
201
+
202
+ # Validate project name first
203
+ validate_project_name(project_name)
204
+
205
+ # Show project configuration
206
+ show_project_configuration(project_name, components, output_dir)
207
+
208
+ # Confirm before proceeding
209
+ if not yes and not typer.confirm("🚀 Create this project?"):
210
+ typer.echo("❌ Project creation cancelled")
211
+ raise typer.Exit(0)
212
+
213
+ # Create project using cookiecutter
214
+ create_project_with_cookiecutter(project_name, components, output_dir, force)
215
+ ```
216
+
217
+ ### Interactive Selection Patterns
218
+ ```python
219
+ def interactive_component_selection() -> list[str]:
220
+ """Interactive component selection with dependency awareness."""
221
+
222
+ typer.echo("🎯 Component Selection")
223
+ typer.echo("=" * 40)
224
+ typer.echo("✅ Core components (backend + frontend) included automatically\\n")
225
+
226
+ selected = []
227
+
228
+ # Infrastructure components
229
+ typer.echo("🏗️ Infrastructure Components:")
230
+ if typer.confirm(" Add Redis (caching, message queues)?"):
231
+ selected.append("redis")
232
+
233
+ if "redis" in selected:
234
+ if typer.confirm(" Add worker infrastructure (background tasks)?"):
235
+ selected.append("worker")
236
+ else:
237
+ if typer.confirm(" Add worker infrastructure? (will auto-add Redis)"):
238
+ selected.extend(["redis", "worker"])
239
+
240
+ if typer.confirm(" Add scheduler infrastructure (scheduled tasks)?"):
241
+ selected.append("scheduler")
242
+
243
+ return selected
244
+ ```
245
+
246
+ ## CLI Error Handling Patterns
247
+
248
+ ### Validation Callbacks
249
+ ```python
250
+ def validate_and_resolve_components(
251
+ ctx: typer.Context, param: typer.CallbackParam, value: str | None
252
+ ) -> list[str] | None:
253
+ """Validate and resolve component dependencies."""
254
+ if not value:
255
+ return None
256
+
257
+ # Parse comma-separated string
258
+ components_raw = [c.strip() for c in value.split(",")]
259
+
260
+ # Check for empty components
261
+ if any(not c for c in components_raw):
262
+ typer.echo("❌ Empty component name is not allowed", err=True)
263
+ raise typer.Exit(1)
264
+
265
+ selected = [c for c in components_raw if c]
266
+
267
+ # Validate components exist
268
+ errors = DependencyResolver.validate_components(selected)
269
+ if errors:
270
+ for error in errors:
271
+ typer.echo(f"❌ {error}", err=True)
272
+ raise typer.Exit(1)
273
+
274
+ # Resolve dependencies
275
+ resolved = DependencyResolver.resolve_dependencies(selected)
276
+
277
+ # Show dependency resolution
278
+ auto_added = DependencyResolver.get_missing_dependencies(selected)
279
+ if auto_added:
280
+ typer.echo(f"📦 Auto-added dependencies: {', '.join(auto_added)}")
281
+
282
+ return resolved
283
+ ```
284
+
285
+ ### Error Message Quality
286
+ ```python
287
+ def show_helpful_error(component: str, available: list[str]) -> None:
288
+ """Show helpful error message with suggestions."""
289
+ # Suggest similar components using fuzzy matching
290
+ suggestion = find_closest_match(component, available)
291
+
292
+ if suggestion:
293
+ typer.echo(f"❌ Unknown component '{component}'. Did you mean '{suggestion}'?", err=True)
294
+ else:
295
+ typer.echo(f"❌ Unknown component '{component}'.", err=True)
296
+
297
+ typer.echo(f" Available components: {', '.join(available)}", err=True)
298
+ typer.echo(" Use 'aegis components' to see detailed information", err=True)
299
+ ```
300
+
301
+ ## CLI Development Best Practices
302
+
303
+ ### Command Design
304
+ 1. **Clear naming** - Use descriptive command and option names
305
+ 2. **Helpful descriptions** - Provide clear help text for all commands
306
+ 3. **Sensible defaults** - Choose good default values for options
307
+ 4. **Progressive disclosure** - Show basic options first, advanced options with help
308
+ 5. **Consistent patterns** - Use similar patterns across commands
309
+
310
+ ### Validation Strategy
311
+ 1. **Early validation** - Validate inputs as early as possible
312
+ 2. **Clear error messages** - Provide actionable error messages
313
+ 3. **Helpful suggestions** - Suggest corrections when possible
314
+ 4. **Context-aware errors** - Show relevant information in errors
315
+ 5. **Graceful degradation** - Handle edge cases gracefully
316
+
317
+ ### User Experience
318
+ 1. **Interactive guidance** - Provide interactive help when possible
319
+ 2. **Visual feedback** - Use emojis and formatting for clarity
320
+ 3. **Progress indication** - Show progress for long-running operations
321
+ 4. **Confirmation prompts** - Ask before destructive operations
322
+ 5. **Escape hatches** - Provide ways to cancel or undo operations
323
+
324
+ ### Code Organization
325
+ 1. **Separation of concerns** - Keep CLI logic separate from business logic
326
+ 2. **Reusable functions** - Extract common patterns into functions
327
+ 3. **Type safety** - Use proper type hints throughout
328
+ 4. **Error handling** - Handle all possible error conditions
329
+ 5. **Testing support** - Design code to be easily testable
330
+
331
+ ## CLI Testing Patterns
332
+
333
+ ### Command Testing
334
+ ```python
335
+ def test_component_validation():
336
+ """Test component validation logic."""
337
+ # Valid components
338
+ assert validate_components(["worker", "scheduler"]) == []
339
+
340
+ # Invalid components
341
+ errors = validate_components(["invalid"])
342
+ assert "Unknown component 'invalid'" in errors[0]
343
+
344
+ # Suggestions
345
+ errors = validate_components(["schedul"])
346
+ assert "Did you mean 'scheduler'?" in errors[0]
347
+ ```
348
+
349
+ ### Integration Testing
350
+ ```python
351
+ def test_project_generation():
352
+ """Test full project generation workflow."""
353
+ with tempfile.TemporaryDirectory() as temp_dir:
354
+ result = run_aegis_init(
355
+ "test-project",
356
+ ["worker"],
357
+ Path(temp_dir)
358
+ )
359
+
360
+ assert result.success
361
+ assert (result.project_path / "app" / "components" / "worker").exists()
362
+ assert (result.project_path / "docker-compose.yml").exists()
363
+ ```
364
+
365
+ This approach ensures the CLI is maintainable, user-friendly, and follows established patterns for command-line tool development.
aegis/core/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ Core modules for Aegis Stack CLI.
3
+
4
+ This package contains the foundational components for the CLI system,
5
+ including component definitions, dependency resolution, and template generation.
6
+ """
@@ -0,0 +1,115 @@
1
+ """
2
+ Component registry and specifications for Aegis Stack.
3
+
4
+ This module defines all available components, their dependencies, and metadata
5
+ used for project generation and validation.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+
11
+
12
+ class ComponentType(Enum):
13
+ """Component type classifications."""
14
+
15
+ CORE = "core" # Always included (backend, frontend)
16
+ INFRASTRUCTURE = "infra" # Redis, workers - foundation for services to use
17
+
18
+
19
+ @dataclass
20
+ class ComponentSpec:
21
+ """Specification for a single component."""
22
+
23
+ name: str
24
+ type: ComponentType
25
+ description: str
26
+ requires: list[str] | None = None # Hard dependencies
27
+ recommends: list[str] | None = None # Soft dependencies
28
+ conflicts: list[str] | None = None # Mutual exclusions
29
+ docker_services: list[str] | None = None
30
+ pyproject_deps: list[str] | None = None
31
+ template_files: list[str] | None = None
32
+
33
+ def __post_init__(self) -> None:
34
+ """Ensure all list fields are initialized."""
35
+ if self.requires is None:
36
+ self.requires = []
37
+ if self.recommends is None:
38
+ self.recommends = []
39
+ if self.conflicts is None:
40
+ self.conflicts = []
41
+ if self.docker_services is None:
42
+ self.docker_services = []
43
+ if self.pyproject_deps is None:
44
+ self.pyproject_deps = []
45
+ if self.template_files is None:
46
+ self.template_files = []
47
+
48
+
49
+ # Component registry - single source of truth
50
+ COMPONENTS: dict[str, ComponentSpec] = {
51
+ "backend": ComponentSpec(
52
+ name="backend",
53
+ type=ComponentType.CORE,
54
+ description="FastAPI backend server",
55
+ pyproject_deps=["fastapi==0.116.1", "uvicorn==0.35.0"],
56
+ template_files=["app/components/backend/"],
57
+ ),
58
+ "frontend": ComponentSpec(
59
+ name="frontend",
60
+ type=ComponentType.CORE,
61
+ description="Flet frontend interface",
62
+ pyproject_deps=["flet==0.28.3"],
63
+ template_files=["app/components/frontend/"],
64
+ ),
65
+ "redis": ComponentSpec(
66
+ name="redis",
67
+ type=ComponentType.INFRASTRUCTURE,
68
+ description="Redis cache and message broker",
69
+ docker_services=["redis"],
70
+ pyproject_deps=["redis==5.0.8"],
71
+ ),
72
+ "worker": ComponentSpec(
73
+ name="worker",
74
+ type=ComponentType.INFRASTRUCTURE,
75
+ description="Background task processing infrastructure with arq",
76
+ requires=["redis"], # Hard dependency
77
+ pyproject_deps=["arq==0.25.0"],
78
+ docker_services=["worker-system", "worker-load-test"],
79
+ template_files=["app/components/worker/"],
80
+ ),
81
+ "scheduler": ComponentSpec(
82
+ name="scheduler",
83
+ type=ComponentType.INFRASTRUCTURE,
84
+ description="Scheduled task execution infrastructure",
85
+ pyproject_deps=["apscheduler==3.10.4"],
86
+ docker_services=["scheduler"],
87
+ template_files=["app/components/scheduler.py", "app/entrypoints/scheduler.py"],
88
+ ),
89
+ "database": ComponentSpec(
90
+ name="database",
91
+ type=ComponentType.INFRASTRUCTURE,
92
+ description="SQLite database with SQLModel ORM",
93
+ pyproject_deps=["sqlmodel>=0.0.14", "sqlalchemy>=2.0.0", "aiosqlite>=0.19.0"],
94
+ template_files=["app/core/db.py"],
95
+ ),
96
+ }
97
+
98
+
99
+ def get_component(name: str) -> ComponentSpec:
100
+ """Get component specification by name."""
101
+ if name not in COMPONENTS:
102
+ raise ValueError(f"Unknown component: {name}")
103
+ return COMPONENTS[name]
104
+
105
+
106
+ def get_components_by_type(component_type: ComponentType) -> dict[str, ComponentSpec]:
107
+ """Get all components of a specific type."""
108
+ return {
109
+ name: spec for name, spec in COMPONENTS.items() if spec.type == component_type
110
+ }
111
+
112
+
113
+ def list_available_components() -> list[str]:
114
+ """Get list of all available component names."""
115
+ return list(COMPONENTS.keys())
@@ -0,0 +1,119 @@
1
+ """
2
+ Component dependency resolution for Aegis Stack.
3
+
4
+ This module handles dependency resolution, validation, and recommendations
5
+ for component selection during project generation.
6
+ """
7
+
8
+ from .components import COMPONENTS
9
+
10
+
11
+ class DependencyResolver:
12
+ """Handles component dependency resolution and validation."""
13
+
14
+ @staticmethod
15
+ def resolve_dependencies(selected_components: list[str]) -> list[str]:
16
+ """
17
+ Resolve all dependencies and return final component list.
18
+
19
+ Args:
20
+ selected_components: List of component names selected by user
21
+
22
+ Returns:
23
+ Complete list of components including dependencies
24
+
25
+ Raises:
26
+ ValueError: If any selected components are invalid
27
+ """
28
+ # Validate all components first
29
+ errors = DependencyResolver.validate_components(selected_components)
30
+ if errors:
31
+ raise ValueError(f"Invalid components: {'; '.join(errors)}")
32
+
33
+ resolved = set(selected_components)
34
+
35
+ # Resolve hard dependencies recursively
36
+ while True:
37
+ before_size = len(resolved)
38
+ for component_name in list(resolved):
39
+ if component_name not in COMPONENTS:
40
+ continue
41
+
42
+ component = COMPONENTS[component_name]
43
+ if component.requires:
44
+ resolved.update(component.requires)
45
+
46
+ if len(resolved) == before_size:
47
+ break # No new dependencies added
48
+
49
+ return sorted(resolved)
50
+
51
+ @staticmethod
52
+ def validate_components(components: list[str]) -> list[str]:
53
+ """
54
+ Validate component selection and return errors.
55
+
56
+ Args:
57
+ components: List of component names to validate
58
+
59
+ Returns:
60
+ List of error messages (empty if valid)
61
+ """
62
+ errors = []
63
+
64
+ for component in components:
65
+ if component not in COMPONENTS:
66
+ errors.append(f"Unknown component: {component}")
67
+ continue
68
+
69
+ spec = COMPONENTS[component]
70
+
71
+ # Check conflicts
72
+ if spec.conflicts:
73
+ for conflict in spec.conflicts:
74
+ if conflict in components:
75
+ errors.append(
76
+ f"Component '{component}' conflicts with '{conflict}'"
77
+ )
78
+
79
+ return errors
80
+
81
+ @staticmethod
82
+ def get_recommendations(selected_components: list[str]) -> list[str]:
83
+ """
84
+ Get recommended components based on selection.
85
+
86
+ Args:
87
+ selected_components: List of already selected components
88
+
89
+ Returns:
90
+ List of recommended component names not already selected
91
+ """
92
+ recommendations = set()
93
+
94
+ for component_name in selected_components:
95
+ if component_name not in COMPONENTS:
96
+ continue
97
+
98
+ component = COMPONENTS[component_name]
99
+ if component.recommends:
100
+ for rec in component.recommends:
101
+ if rec not in selected_components:
102
+ recommendations.add(rec)
103
+
104
+ return sorted(recommendations)
105
+
106
+ @staticmethod
107
+ def get_missing_dependencies(selected_components: list[str]) -> list[str]:
108
+ """
109
+ Get dependencies that would be auto-added.
110
+
111
+ Args:
112
+ selected_components: List of user-selected components
113
+
114
+ Returns:
115
+ List of dependencies that would be automatically added
116
+ """
117
+ resolved = DependencyResolver.resolve_dependencies(selected_components)
118
+ auto_added = set(resolved) - set(selected_components)
119
+ return sorted(auto_added)