repr-cli 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.
- repr/__init__.py +10 -0
- repr/analyzer.py +915 -0
- repr/api.py +263 -0
- repr/auth.py +300 -0
- repr/cli.py +858 -0
- repr/config.py +392 -0
- repr/discovery.py +472 -0
- repr/extractor.py +388 -0
- repr/highlights.py +712 -0
- repr/openai_analysis.py +597 -0
- repr/tools.py +446 -0
- repr/ui.py +430 -0
- repr_cli-0.1.0.dist-info/METADATA +326 -0
- repr_cli-0.1.0.dist-info/RECORD +18 -0
- repr_cli-0.1.0.dist-info/WHEEL +5 -0
- repr_cli-0.1.0.dist-info/entry_points.txt +2 -0
- repr_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- repr_cli-0.1.0.dist-info/top_level.txt +1 -0
repr/ui.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich terminal UI components for beautiful CLI output.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.console import Console, Group
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.progress import (
|
|
8
|
+
BarColumn,
|
|
9
|
+
Progress,
|
|
10
|
+
SpinnerColumn,
|
|
11
|
+
TaskProgressColumn,
|
|
12
|
+
TextColumn,
|
|
13
|
+
TimeElapsedColumn,
|
|
14
|
+
)
|
|
15
|
+
from rich.style import Style
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich.markdown import Markdown
|
|
19
|
+
from rich.live import Live
|
|
20
|
+
from rich.layout import Layout
|
|
21
|
+
from rich import box
|
|
22
|
+
|
|
23
|
+
from . import __version__
|
|
24
|
+
from .config import is_dev_mode
|
|
25
|
+
|
|
26
|
+
# Console instance for consistent output
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# Brand colors
|
|
30
|
+
BRAND_PRIMARY = "#6366f1" # Indigo
|
|
31
|
+
BRAND_SUCCESS = "#22c55e" # Green
|
|
32
|
+
BRAND_WARNING = "#eab308" # Yellow
|
|
33
|
+
BRAND_ERROR = "#ef4444" # Red
|
|
34
|
+
BRAND_INFO = "#06b6d4" # Cyan
|
|
35
|
+
BRAND_MUTED = "#6b7280" # Gray
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_header() -> None:
|
|
39
|
+
"""Print the branded CLI header."""
|
|
40
|
+
header_text = Text()
|
|
41
|
+
header_text.append("🚀 ", style="bold")
|
|
42
|
+
header_text.append("ResumeFlow CLI", style=f"bold {BRAND_PRIMARY}")
|
|
43
|
+
header_text.append(f" v{__version__}", style=BRAND_MUTED)
|
|
44
|
+
|
|
45
|
+
panel = Panel(
|
|
46
|
+
header_text,
|
|
47
|
+
box=box.ROUNDED,
|
|
48
|
+
border_style=BRAND_PRIMARY,
|
|
49
|
+
padding=(0, 2),
|
|
50
|
+
)
|
|
51
|
+
console.print(panel)
|
|
52
|
+
console.print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_success(message: str) -> None:
|
|
56
|
+
"""Print a success message."""
|
|
57
|
+
console.print(f"[bold {BRAND_SUCCESS}]✓[/] {message}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def print_error(message: str) -> None:
|
|
61
|
+
"""Print an error message."""
|
|
62
|
+
console.print(f"[bold {BRAND_ERROR}]✗[/] {message}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def print_warning(message: str) -> None:
|
|
66
|
+
"""Print a warning message."""
|
|
67
|
+
console.print(f"[bold {BRAND_WARNING}]⚠[/] {message}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_info(message: str) -> None:
|
|
71
|
+
"""Print an info message."""
|
|
72
|
+
console.print(f"[{BRAND_INFO}]ℹ[/] {message}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def print_step(message: str, completed: bool = False, pending: bool = False) -> None:
|
|
76
|
+
"""Print a step indicator."""
|
|
77
|
+
if completed:
|
|
78
|
+
symbol = f"[bold {BRAND_SUCCESS}]✓[/]"
|
|
79
|
+
elif pending:
|
|
80
|
+
symbol = f"[{BRAND_MUTED}]○[/]"
|
|
81
|
+
else:
|
|
82
|
+
symbol = f"[bold {BRAND_PRIMARY}]●[/]"
|
|
83
|
+
|
|
84
|
+
console.print(f" ├── {symbol} {message}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_last_step(message: str, completed: bool = False, pending: bool = False) -> None:
|
|
88
|
+
"""Print the last step indicator."""
|
|
89
|
+
if completed:
|
|
90
|
+
symbol = f"[bold {BRAND_SUCCESS}]✓[/]"
|
|
91
|
+
elif pending:
|
|
92
|
+
symbol = f"[{BRAND_MUTED}]○[/]"
|
|
93
|
+
else:
|
|
94
|
+
symbol = f"[bold {BRAND_PRIMARY}]●[/]"
|
|
95
|
+
|
|
96
|
+
console.print(f" └── {symbol} {message}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_next_steps(steps: list[str]) -> None:
|
|
100
|
+
"""Print next steps section."""
|
|
101
|
+
console.print()
|
|
102
|
+
console.print("[bold]Next steps:[/]")
|
|
103
|
+
for step in steps:
|
|
104
|
+
console.print(f" [bold {BRAND_INFO}]→[/] {step}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def create_repo_table() -> Table:
|
|
108
|
+
"""Create a table for repository analysis display."""
|
|
109
|
+
table = Table(
|
|
110
|
+
box=box.ROUNDED,
|
|
111
|
+
border_style=BRAND_MUTED,
|
|
112
|
+
show_header=True,
|
|
113
|
+
header_style="bold",
|
|
114
|
+
)
|
|
115
|
+
table.add_column("Repository", style="bold")
|
|
116
|
+
table.add_column("Language", style=BRAND_INFO)
|
|
117
|
+
table.add_column("Commits", justify="right")
|
|
118
|
+
table.add_column("Age", justify="right")
|
|
119
|
+
table.add_column("Status", justify="center")
|
|
120
|
+
return table
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def add_repo_row(
|
|
124
|
+
table: Table,
|
|
125
|
+
name: str,
|
|
126
|
+
language: str | None = None,
|
|
127
|
+
commits: int | None = None,
|
|
128
|
+
age: str | None = None,
|
|
129
|
+
status: str = "pending",
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Add a row to the repository table."""
|
|
132
|
+
status_map = {
|
|
133
|
+
"pending": f"[{BRAND_MUTED}]○ Pending[/]",
|
|
134
|
+
"analyzing": f"[bold {BRAND_PRIMARY}]● Analyzing...[/]",
|
|
135
|
+
"completed": f"[bold {BRAND_SUCCESS}]✓[/]",
|
|
136
|
+
"skipped": f"[{BRAND_MUTED}]⊘ skipped[/]",
|
|
137
|
+
"error": f"[bold {BRAND_ERROR}]✗ Error[/]",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
table.add_row(
|
|
141
|
+
name,
|
|
142
|
+
language or "-",
|
|
143
|
+
str(commits) if commits else "-",
|
|
144
|
+
age or "-",
|
|
145
|
+
status_map.get(status, status),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_profile_table() -> Table:
|
|
150
|
+
"""Create a table for profile listing."""
|
|
151
|
+
table = Table(
|
|
152
|
+
box=box.ROUNDED,
|
|
153
|
+
border_style=BRAND_MUTED,
|
|
154
|
+
show_header=True,
|
|
155
|
+
header_style="bold",
|
|
156
|
+
)
|
|
157
|
+
table.add_column("Profile", style="bold")
|
|
158
|
+
table.add_column("Projects", justify="right")
|
|
159
|
+
table.add_column("Size", justify="right")
|
|
160
|
+
table.add_column("Status", justify="center")
|
|
161
|
+
return table
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_analysis_progress() -> Progress:
|
|
165
|
+
"""Create a progress bar for analysis."""
|
|
166
|
+
return Progress(
|
|
167
|
+
SpinnerColumn(style=BRAND_PRIMARY),
|
|
168
|
+
TextColumn("[bold]{task.description}"),
|
|
169
|
+
BarColumn(bar_width=30, style=BRAND_MUTED, complete_style=BRAND_PRIMARY),
|
|
170
|
+
TaskProgressColumn(),
|
|
171
|
+
TimeElapsedColumn(),
|
|
172
|
+
console=console,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def create_simple_progress() -> Progress:
|
|
177
|
+
"""Create a simple progress indicator."""
|
|
178
|
+
return Progress(
|
|
179
|
+
SpinnerColumn(style=BRAND_PRIMARY),
|
|
180
|
+
TextColumn("[bold]{task.description}"),
|
|
181
|
+
console=console,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def print_panel(title: str, content: str, border_color: str = BRAND_PRIMARY) -> None:
|
|
186
|
+
"""Print content in a panel."""
|
|
187
|
+
panel = Panel(
|
|
188
|
+
content,
|
|
189
|
+
title=title,
|
|
190
|
+
box=box.ROUNDED,
|
|
191
|
+
border_style=border_color,
|
|
192
|
+
padding=(1, 2),
|
|
193
|
+
)
|
|
194
|
+
console.print(panel)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def print_markdown(content: str) -> None:
|
|
198
|
+
"""Print markdown content."""
|
|
199
|
+
md = Markdown(content)
|
|
200
|
+
console.print(md)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def print_profile_preview(content: str, max_lines: int = 15) -> None:
|
|
204
|
+
"""Print a preview of a profile."""
|
|
205
|
+
lines = content.split("\n")
|
|
206
|
+
preview = "\n".join(lines[:max_lines])
|
|
207
|
+
if len(lines) > max_lines:
|
|
208
|
+
preview += "\n\n..."
|
|
209
|
+
|
|
210
|
+
panel = Panel(
|
|
211
|
+
Markdown(preview),
|
|
212
|
+
box=box.ROUNDED,
|
|
213
|
+
border_style=BRAND_PRIMARY,
|
|
214
|
+
padding=(1, 2),
|
|
215
|
+
)
|
|
216
|
+
console.print(panel)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def print_auth_code(code: str) -> None:
|
|
220
|
+
"""Print the authentication code prominently."""
|
|
221
|
+
code_text = Text(code, style=f"bold {BRAND_PRIMARY}")
|
|
222
|
+
panel = Panel(
|
|
223
|
+
code_text,
|
|
224
|
+
box=box.ROUNDED,
|
|
225
|
+
border_style=BRAND_PRIMARY,
|
|
226
|
+
padding=(0, 4),
|
|
227
|
+
)
|
|
228
|
+
console.print()
|
|
229
|
+
console.print(panel, justify="center")
|
|
230
|
+
console.print()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def print_connection_status(connected: bool) -> None:
|
|
234
|
+
"""Print WebSocket connection status."""
|
|
235
|
+
host = "localhost:8003" if is_dev_mode() else "resumeflow.dev"
|
|
236
|
+
if connected:
|
|
237
|
+
console.print(f"Connected to {host} [{BRAND_SUCCESS}]●[/]")
|
|
238
|
+
else:
|
|
239
|
+
console.print(f"Disconnected from {host} [{BRAND_ERROR}]●[/]")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def format_bytes(size: int) -> str:
|
|
243
|
+
"""Format bytes to human readable string."""
|
|
244
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
245
|
+
if size < 1024:
|
|
246
|
+
return f"{size:.1f} {unit}"
|
|
247
|
+
size /= 1024
|
|
248
|
+
return f"{size:.1f} TB"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def format_duration(months: int) -> str:
|
|
252
|
+
"""Format duration in months to human readable string."""
|
|
253
|
+
if months < 1:
|
|
254
|
+
return "< 1 mo"
|
|
255
|
+
elif months < 12:
|
|
256
|
+
return f"{months} mo"
|
|
257
|
+
else:
|
|
258
|
+
years = months // 12
|
|
259
|
+
remaining_months = months % 12
|
|
260
|
+
if remaining_months == 0:
|
|
261
|
+
return f"{years} yr"
|
|
262
|
+
return f"{years} yr {remaining_months} mo"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _AnimatedRenderable:
|
|
266
|
+
"""A renderable wrapper that calls the render function on each refresh."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, render_func):
|
|
269
|
+
self.render_func = render_func
|
|
270
|
+
|
|
271
|
+
def __rich_console__(self, console, options):
|
|
272
|
+
yield self.render_func()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class AnalysisDisplay:
|
|
276
|
+
"""Live display for analysis progress with animations."""
|
|
277
|
+
|
|
278
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
279
|
+
PROGRESS_CHARS = ["░", "▒", "▓", "█"]
|
|
280
|
+
|
|
281
|
+
def __init__(self):
|
|
282
|
+
self.repos: list[dict] = []
|
|
283
|
+
self.current_step = ""
|
|
284
|
+
self.current_detail = ""
|
|
285
|
+
self.current_repo = ""
|
|
286
|
+
self.progress_pct: float = 0.0
|
|
287
|
+
self.connected = False
|
|
288
|
+
self.live: Live | None = None
|
|
289
|
+
self._frame: int = 0
|
|
290
|
+
self._tick: int = 0
|
|
291
|
+
|
|
292
|
+
def _get_spinner(self) -> str:
|
|
293
|
+
return self.SPINNER_FRAMES[self._frame % len(self.SPINNER_FRAMES)]
|
|
294
|
+
|
|
295
|
+
def _get_progress_bar(self, width: int = 30) -> Text:
|
|
296
|
+
filled = int(self.progress_pct / 100 * width)
|
|
297
|
+
shimmer_pos = self._tick % (width + 5)
|
|
298
|
+
|
|
299
|
+
bar = Text()
|
|
300
|
+
for i in range(width):
|
|
301
|
+
if i < filled:
|
|
302
|
+
if i == shimmer_pos or i == shimmer_pos - 1:
|
|
303
|
+
bar.append("█", style=f"bold {BRAND_PRIMARY}")
|
|
304
|
+
else:
|
|
305
|
+
bar.append("█", style=BRAND_PRIMARY)
|
|
306
|
+
elif i == filled and filled < width:
|
|
307
|
+
partial_idx = int((self.progress_pct % (100/width)) / (100/width) * 4)
|
|
308
|
+
bar.append(self.PROGRESS_CHARS[min(partial_idx, 3)], style=BRAND_PRIMARY)
|
|
309
|
+
else:
|
|
310
|
+
bar.append("░", style=BRAND_MUTED)
|
|
311
|
+
|
|
312
|
+
return bar
|
|
313
|
+
|
|
314
|
+
def _get_animated_status(self, status: str) -> str:
|
|
315
|
+
if status == "analyzing":
|
|
316
|
+
spinner = self._get_spinner()
|
|
317
|
+
colors = [BRAND_PRIMARY, "#818cf8", "#a5b4fc", "#818cf8"]
|
|
318
|
+
color = colors[self._frame % len(colors)]
|
|
319
|
+
return f"[bold {color}]{spinner} Analyzing[/]"
|
|
320
|
+
elif status == "pending":
|
|
321
|
+
dots = "." * ((self._tick // 2) % 4)
|
|
322
|
+
return f"[{BRAND_MUTED}]○ Waiting{dots}[/]"
|
|
323
|
+
elif status == "completed":
|
|
324
|
+
return f"[bold {BRAND_SUCCESS}]✓ Done[/]"
|
|
325
|
+
elif status == "skipped":
|
|
326
|
+
return f"[{BRAND_MUTED}]⊘ Skipped[/]"
|
|
327
|
+
elif status == "error":
|
|
328
|
+
return f"[bold {BRAND_ERROR}]✗ Error[/]"
|
|
329
|
+
return status
|
|
330
|
+
|
|
331
|
+
def _render(self):
|
|
332
|
+
self._frame = (self._frame + 1) % len(self.SPINNER_FRAMES)
|
|
333
|
+
self._tick += 1
|
|
334
|
+
|
|
335
|
+
lines = []
|
|
336
|
+
|
|
337
|
+
if self.current_step:
|
|
338
|
+
spinner = self._get_spinner()
|
|
339
|
+
|
|
340
|
+
# Map step names to nice icons
|
|
341
|
+
step_icons = {
|
|
342
|
+
"Starting": "🚀",
|
|
343
|
+
"Extracting": "📂",
|
|
344
|
+
"Preparing": "📋",
|
|
345
|
+
"Analyzing": "🔍",
|
|
346
|
+
"Synthesizing": "✨",
|
|
347
|
+
"Merging": "🔗",
|
|
348
|
+
"Finalizing": "📝",
|
|
349
|
+
"Complete": "✅",
|
|
350
|
+
}
|
|
351
|
+
icon = step_icons.get(self.current_step, "")
|
|
352
|
+
|
|
353
|
+
step_text = self.current_step.replace("_", " ").title()
|
|
354
|
+
status_line = Text()
|
|
355
|
+
status_line.append(f"{spinner} ", style=f"bold {BRAND_PRIMARY}")
|
|
356
|
+
if icon:
|
|
357
|
+
status_line.append(f"{icon} ", style="")
|
|
358
|
+
status_line.append(step_text, style="bold")
|
|
359
|
+
|
|
360
|
+
if self.current_repo:
|
|
361
|
+
status_line.append(" → ", style=BRAND_MUTED)
|
|
362
|
+
status_line.append(self.current_repo, style=BRAND_INFO)
|
|
363
|
+
|
|
364
|
+
lines.append(status_line)
|
|
365
|
+
|
|
366
|
+
if self.current_detail:
|
|
367
|
+
detail_line = Text()
|
|
368
|
+
detail_line.append(" ", style="")
|
|
369
|
+
detail_line.append(self.current_detail, style=BRAND_MUTED)
|
|
370
|
+
lines.append(detail_line)
|
|
371
|
+
|
|
372
|
+
lines.append(Text())
|
|
373
|
+
|
|
374
|
+
# Always show progress bar once we have a step (even at 0%)
|
|
375
|
+
if self.current_step:
|
|
376
|
+
progress_line = Text()
|
|
377
|
+
progress_line.append(" ")
|
|
378
|
+
progress_line.append_text(self._get_progress_bar(width=40))
|
|
379
|
+
progress_line.append(f" {self.progress_pct:.0f}%", style=f"bold {BRAND_PRIMARY}")
|
|
380
|
+
lines.append(progress_line)
|
|
381
|
+
lines.append(Text())
|
|
382
|
+
|
|
383
|
+
return Group(*lines) if lines else Text("")
|
|
384
|
+
|
|
385
|
+
def start(self) -> None:
|
|
386
|
+
"""Start the live display."""
|
|
387
|
+
# Use _AnimatedRenderable so Rich calls _render() on each refresh cycle
|
|
388
|
+
self.live = Live(
|
|
389
|
+
_AnimatedRenderable(self._render),
|
|
390
|
+
console=console,
|
|
391
|
+
refresh_per_second=10,
|
|
392
|
+
transient=False,
|
|
393
|
+
)
|
|
394
|
+
self.live.start()
|
|
395
|
+
|
|
396
|
+
def stop(self) -> None:
|
|
397
|
+
"""Stop the live display."""
|
|
398
|
+
if self.live:
|
|
399
|
+
self.live.stop()
|
|
400
|
+
|
|
401
|
+
def update_progress(
|
|
402
|
+
self,
|
|
403
|
+
step: str | None = None,
|
|
404
|
+
detail: str | None = None,
|
|
405
|
+
repo: str | None = None,
|
|
406
|
+
progress: float | None = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Update progress information."""
|
|
409
|
+
if step is not None:
|
|
410
|
+
self.current_step = step
|
|
411
|
+
if detail is not None:
|
|
412
|
+
self.current_detail = detail
|
|
413
|
+
if repo is not None:
|
|
414
|
+
self.current_repo = repo
|
|
415
|
+
if progress is not None:
|
|
416
|
+
self.progress_pct = progress
|
|
417
|
+
|
|
418
|
+
def update_repo(self, name: str, **kwargs) -> None:
|
|
419
|
+
"""Update a repository's display status."""
|
|
420
|
+
for repo in self.repos:
|
|
421
|
+
if repo["name"] == name:
|
|
422
|
+
repo.update(kwargs)
|
|
423
|
+
break
|
|
424
|
+
else:
|
|
425
|
+
self.repos.append({"name": name, **kwargs})
|
|
426
|
+
|
|
427
|
+
def set_repos(self, repos: list[dict]) -> None:
|
|
428
|
+
"""Set all repositories."""
|
|
429
|
+
self.repos = repos
|
|
430
|
+
|