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
codeframe/tui/app.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""CodeFRAME TUI Dashboard — live terminal dashboard.
|
|
2
|
+
|
|
3
|
+
A Textual application showing tasks, events, and blockers
|
|
4
|
+
with auto-refresh and keyboard navigation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from textual.app import App, ComposeResult
|
|
10
|
+
from textual.binding import Binding
|
|
11
|
+
from textual.containers import Horizontal, Vertical
|
|
12
|
+
from textual.reactive import reactive
|
|
13
|
+
from textual.widgets import DataTable, Footer, Header, RichLog, Static
|
|
14
|
+
|
|
15
|
+
from codeframe.core.workspace import Workspace
|
|
16
|
+
from codeframe.tui.data_service import DashboardData, load_dashboard_data
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Status → color mapping for task rows
|
|
20
|
+
_STATUS_COLORS: dict[str, str] = {
|
|
21
|
+
"DONE": "green",
|
|
22
|
+
"IN_PROGRESS": "cyan",
|
|
23
|
+
"READY": "yellow",
|
|
24
|
+
"BACKLOG": "dim",
|
|
25
|
+
"BLOCKED": "red",
|
|
26
|
+
"FAILED": "red bold",
|
|
27
|
+
"MERGED": "green dim",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StatusBar(Static):
|
|
32
|
+
"""Top status bar showing task counts and workspace info."""
|
|
33
|
+
|
|
34
|
+
def update_from_data(self, data: DashboardData) -> None:
|
|
35
|
+
counts = data.task_counts
|
|
36
|
+
total = sum(counts.values())
|
|
37
|
+
done = counts.get("DONE", 0) + counts.get("MERGED", 0)
|
|
38
|
+
active = counts.get("IN_PROGRESS", 0)
|
|
39
|
+
blocked = counts.get("BLOCKED", 0) + counts.get("FAILED", 0)
|
|
40
|
+
ready = counts.get("READY", 0) + counts.get("BACKLOG", 0)
|
|
41
|
+
|
|
42
|
+
parts = [
|
|
43
|
+
f"[bold]{data.workspace_name}[/bold]",
|
|
44
|
+
f"Tasks: {total}",
|
|
45
|
+
f"[green]{done} done[/green]",
|
|
46
|
+
f"[cyan]{active} active[/cyan]",
|
|
47
|
+
f"[yellow]{ready} ready[/yellow]",
|
|
48
|
+
]
|
|
49
|
+
if blocked > 0:
|
|
50
|
+
parts.append(f"[red]{blocked} blocked/failed[/red]")
|
|
51
|
+
if data.blocker_count > 0:
|
|
52
|
+
parts.append(f"[red bold]{data.blocker_count} blockers[/red bold]")
|
|
53
|
+
if data.open_obligation_count > 0:
|
|
54
|
+
parts.append(f"[yellow bold]{data.open_obligation_count} open obligations[/yellow bold]")
|
|
55
|
+
|
|
56
|
+
self.update(" | ".join(parts))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DashboardApp(App):
|
|
60
|
+
"""CodeFRAME TUI Dashboard."""
|
|
61
|
+
|
|
62
|
+
CSS = """
|
|
63
|
+
Screen {
|
|
64
|
+
layout: vertical;
|
|
65
|
+
}
|
|
66
|
+
#status-bar {
|
|
67
|
+
height: 1;
|
|
68
|
+
background: $surface;
|
|
69
|
+
padding: 0 1;
|
|
70
|
+
}
|
|
71
|
+
#main-content {
|
|
72
|
+
height: 1fr;
|
|
73
|
+
}
|
|
74
|
+
#task-panel {
|
|
75
|
+
width: 2fr;
|
|
76
|
+
border: solid $primary;
|
|
77
|
+
}
|
|
78
|
+
#right-panel {
|
|
79
|
+
width: 1fr;
|
|
80
|
+
}
|
|
81
|
+
#event-log {
|
|
82
|
+
height: 2fr;
|
|
83
|
+
border: solid $secondary;
|
|
84
|
+
}
|
|
85
|
+
#blocker-panel {
|
|
86
|
+
height: 1fr;
|
|
87
|
+
border: solid $error;
|
|
88
|
+
}
|
|
89
|
+
#proof-panel {
|
|
90
|
+
height: 1fr;
|
|
91
|
+
border: solid $warning;
|
|
92
|
+
}
|
|
93
|
+
DataTable {
|
|
94
|
+
height: 1fr;
|
|
95
|
+
}
|
|
96
|
+
RichLog {
|
|
97
|
+
height: 1fr;
|
|
98
|
+
}
|
|
99
|
+
.panel-title {
|
|
100
|
+
background: $surface;
|
|
101
|
+
padding: 0 1;
|
|
102
|
+
text-style: bold;
|
|
103
|
+
}
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
TITLE = "CodeFRAME Dashboard"
|
|
107
|
+
BINDINGS = [
|
|
108
|
+
Binding("q", "quit", "Quit"),
|
|
109
|
+
Binding("r", "refresh", "Refresh"),
|
|
110
|
+
Binding("tab", "focus_next", "Next Panel"),
|
|
111
|
+
Binding("shift+tab", "focus_previous", "Prev Panel"),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
workspace: Optional[Workspace] = None
|
|
115
|
+
refresh_interval: int = 2
|
|
116
|
+
data: reactive[Optional[DashboardData]] = reactive(None)
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
workspace: Workspace,
|
|
121
|
+
refresh_interval: int = 2,
|
|
122
|
+
**kwargs,
|
|
123
|
+
):
|
|
124
|
+
super().__init__(**kwargs)
|
|
125
|
+
self.workspace = workspace
|
|
126
|
+
self.refresh_interval = refresh_interval
|
|
127
|
+
|
|
128
|
+
def compose(self) -> ComposeResult:
|
|
129
|
+
yield Header()
|
|
130
|
+
yield StatusBar(id="status-bar")
|
|
131
|
+
with Horizontal(id="main-content"):
|
|
132
|
+
with Vertical(id="task-panel"):
|
|
133
|
+
yield Static("Tasks", classes="panel-title")
|
|
134
|
+
yield DataTable(id="task-table")
|
|
135
|
+
with Vertical(id="right-panel"):
|
|
136
|
+
with Vertical(id="event-log"):
|
|
137
|
+
yield Static("Recent Events", classes="panel-title")
|
|
138
|
+
yield RichLog(id="event-log-content", highlight=True, markup=True)
|
|
139
|
+
with Vertical(id="blocker-panel"):
|
|
140
|
+
yield Static("Open Blockers", classes="panel-title")
|
|
141
|
+
yield RichLog(id="blocker-log", highlight=True, markup=True)
|
|
142
|
+
with Vertical(id="proof-panel"):
|
|
143
|
+
yield Static("PROOF9 Obligations", classes="panel-title")
|
|
144
|
+
yield RichLog(id="proof-log", highlight=True, markup=True)
|
|
145
|
+
yield Footer()
|
|
146
|
+
|
|
147
|
+
def on_mount(self) -> None:
|
|
148
|
+
# Set up task table columns
|
|
149
|
+
table = self.query_one("#task-table", DataTable)
|
|
150
|
+
table.add_columns("ID", "Title", "Status", "Priority")
|
|
151
|
+
table.cursor_type = "row"
|
|
152
|
+
|
|
153
|
+
# Initial data load
|
|
154
|
+
self._refresh_data()
|
|
155
|
+
|
|
156
|
+
# Auto-refresh
|
|
157
|
+
self.set_interval(self.refresh_interval, self._refresh_data)
|
|
158
|
+
|
|
159
|
+
def _refresh_data(self) -> None:
|
|
160
|
+
"""Load fresh data from the workspace and update all widgets."""
|
|
161
|
+
if not self.workspace:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
data = load_dashboard_data(self.workspace)
|
|
165
|
+
self.data = data
|
|
166
|
+
|
|
167
|
+
self._update_status_bar(data)
|
|
168
|
+
self._update_task_table(data)
|
|
169
|
+
self._update_event_log(data)
|
|
170
|
+
self._update_blocker_panel(data)
|
|
171
|
+
self._update_proof_panel(data)
|
|
172
|
+
|
|
173
|
+
if data.error:
|
|
174
|
+
self.notify(f"Data loading error: {data.error}", severity="warning")
|
|
175
|
+
|
|
176
|
+
def _update_status_bar(self, data: DashboardData) -> None:
|
|
177
|
+
status_bar = self.query_one("#status-bar", StatusBar)
|
|
178
|
+
status_bar.update_from_data(data)
|
|
179
|
+
|
|
180
|
+
def _update_task_table(self, data: DashboardData) -> None:
|
|
181
|
+
table = self.query_one("#task-table", DataTable)
|
|
182
|
+
table.clear()
|
|
183
|
+
|
|
184
|
+
for task in data.tasks:
|
|
185
|
+
status_val = task.status.value if hasattr(task.status, "value") else str(task.status)
|
|
186
|
+
color = _STATUS_COLORS.get(status_val, "white")
|
|
187
|
+
table.add_row(
|
|
188
|
+
task.id[:8],
|
|
189
|
+
task.title[:50],
|
|
190
|
+
f"[{color}]{status_val}[/{color}]",
|
|
191
|
+
str(task.priority),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _update_event_log(self, data: DashboardData) -> None:
|
|
195
|
+
log = self.query_one("#event-log-content", RichLog)
|
|
196
|
+
log.clear()
|
|
197
|
+
|
|
198
|
+
for event in reversed(data.events): # oldest first
|
|
199
|
+
ts = event.created_at.strftime("%H:%M:%S") if hasattr(event.created_at, "strftime") else str(event.created_at)[:8]
|
|
200
|
+
log.write(f"[dim]{ts}[/dim] {event.event_type}")
|
|
201
|
+
|
|
202
|
+
def _update_blocker_panel(self, data: DashboardData) -> None:
|
|
203
|
+
log = self.query_one("#blocker-log", RichLog)
|
|
204
|
+
log.clear()
|
|
205
|
+
|
|
206
|
+
if not data.blockers:
|
|
207
|
+
log.write("[dim]No open blockers[/dim]")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
for blocker in data.blockers:
|
|
211
|
+
log.write(f"[red bold]{blocker.id[:8]}[/red bold]: {blocker.question[:60]}")
|
|
212
|
+
|
|
213
|
+
_SEVERITY_COLORS: dict[str, str] = {
|
|
214
|
+
"critical": "red bold",
|
|
215
|
+
"high": "red",
|
|
216
|
+
"medium": "yellow",
|
|
217
|
+
"low": "dim",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_OBLIGATION_ICONS: dict[str, str] = {
|
|
221
|
+
"satisfied": "✅",
|
|
222
|
+
"failed": "❌",
|
|
223
|
+
"pending": "⏳",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _update_proof_panel(self, data: DashboardData) -> None:
|
|
227
|
+
log = self.query_one("#proof-log", RichLog)
|
|
228
|
+
log.clear()
|
|
229
|
+
|
|
230
|
+
if not data.open_requirements and not data.expiring_waivers:
|
|
231
|
+
log.write("[dim]No open obligations[/dim]")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
for req in data.expiring_waivers:
|
|
235
|
+
from datetime import date
|
|
236
|
+
days = (req.waiver.expires - date.today()).days
|
|
237
|
+
log.write(
|
|
238
|
+
f"[yellow bold]⚠ {req.id}[/yellow bold]: waiver expires in {days}d — {req.title[:50]}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
for req in data.open_requirements:
|
|
242
|
+
gate_parts = [
|
|
243
|
+
f"{obl.gate.value.upper()} {self._OBLIGATION_ICONS.get(obl.status, '⏳')}"
|
|
244
|
+
for obl in req.obligations
|
|
245
|
+
]
|
|
246
|
+
gate_summary = " ".join(gate_parts)
|
|
247
|
+
sev_color = self._SEVERITY_COLORS.get(req.severity.value, "white")
|
|
248
|
+
log.write(
|
|
249
|
+
f"[red]{req.id}[/red] [{sev_color}]{req.severity.value}[/{sev_color}]"
|
|
250
|
+
f" {req.title[:45]} | {gate_summary}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def action_refresh(self) -> None:
|
|
254
|
+
"""Manual refresh via 'r' key."""
|
|
255
|
+
self._refresh_data()
|
|
256
|
+
self.notify("Refreshed")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Dashboard data service — thin wrapper over core modules.
|
|
2
|
+
|
|
3
|
+
Loads all dashboard data in a single call to minimize DB access.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from codeframe.core.workspace import Workspace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DashboardData:
|
|
14
|
+
"""Snapshot of workspace state for display."""
|
|
15
|
+
|
|
16
|
+
workspace_name: str = ""
|
|
17
|
+
workspace_path: str = ""
|
|
18
|
+
tech_stack: str = ""
|
|
19
|
+
|
|
20
|
+
# Task counts by status
|
|
21
|
+
task_counts: dict[str, int] = field(default_factory=dict)
|
|
22
|
+
tasks: list = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
# Open blockers
|
|
25
|
+
blockers: list = field(default_factory=list)
|
|
26
|
+
blocker_count: int = 0
|
|
27
|
+
|
|
28
|
+
# Recent events
|
|
29
|
+
events: list = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
# PROOF9 obligations
|
|
32
|
+
open_requirements: list = field(default_factory=list)
|
|
33
|
+
expiring_waivers: list = field(default_factory=list)
|
|
34
|
+
open_obligation_count: int = 0
|
|
35
|
+
|
|
36
|
+
# Error (if data loading failed)
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_dashboard_data(
|
|
41
|
+
workspace: Workspace, event_limit: int = 50
|
|
42
|
+
) -> DashboardData:
|
|
43
|
+
"""Load all dashboard data from a workspace.
|
|
44
|
+
|
|
45
|
+
Queries tasks, blockers, and events in one shot.
|
|
46
|
+
Returns a DashboardData snapshot for rendering.
|
|
47
|
+
"""
|
|
48
|
+
data = DashboardData(
|
|
49
|
+
workspace_name=workspace.repo_path.name,
|
|
50
|
+
workspace_path=str(workspace.repo_path),
|
|
51
|
+
tech_stack=workspace.tech_stack or "",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from codeframe.core import tasks as task_module
|
|
56
|
+
|
|
57
|
+
all_tasks = task_module.list_tasks(workspace)
|
|
58
|
+
data.tasks = all_tasks
|
|
59
|
+
|
|
60
|
+
counts: dict[str, int] = {}
|
|
61
|
+
for t in all_tasks:
|
|
62
|
+
status_name = t.status.value if hasattr(t.status, "value") else str(t.status)
|
|
63
|
+
counts[status_name] = counts.get(status_name, 0) + 1
|
|
64
|
+
data.task_counts = counts
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
data.error = f"Tasks: {exc}"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from codeframe.core import blockers
|
|
70
|
+
data.blockers = blockers.list_open(workspace)
|
|
71
|
+
data.blocker_count = len(data.blockers)
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
if not data.error:
|
|
74
|
+
data.error = f"Blockers: {exc}"
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
from codeframe.core.events import list_recent
|
|
78
|
+
data.events = list_recent(workspace, limit=event_limit)
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
if not data.error:
|
|
81
|
+
data.error = f"Events: {exc}"
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
from datetime import date
|
|
85
|
+
from codeframe.core.proof.ledger import list_requirements
|
|
86
|
+
from codeframe.core.proof.models import ReqStatus
|
|
87
|
+
|
|
88
|
+
open_reqs = list_requirements(workspace, status=ReqStatus.OPEN)
|
|
89
|
+
data.open_requirements = open_reqs
|
|
90
|
+
data.open_obligation_count = len(open_reqs)
|
|
91
|
+
|
|
92
|
+
waived = list_requirements(workspace, status=ReqStatus.WAIVED)
|
|
93
|
+
today = date.today()
|
|
94
|
+
data.expiring_waivers = [
|
|
95
|
+
req for req in waived
|
|
96
|
+
if req.waiver and req.waiver.expires is not None
|
|
97
|
+
and 0 <= (req.waiver.expires - today).days <= 7
|
|
98
|
+
]
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
if not data.error:
|
|
101
|
+
data.error = f"Proof: {exc}"
|
|
102
|
+
|
|
103
|
+
return data
|
codeframe/ui/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""FastAPI dependency injection providers.
|
|
2
|
+
|
|
3
|
+
This module provides dependency injection functions for accessing
|
|
4
|
+
shared application state across all API endpoints.
|
|
5
|
+
|
|
6
|
+
v2-only: All dependencies use codeframe.core modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import HTTPException, Query, Request
|
|
13
|
+
|
|
14
|
+
from codeframe.workspace import WorkspaceManager
|
|
15
|
+
|
|
16
|
+
# v2 imports
|
|
17
|
+
from codeframe.core.workspace import Workspace, get_workspace, workspace_exists
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_workspace_manager(request: Request) -> WorkspaceManager:
|
|
21
|
+
"""Get workspace manager from application state.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
request: FastAPI request object
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
WorkspaceManager instance from app.state.workspace_manager
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
@router.post("/endpoint")
|
|
31
|
+
async def endpoint(workspace_mgr: WorkspaceManager = Depends(get_workspace_manager)):
|
|
32
|
+
# Use workspace_mgr here
|
|
33
|
+
...
|
|
34
|
+
"""
|
|
35
|
+
return request.app.state.workspace_manager
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_v2_workspace(
|
|
39
|
+
workspace_path: Optional[str] = Query(
|
|
40
|
+
None,
|
|
41
|
+
description="Path to workspace directory (defaults to server's working directory)",
|
|
42
|
+
),
|
|
43
|
+
request: Request = None,
|
|
44
|
+
) -> Workspace:
|
|
45
|
+
"""Get v2 Workspace from path or server default.
|
|
46
|
+
|
|
47
|
+
This dependency resolves a Workspace from either:
|
|
48
|
+
1. An explicit workspace_path query parameter
|
|
49
|
+
2. The server's default workspace (from app.state.default_workspace_path)
|
|
50
|
+
3. The server's current working directory
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
workspace_path: Optional explicit path to workspace
|
|
54
|
+
request: FastAPI request for accessing app state
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
v2 Workspace instance
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
HTTPException:
|
|
61
|
+
- 400: No workspace path provided and no default configured
|
|
62
|
+
- 404: Workspace not found at path
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
@router.get("/v2/endpoint")
|
|
66
|
+
async def endpoint(workspace: Workspace = Depends(get_v2_workspace)):
|
|
67
|
+
# Use workspace here
|
|
68
|
+
...
|
|
69
|
+
"""
|
|
70
|
+
# Resolve workspace path
|
|
71
|
+
if workspace_path:
|
|
72
|
+
path = Path(workspace_path).resolve()
|
|
73
|
+
elif request and getattr(request.app.state, "default_workspace_path", None):
|
|
74
|
+
path = Path(request.app.state.default_workspace_path).resolve()
|
|
75
|
+
else:
|
|
76
|
+
# Fall back to current working directory
|
|
77
|
+
path = Path.cwd()
|
|
78
|
+
|
|
79
|
+
# Validate workspace exists
|
|
80
|
+
# Note: Avoid exposing full filesystem paths in error messages for hosted deployments
|
|
81
|
+
if not workspace_exists(path):
|
|
82
|
+
raise HTTPException(
|
|
83
|
+
status_code=404,
|
|
84
|
+
detail="Workspace not found at specified path. Initialize with 'cf init <path>'",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
workspace = get_workspace(path)
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
raise HTTPException(
|
|
91
|
+
status_code=404,
|
|
92
|
+
detail="Workspace not found at specified path. Initialize with 'cf init <path>'",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Note: get_workspace() raises FileNotFoundError rather than returning None,
|
|
96
|
+
# so no additional null check is needed here.
|
|
97
|
+
return workspace
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"get_workspace_manager",
|
|
102
|
+
"get_v2_workspace",
|
|
103
|
+
]
|