repr-cli 0.1.0__py3-none-any.whl → 0.2.2__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 +1 -1
- repr/__main__.py +6 -0
- repr/api.py +127 -1
- repr/auth.py +66 -2
- repr/cli.py +2143 -663
- repr/config.py +658 -32
- repr/discovery.py +5 -0
- repr/doctor.py +458 -0
- repr/hooks.py +634 -0
- repr/keychain.py +255 -0
- repr/llm.py +506 -0
- repr/openai_analysis.py +92 -21
- repr/privacy.py +333 -0
- repr/storage.py +527 -0
- repr/templates.py +229 -0
- repr/tools.py +202 -0
- repr/ui.py +79 -364
- repr_cli-0.2.2.dist-info/METADATA +263 -0
- repr_cli-0.2.2.dist-info/RECORD +24 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/licenses/LICENSE +1 -1
- repr/analyzer.py +0 -915
- repr/highlights.py +0 -712
- repr_cli-0.1.0.dist-info/METADATA +0 -326
- repr_cli-0.1.0.dist-info/RECORD +0 -18
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/WHEEL +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/entry_points.txt +0 -0
- {repr_cli-0.1.0.dist-info → repr_cli-0.2.2.dist-info}/top_level.txt +0 -0
repr/ui.py
CHANGED
|
@@ -1,197 +1,60 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Terminal UI utilities for repr CLI.
|
|
3
|
+
|
|
4
|
+
Simple, focused output helpers using Rich.
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
|
-
from rich.console import Console
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.markdown import Markdown
|
|
6
9
|
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
10
|
from rich.table import Table
|
|
17
|
-
from rich.
|
|
18
|
-
from rich.
|
|
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()
|
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
12
|
+
from rich.prompt import Confirm
|
|
28
13
|
|
|
29
14
|
# Brand colors
|
|
30
15
|
BRAND_PRIMARY = "#6366f1" # Indigo
|
|
31
16
|
BRAND_SUCCESS = "#22c55e" # Green
|
|
32
|
-
BRAND_WARNING = "#
|
|
17
|
+
BRAND_WARNING = "#f59e0b" # Amber
|
|
33
18
|
BRAND_ERROR = "#ef4444" # Red
|
|
34
|
-
BRAND_INFO = "#06b6d4" # Cyan
|
|
35
19
|
BRAND_MUTED = "#6b7280" # Gray
|
|
36
20
|
|
|
21
|
+
# Console instance
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
37
24
|
|
|
38
25
|
def print_header() -> None:
|
|
39
|
-
"""Print the
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
26
|
+
"""Print the repr header/banner."""
|
|
27
|
+
console.print()
|
|
28
|
+
console.print(f"[bold {BRAND_PRIMARY}]repr[/] - understand what you've actually worked on")
|
|
52
29
|
console.print()
|
|
53
30
|
|
|
54
31
|
|
|
55
32
|
def print_success(message: str) -> None:
|
|
56
33
|
"""Print a success message."""
|
|
57
|
-
console.print(f"[
|
|
34
|
+
console.print(f"[{BRAND_SUCCESS}]✓[/] {message}")
|
|
58
35
|
|
|
59
36
|
|
|
60
37
|
def print_error(message: str) -> None:
|
|
61
38
|
"""Print an error message."""
|
|
62
|
-
console.print(f"[
|
|
39
|
+
console.print(f"[{BRAND_ERROR}]✗[/] {message}")
|
|
63
40
|
|
|
64
41
|
|
|
65
42
|
def print_warning(message: str) -> None:
|
|
66
43
|
"""Print a warning message."""
|
|
67
|
-
console.print(f"[
|
|
44
|
+
console.print(f"[{BRAND_WARNING}]⚠[/] {message}")
|
|
68
45
|
|
|
69
46
|
|
|
70
47
|
def print_info(message: str) -> None:
|
|
71
48
|
"""Print an info message."""
|
|
72
|
-
console.print(f"[{
|
|
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}")
|
|
49
|
+
console.print(f"[{BRAND_MUTED}]ℹ[/] {message}")
|
|
97
50
|
|
|
98
51
|
|
|
99
52
|
def print_next_steps(steps: list[str]) -> None:
|
|
100
|
-
"""Print next steps
|
|
53
|
+
"""Print next steps suggestions."""
|
|
101
54
|
console.print()
|
|
102
55
|
console.print("[bold]Next steps:[/]")
|
|
103
56
|
for step in steps:
|
|
104
|
-
console.print(f"
|
|
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)
|
|
57
|
+
console.print(f" {step}")
|
|
195
58
|
|
|
196
59
|
|
|
197
60
|
def print_markdown(content: str) -> None:
|
|
@@ -200,47 +63,46 @@ def print_markdown(content: str) -> None:
|
|
|
200
63
|
console.print(md)
|
|
201
64
|
|
|
202
65
|
|
|
203
|
-
def
|
|
204
|
-
"""Print a
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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)
|
|
66
|
+
def print_panel(title: str, content: str = "", border_color: str = BRAND_PRIMARY) -> None:
|
|
67
|
+
"""Print a bordered panel."""
|
|
68
|
+
if content:
|
|
69
|
+
console.print(Panel(content, title=title, border_style=border_color))
|
|
70
|
+
else:
|
|
71
|
+
console.print(Panel(title, border_style=border_color))
|
|
217
72
|
|
|
218
73
|
|
|
219
74
|
def print_auth_code(code: str) -> None:
|
|
220
|
-
"""Print
|
|
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
|
-
)
|
|
75
|
+
"""Print an auth code prominently."""
|
|
228
76
|
console.print()
|
|
229
|
-
console.print(
|
|
77
|
+
console.print(Panel(
|
|
78
|
+
f"[bold white on {BRAND_PRIMARY}] {code} [/]",
|
|
79
|
+
title="Enter this code",
|
|
80
|
+
border_style=BRAND_PRIMARY,
|
|
81
|
+
padding=(1, 4),
|
|
82
|
+
))
|
|
230
83
|
console.print()
|
|
231
84
|
|
|
232
85
|
|
|
233
|
-
def
|
|
234
|
-
"""
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
86
|
+
def create_spinner(message: str = "Working...") -> Progress:
|
|
87
|
+
"""Create a spinner progress indicator."""
|
|
88
|
+
return Progress(
|
|
89
|
+
SpinnerColumn(),
|
|
90
|
+
TextColumn("[progress.description]{task.description}"),
|
|
91
|
+
console=console,
|
|
92
|
+
transient=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_table(title: str, columns: list[str]) -> Table:
|
|
97
|
+
"""Create a styled table."""
|
|
98
|
+
table = Table(title=title, border_style=BRAND_MUTED)
|
|
99
|
+
for col in columns:
|
|
100
|
+
table.add_column(col)
|
|
101
|
+
return table
|
|
240
102
|
|
|
241
103
|
|
|
242
104
|
def format_bytes(size: int) -> str:
|
|
243
|
-
"""Format bytes to human readable
|
|
105
|
+
"""Format bytes to human readable."""
|
|
244
106
|
for unit in ["B", "KB", "MB", "GB"]:
|
|
245
107
|
if size < 1024:
|
|
246
108
|
return f"{size:.1f} {unit}"
|
|
@@ -248,183 +110,36 @@ def format_bytes(size: int) -> str:
|
|
|
248
110
|
return f"{size:.1f} TB"
|
|
249
111
|
|
|
250
112
|
|
|
251
|
-
def
|
|
252
|
-
"""Format
|
|
253
|
-
|
|
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
|
|
113
|
+
def format_relative_time(iso_date: str) -> str:
|
|
114
|
+
"""Format ISO date as relative time."""
|
|
115
|
+
from datetime import datetime
|
|
270
116
|
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
117
|
+
try:
|
|
118
|
+
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
|
|
119
|
+
now = datetime.now(dt.tzinfo)
|
|
120
|
+
delta = now - dt
|
|
334
121
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
122
|
+
if delta.days > 365:
|
|
123
|
+
years = delta.days // 365
|
|
124
|
+
return f"{years}y ago"
|
|
125
|
+
elif delta.days > 30:
|
|
126
|
+
months = delta.days // 30
|
|
127
|
+
return f"{months}mo ago"
|
|
128
|
+
elif delta.days > 0:
|
|
129
|
+
return f"{delta.days}d ago"
|
|
130
|
+
elif delta.seconds > 3600:
|
|
131
|
+
hours = delta.seconds // 3600
|
|
132
|
+
return f"{hours}h ago"
|
|
133
|
+
elif delta.seconds > 60:
|
|
134
|
+
mins = delta.seconds // 60
|
|
135
|
+
return f"{mins}m ago"
|
|
424
136
|
else:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
137
|
+
return "just now"
|
|
138
|
+
except (ValueError, TypeError):
|
|
139
|
+
return iso_date[:10] if iso_date else "unknown"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def confirm(message: str, default: bool = False) -> bool:
|
|
143
|
+
"""Prompt for confirmation."""
|
|
144
|
+
return Confirm.ask(message, default=default)
|
|
430
145
|
|