codeframe-ai 0.9.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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,409 @@
1
+ """CLI environment validation commands.
2
+
3
+ This module provides commands for:
4
+ - Checking environment health
5
+ - Running comprehensive diagnostics
6
+ - Installing missing tools
7
+ - Auto-installing all missing tools
8
+
9
+ Usage:
10
+ codeframe env check # Quick validation
11
+ codeframe env doctor # Comprehensive diagnostics
12
+ codeframe env install-missing pytest # Install specific tool
13
+ codeframe env auto-install --yes # Install all missing
14
+ """
15
+
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import typer
20
+ from rich.console import Console
21
+ from rich.table import Table
22
+ from rich.panel import Panel
23
+ from rich.progress import Progress, SpinnerColumn, TextColumn
24
+
25
+ from codeframe.core.environment import (
26
+ EnvironmentValidator,
27
+ ToolStatus,
28
+ )
29
+ from codeframe.core.installer import ToolInstaller
30
+
31
+
32
+ console = Console()
33
+
34
+ env_app = typer.Typer(
35
+ name="env",
36
+ help="Environment validation and tool management",
37
+ no_args_is_help=True,
38
+ )
39
+
40
+
41
+ @env_app.command()
42
+ def check(
43
+ project: Optional[str] = typer.Option(
44
+ None, "--project", "-p", help="Project directory to validate"
45
+ ),
46
+ ):
47
+ """Quick environment validation.
48
+
49
+ Checks if required tools are installed and shows health score.
50
+
51
+ Examples:
52
+
53
+ codeframe env check
54
+
55
+ codeframe env check --project ./my-project
56
+ """
57
+ # Determine project path
58
+ if project:
59
+ project_path = Path(project)
60
+ if not project_path.exists():
61
+ console.print(f"[red]Error:[/red] Project path not found: {project}")
62
+ raise typer.Exit(1)
63
+ else:
64
+ project_path = Path.cwd()
65
+
66
+ # Run validation
67
+ validator = EnvironmentValidator()
68
+
69
+ with Progress(
70
+ SpinnerColumn(),
71
+ TextColumn("[progress.description]{task.description}"),
72
+ console=console,
73
+ transient=True,
74
+ ) as progress:
75
+ progress.add_task("Checking environment...", total=None)
76
+ result = validator.validate_environment(project_path)
77
+
78
+ # Display results
79
+ health_pct = int(result.health_score * 100)
80
+
81
+ if result.is_healthy:
82
+ health_color = "green"
83
+ health_status = "Healthy"
84
+ elif result.health_score >= 0.5:
85
+ health_color = "yellow"
86
+ health_status = "Partial"
87
+ else:
88
+ health_color = "red"
89
+ health_status = "Unhealthy"
90
+
91
+ console.print()
92
+ console.print(f"[bold]Project Type:[/bold] {result.project_type}")
93
+ console.print(f"[bold]Health Score:[/bold] [{health_color}]{health_pct}%[/{health_color}] ({health_status})")
94
+ console.print()
95
+
96
+ # Show tool summary
97
+ available_count = sum(1 for t in result.detected_tools.values() if t.is_available)
98
+ total_count = len(result.detected_tools)
99
+ console.print(f"[bold]Tools:[/bold] {available_count}/{total_count} available")
100
+
101
+ if result.missing_tools:
102
+ console.print(f"[bold]Missing (required):[/bold] [red]{', '.join(result.missing_tools)}[/red]")
103
+
104
+ if result.optional_missing:
105
+ console.print(f"[bold]Missing (optional):[/bold] [yellow]{', '.join(result.optional_missing)}[/yellow]")
106
+
107
+ console.print()
108
+
109
+ # Show quick recommendations
110
+ if result.recommendations:
111
+ console.print("[bold]Quick fix:[/bold]")
112
+ for rec in result.recommendations[:3]: # Show top 3
113
+ console.print(f" • {rec}")
114
+ console.print()
115
+ console.print("[dim]Run 'codeframe env doctor' for full diagnostics[/dim]")
116
+
117
+ if not result.is_healthy:
118
+ raise typer.Exit(1)
119
+
120
+
121
+ @env_app.command()
122
+ def doctor(
123
+ project: Optional[str] = typer.Option(
124
+ None, "--project", "-p", help="Project directory to analyze"
125
+ ),
126
+ ):
127
+ """Comprehensive environment health analysis.
128
+
129
+ Provides detailed diagnostics including version compatibility,
130
+ recommendations, and potential conflicts.
131
+
132
+ Examples:
133
+
134
+ codeframe env doctor
135
+
136
+ codeframe env doctor --project ./my-project
137
+ """
138
+ # Determine project path
139
+ if project:
140
+ project_path = Path(project)
141
+ if not project_path.exists():
142
+ console.print(f"[red]Error:[/red] Project path not found: {project}")
143
+ raise typer.Exit(1)
144
+ else:
145
+ project_path = Path.cwd()
146
+
147
+ # Run validation
148
+ validator = EnvironmentValidator()
149
+
150
+ with Progress(
151
+ SpinnerColumn(),
152
+ TextColumn("[progress.description]{task.description}"),
153
+ console=console,
154
+ transient=True,
155
+ ) as progress:
156
+ progress.add_task("Running comprehensive diagnostics...", total=None)
157
+ result = validator.validate_environment(project_path)
158
+
159
+ # Display header
160
+ health_pct = int(result.health_score * 100)
161
+ console.print()
162
+ console.print(Panel(
163
+ f"[bold]Environment Health Report[/bold]\n"
164
+ f"Project: {project_path.name}\n"
165
+ f"Type: {result.project_type}\n"
166
+ f"Score: {health_pct}%",
167
+ title="CodeFRAME Doctor",
168
+ ))
169
+
170
+ # Tools table
171
+ table = Table(title="Detected Tools")
172
+ table.add_column("Tool", style="cyan")
173
+ table.add_column("Status", style="green")
174
+ table.add_column("Version")
175
+ table.add_column("Path", style="dim")
176
+
177
+ for name, info in sorted(result.detected_tools.items()):
178
+ if info.status == ToolStatus.AVAILABLE:
179
+ status = "[green]Available[/green]"
180
+ elif info.status == ToolStatus.NOT_FOUND:
181
+ status = "[red]Not Found[/red]"
182
+ elif info.status == ToolStatus.VERSION_INCOMPATIBLE:
183
+ status = "[yellow]Incompatible[/yellow]"
184
+ else:
185
+ status = "[dim]Error[/dim]"
186
+
187
+ table.add_row(
188
+ name,
189
+ status,
190
+ info.version or "-",
191
+ info.path or "-",
192
+ )
193
+
194
+ console.print()
195
+ console.print(table)
196
+
197
+ # Warnings
198
+ if result.warnings:
199
+ console.print()
200
+ console.print("[bold yellow]Warnings:[/bold yellow]")
201
+ for warning in result.warnings:
202
+ console.print(f" [yellow]⚠[/yellow] {warning}")
203
+
204
+ # Conflicts
205
+ if result.conflicts:
206
+ console.print()
207
+ console.print("[bold red]Conflicts:[/bold red]")
208
+ for conflict in result.conflicts:
209
+ console.print(f" [red]✗[/red] {conflict}")
210
+
211
+ # Recommendations
212
+ if result.recommendations:
213
+ console.print()
214
+ console.print("[bold]Recommendations:[/bold]")
215
+ for rec in result.recommendations:
216
+ console.print(f" • {rec}")
217
+
218
+ # Stale worktree check
219
+ try:
220
+ from codeframe.core.worktrees import WorktreeRegistry
221
+ stale = WorktreeRegistry().list_stale(project_path)
222
+ if stale:
223
+ console.print()
224
+ console.print("[bold yellow]Stale Worktrees:[/bold yellow]")
225
+ for entry in stale:
226
+ console.print(
227
+ f" [yellow]⚠[/yellow] task [cyan]{entry['task_id']}[/cyan] "
228
+ f"(pid {entry.get('pid', '?')} no longer running)"
229
+ )
230
+ console.print()
231
+ console.print(
232
+ "[dim]To clean up, run:[/dim] codeframe work batch run --all-ready "
233
+ "[dim](auto-cleans on next run)[/dim]"
234
+ )
235
+ console.print(
236
+ "[dim]Or remove manually:[/dim] rm -rf .codeframe/worktrees/"
237
+ )
238
+ except Exception:
239
+ pass # Worktree registry is optional; never fail doctor over it
240
+
241
+ console.print()
242
+
243
+ if not result.is_healthy:
244
+ console.print("[bold]To fix issues, run:[/bold]")
245
+ console.print(" codeframe env auto-install --yes")
246
+ console.print()
247
+ raise typer.Exit(1)
248
+
249
+
250
+ @env_app.command("install-missing")
251
+ def install_missing(
252
+ tool: str = typer.Argument(..., help="Name of the tool to install"),
253
+ yes: bool = typer.Option(
254
+ False, "--yes", "-y", help="Skip confirmation prompt"
255
+ ),
256
+ ):
257
+ """Install a specific missing tool.
258
+
259
+ Attempts to install the specified tool using the appropriate
260
+ package manager (pip, npm, cargo, or system).
261
+
262
+ Examples:
263
+
264
+ codeframe env install-missing pytest
265
+
266
+ codeframe env install-missing eslint --yes
267
+ """
268
+ installer = ToolInstaller()
269
+
270
+ # Check if we can install this tool
271
+ if not installer.can_install(tool):
272
+ console.print(f"[red]Error:[/red] Cannot install '{tool}' - no installer available")
273
+ console.print()
274
+ console.print("This tool may need manual installation.")
275
+ console.print(f"Try: [dim]{installer.get_install_command(tool) or f'Install {tool} manually'}[/dim]")
276
+ raise typer.Exit(1)
277
+
278
+ # Show install command
279
+ install_cmd = installer.get_install_command(tool)
280
+ console.print(f"[bold]Tool:[/bold] {tool}")
281
+ console.print(f"[bold]Command:[/bold] {install_cmd}")
282
+ console.print()
283
+
284
+ # Confirm if needed
285
+ if not yes:
286
+ confirmed = typer.confirm("Proceed with installation?")
287
+ if not confirmed:
288
+ console.print("[yellow]Installation cancelled[/yellow]")
289
+ raise typer.Exit(0)
290
+
291
+ # Install
292
+ with Progress(
293
+ SpinnerColumn(),
294
+ TextColumn("[progress.description]{task.description}"),
295
+ console=console,
296
+ transient=True,
297
+ ) as progress:
298
+ progress.add_task(f"Installing {tool}...", total=None)
299
+ result = installer.install_tool(tool, confirm=False)
300
+
301
+ # Report result
302
+ if result.success:
303
+ console.print(f"[green]✓[/green] {result.message}")
304
+ else:
305
+ console.print(f"[red]✗[/red] {result.message}")
306
+ if result.error_output:
307
+ console.print()
308
+ console.print("[dim]Error output:[/dim]")
309
+ console.print(result.error_output[:500]) # Truncate long errors
310
+ raise typer.Exit(1)
311
+
312
+
313
+ @env_app.command("auto-install")
314
+ def auto_install(
315
+ project: Optional[str] = typer.Option(
316
+ None, "--project", "-p", help="Project directory"
317
+ ),
318
+ yes: bool = typer.Option(
319
+ False, "--yes", "-y", help="Skip confirmation prompt"
320
+ ),
321
+ ):
322
+ """Automatically install all missing required tools.
323
+
324
+ Detects missing tools for the project and installs them
325
+ using the appropriate package managers.
326
+
327
+ Examples:
328
+
329
+ codeframe env auto-install
330
+
331
+ codeframe env auto-install --yes
332
+
333
+ codeframe env auto-install --project ./my-project --yes
334
+ """
335
+ # Determine project path
336
+ if project:
337
+ project_path = Path(project)
338
+ if not project_path.exists():
339
+ console.print(f"[red]Error:[/red] Project path not found: {project}")
340
+ raise typer.Exit(1)
341
+ else:
342
+ project_path = Path.cwd()
343
+
344
+ # Get missing tools
345
+ validator = EnvironmentValidator()
346
+ validation_result = validator.validate_environment(project_path)
347
+
348
+ if not validation_result.missing_tools:
349
+ console.print("[green]✓[/green] All required tools are already installed!")
350
+ console.print()
351
+ console.print("[dim]Nothing to install.[/dim]")
352
+ return
353
+
354
+ # Show what will be installed
355
+ console.print("[bold]Missing tools to install:[/bold]")
356
+ for tool in validation_result.missing_tools:
357
+ console.print(f" • {tool}")
358
+ console.print()
359
+
360
+ # Confirm if needed
361
+ if not yes:
362
+ confirmed = typer.confirm(f"Install {len(validation_result.missing_tools)} tool(s)?")
363
+ if not confirmed:
364
+ console.print("[yellow]Installation cancelled[/yellow]")
365
+ raise typer.Exit(0)
366
+
367
+ # Install each tool
368
+ installer = ToolInstaller()
369
+ results = []
370
+
371
+ with Progress(
372
+ SpinnerColumn(),
373
+ TextColumn("[progress.description]{task.description}"),
374
+ console=console,
375
+ ) as progress:
376
+ task = progress.add_task("Installing tools...", total=len(validation_result.missing_tools))
377
+
378
+ for tool in validation_result.missing_tools:
379
+ progress.update(task, description=f"Installing {tool}...")
380
+
381
+ if installer.can_install(tool):
382
+ result = installer.install_tool(tool, confirm=False)
383
+ results.append(result)
384
+ else:
385
+ results.append(None)
386
+
387
+ progress.advance(task)
388
+
389
+ console.print()
390
+
391
+ # Report results
392
+ success_count = sum(1 for r in results if r and r.success)
393
+ fail_count = len(results) - success_count
394
+
395
+ console.print("[bold]Results:[/bold]")
396
+ for i, tool in enumerate(validation_result.missing_tools):
397
+ result = results[i]
398
+ if result is None:
399
+ console.print(f" [yellow]⚠[/yellow] {tool}: No installer available")
400
+ elif result.success:
401
+ console.print(f" [green]✓[/green] {tool}: Installed")
402
+ else:
403
+ console.print(f" [red]✗[/red] {tool}: {result.message}")
404
+
405
+ console.print()
406
+ console.print(f"[bold]Summary:[/bold] {success_count} installed, {fail_count} failed")
407
+
408
+ if fail_count > 0:
409
+ raise typer.Exit(1)
@@ -0,0 +1,56 @@
1
+ """Shared CLI helper utilities.
2
+
3
+ This module provides common utilities used across CLI command modules:
4
+ - require_auth: Authentication check for API client
5
+ - format_date: Safe date string formatting
6
+ - console: Shared Rich Console instance
7
+
8
+ Usage:
9
+ from codeframe.cli.helpers import require_auth, format_date, console
10
+ """
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from codeframe.cli.api_client import APIClient
16
+
17
+ # Shared console instance for all CLI modules
18
+ console = Console()
19
+
20
+
21
+ def require_auth(client: APIClient) -> None:
22
+ """Check if client is authenticated, exit with error if not.
23
+
24
+ Args:
25
+ client: APIClient instance to check for authentication
26
+
27
+ Raises:
28
+ typer.Exit: If client is not authenticated (exit code 1)
29
+ """
30
+ if not client.token:
31
+ console.print("[yellow]Not logged in.[/yellow]")
32
+ console.print("Please log in: codeframe auth login")
33
+ raise typer.Exit(1)
34
+
35
+
36
+ def format_date(date_str: str | None, length: int = 10) -> str:
37
+ """Safely extract date portion from an ISO datetime string.
38
+
39
+ Args:
40
+ date_str: ISO datetime string (e.g., "2024-01-15T10:30:00Z") or None
41
+ length: Number of characters to extract (default 10 for YYYY-MM-DD)
42
+
43
+ Returns:
44
+ Truncated date string or empty string if input is falsy or too short
45
+
46
+ Examples:
47
+ >>> format_date("2024-01-15T10:30:00Z")
48
+ '2024-01-15'
49
+ >>> format_date(None)
50
+ ''
51
+ >>> format_date("short")
52
+ ''
53
+ """
54
+ if not date_str or len(date_str) < length:
55
+ return ""
56
+ return date_str[:length]
@@ -0,0 +1,208 @@
1
+ """CLI workspace lifecycle hooks management.
2
+
3
+ Usage:
4
+ codeframe hooks show # Display configured hooks
5
+ codeframe hooks run <hook_name> # Manually trigger a hook
6
+ codeframe hooks set <name> <cmd> # Set a hook command
7
+ codeframe hooks clear <name> # Remove a hook
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ console = Console()
18
+
19
+ hooks_app = typer.Typer(
20
+ name="hooks",
21
+ help="Workspace lifecycle hooks management",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ VALID_HOOK_NAMES = [
26
+ "after_init",
27
+ "before_task",
28
+ "after_task_success",
29
+ "after_task_failure",
30
+ "before_remove",
31
+ ]
32
+
33
+
34
+ @hooks_app.command("show")
35
+ def hooks_show(
36
+ workspace_path: Optional[Path] = typer.Option(
37
+ None, "--workspace", "-w",
38
+ help="Workspace path (defaults to current directory)",
39
+ ),
40
+ ) -> None:
41
+ """Display configured hooks from .codeframe/config.yaml."""
42
+ from codeframe.core.config import load_environment_config
43
+
44
+ from codeframe.core.workspace import get_workspace
45
+ path = workspace_path or Path.cwd()
46
+ try:
47
+ ws = get_workspace(path)
48
+ path = ws.repo_path
49
+ except (FileNotFoundError, ValueError):
50
+ pass # Workspace not initialized; fall back to raw path
51
+ config = load_environment_config(path)
52
+
53
+ if not config:
54
+ console.print("[yellow]No workspace configuration found.[/yellow]")
55
+ console.print("Run 'codeframe init .' first.")
56
+ raise typer.Exit(1)
57
+
58
+ table = Table(title="Workspace Hooks")
59
+ table.add_column("Hook Point", style="cyan")
60
+ table.add_column("Command", style="dim")
61
+ table.add_column("Status")
62
+
63
+ for hook_name in VALID_HOOK_NAMES:
64
+ command = getattr(config.hooks, hook_name, None)
65
+ if command:
66
+ table.add_row(hook_name, command, "[green]configured[/green]")
67
+ else:
68
+ table.add_row(hook_name, "-", "[dim]not set[/dim]")
69
+
70
+ table.add_row("", "", "")
71
+ table.add_row("hook_timeout", f"{config.hooks.hook_timeout}s", "[dim]default[/dim]")
72
+
73
+ console.print(table)
74
+
75
+
76
+ @hooks_app.command("run")
77
+ def hooks_run(
78
+ hook_name: str = typer.Argument(..., help="Hook name to execute"),
79
+ task_id: str = typer.Option("", "--task-id", help="Task ID for template rendering"),
80
+ task_title: str = typer.Option("", "--task-title", help="Task title for template rendering"),
81
+ workspace_path: Optional[Path] = typer.Option(
82
+ None, "--workspace", "-w",
83
+ help="Workspace path (defaults to current directory)",
84
+ ),
85
+ ) -> None:
86
+ """Manually trigger a named hook."""
87
+ from codeframe.core.config import load_environment_config
88
+ from codeframe.core.hooks import HookContext, execute_hook
89
+
90
+ if hook_name not in VALID_HOOK_NAMES:
91
+ console.print(f"[red]Error:[/red] Invalid hook name '{hook_name}'")
92
+ console.print(f"Valid hooks: {', '.join(VALID_HOOK_NAMES)}")
93
+ raise typer.Exit(1)
94
+
95
+ from codeframe.core.workspace import get_workspace
96
+ path = workspace_path or Path.cwd()
97
+ try:
98
+ ws = get_workspace(path)
99
+ path = ws.repo_path
100
+ except (FileNotFoundError, ValueError):
101
+ pass # Workspace not initialized; fall back to raw path
102
+ config = load_environment_config(path)
103
+
104
+ if not config:
105
+ console.print("[yellow]No workspace configuration found.[/yellow]")
106
+ raise typer.Exit(1)
107
+
108
+ ctx = HookContext(
109
+ task_id=task_id,
110
+ task_title=task_title,
111
+ task_status="manual",
112
+ workspace_path=str(path),
113
+ )
114
+
115
+ result = execute_hook(hook_name, config, path, ctx, abort_on_failure=False)
116
+
117
+ if result is None:
118
+ console.print(f"[yellow]Hook '{hook_name}' is not configured.[/yellow]")
119
+ return
120
+
121
+ if result.success:
122
+ console.print(f"[green]Hook '{hook_name}' succeeded[/green] ({result.duration_ms}ms)")
123
+ else:
124
+ console.print(f"[red]Hook '{hook_name}' failed[/red] ({result.duration_ms}ms)")
125
+ if result.timed_out:
126
+ console.print(" [yellow]Timed out[/yellow]")
127
+
128
+ if result.stdout.strip():
129
+ console.print(f" stdout: {result.stdout.strip()[:500]}")
130
+ if result.stderr.strip():
131
+ console.print(f" stderr: {result.stderr.strip()[:500]}")
132
+
133
+ if not result.success:
134
+ raise typer.Exit(1)
135
+
136
+
137
+ @hooks_app.command("set")
138
+ def hooks_set(
139
+ hook_name: str = typer.Argument(..., help="Hook name to configure"),
140
+ command: str = typer.Argument(..., help="Shell command template"),
141
+ workspace_path: Optional[Path] = typer.Option(
142
+ None, "--workspace", "-w",
143
+ help="Workspace path (defaults to current directory)",
144
+ ),
145
+ ) -> None:
146
+ """Set or update a hook command."""
147
+ from codeframe.core.config import (
148
+ load_environment_config,
149
+ save_environment_config,
150
+ get_default_environment_config,
151
+ )
152
+
153
+ if hook_name not in VALID_HOOK_NAMES:
154
+ console.print(f"[red]Error:[/red] Invalid hook name '{hook_name}'")
155
+ console.print(f"Valid hooks: {', '.join(VALID_HOOK_NAMES)}")
156
+ raise typer.Exit(1)
157
+
158
+ from codeframe.core.workspace import get_workspace
159
+ path = workspace_path or Path.cwd()
160
+ try:
161
+ ws = get_workspace(path)
162
+ path = ws.repo_path
163
+ except (FileNotFoundError, ValueError):
164
+ pass # Workspace not initialized; fall back to raw path
165
+ config = load_environment_config(path) or get_default_environment_config()
166
+
167
+ setattr(config.hooks, hook_name, command)
168
+ save_environment_config(path, config)
169
+
170
+ console.print(f"[green]Hook '{hook_name}' set to:[/green] {command}")
171
+
172
+
173
+ @hooks_app.command("clear")
174
+ def hooks_clear(
175
+ hook_name: str = typer.Argument(..., help="Hook name to clear"),
176
+ workspace_path: Optional[Path] = typer.Option(
177
+ None, "--workspace", "-w",
178
+ help="Workspace path (defaults to current directory)",
179
+ ),
180
+ ) -> None:
181
+ """Remove a hook."""
182
+ from codeframe.core.config import (
183
+ load_environment_config,
184
+ save_environment_config,
185
+ )
186
+
187
+ if hook_name not in VALID_HOOK_NAMES:
188
+ console.print(f"[red]Error:[/red] Invalid hook name '{hook_name}'")
189
+ console.print(f"Valid hooks: {', '.join(VALID_HOOK_NAMES)}")
190
+ raise typer.Exit(1)
191
+
192
+ from codeframe.core.workspace import get_workspace
193
+ path = workspace_path or Path.cwd()
194
+ try:
195
+ ws = get_workspace(path)
196
+ path = ws.repo_path
197
+ except (FileNotFoundError, ValueError):
198
+ pass # Workspace not initialized; fall back to raw path
199
+ config = load_environment_config(path)
200
+
201
+ if not config:
202
+ console.print("[yellow]No workspace configuration found.[/yellow]")
203
+ raise typer.Exit(1)
204
+
205
+ setattr(config.hooks, hook_name, None)
206
+ save_environment_config(path, config)
207
+
208
+ console.print(f"[green]Hook '{hook_name}' cleared.[/green]")