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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- 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)
|
codeframe/cli/helpers.py
ADDED
|
@@ -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]")
|