crespo 1.0.0__tar.gz
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.
- crespo-1.0.0/CLI/crespo/__init__.py +0 -0
- crespo-1.0.0/CLI/crespo/cli.py +504 -0
- crespo-1.0.0/CLI/crespo/config.py +421 -0
- crespo-1.0.0/CLI/crespo/counter.py +19 -0
- crespo-1.0.0/CLI/crespo/cresbee.png +0 -0
- crespo-1.0.0/CLI/crespo/generate.py +200 -0
- crespo-1.0.0/CLI/crespo/keystore.py +58 -0
- crespo-1.0.0/CLI/crespo/main.py +275 -0
- crespo-1.0.0/CLI/crespo/parse.py +249 -0
- crespo-1.0.0/CLI/crespo/security.py +17 -0
- crespo-1.0.0/CLI/crespo/summary.py +179 -0
- crespo-1.0.0/CLI/crespo/tests/__init__.py +0 -0
- crespo-1.0.0/CLI/crespo/tests/test.py +331 -0
- crespo-1.0.0/CLI/crespo/walker.py +125 -0
- crespo-1.0.0/CLI/crespo.egg-info/PKG-INFO +32 -0
- crespo-1.0.0/CLI/crespo.egg-info/SOURCES.txt +23 -0
- crespo-1.0.0/CLI/crespo.egg-info/dependency_links.txt +1 -0
- crespo-1.0.0/CLI/crespo.egg-info/entry_points.txt +2 -0
- crespo-1.0.0/CLI/crespo.egg-info/requires.txt +12 -0
- crespo-1.0.0/CLI/crespo.egg-info/top_level.txt +1 -0
- crespo-1.0.0/LICENSE +21 -0
- crespo-1.0.0/MANIFEST.in +4 -0
- crespo-1.0.0/PKG-INFO +32 -0
- crespo-1.0.0/pyproject.toml +62 -0
- crespo-1.0.0/setup.cfg +4 -0
|
File without changes
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from rich.columns import Columns
|
|
14
|
+
from rich.rule import Rule
|
|
15
|
+
from rich.align import Align
|
|
16
|
+
from rich import print as rprint
|
|
17
|
+
from rich.tree import Tree
|
|
18
|
+
from rich import box
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# ─── Cresbee pixel art renderer ───────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def _ansi_fg(r, g, b): return f"\033[38;2;{r};{g};{b}m"
|
|
26
|
+
def _ansi_bg(r, g, b): return f"\033[48;2;{r};{g};{b}m"
|
|
27
|
+
RESET = "\033[0m"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_cresbee(image_path: str, width: int = 38) -> list[str]:
|
|
31
|
+
"""
|
|
32
|
+
Render Cresbee PNG as half-block (▀) terminal art.
|
|
33
|
+
Uses NEAREST resampling for sharp pixel-art edges.
|
|
34
|
+
Alpha threshold 80 keeps only solid pixels — no blurry anti-alias halo.
|
|
35
|
+
Returns a list of ANSI strings, one per terminal row.
|
|
36
|
+
Falls back to ASCII art on any error.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
from PIL import Image
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
img = Image.open(image_path).convert("RGBA")
|
|
43
|
+
arr = np.array(img).copy()
|
|
44
|
+
|
|
45
|
+
# ── strip near-black background ──────────────────────────────────────
|
|
46
|
+
r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]
|
|
47
|
+
bg = (r < 22) & (g < 22) & (b < 22)
|
|
48
|
+
arr[bg, 3] = 0
|
|
49
|
+
|
|
50
|
+
clean = Image.fromarray(arr, "RGBA")
|
|
51
|
+
bbox = clean.getbbox()
|
|
52
|
+
if not bbox:
|
|
53
|
+
return _fallback_cresbee()
|
|
54
|
+
|
|
55
|
+
cropped = clean.crop(bbox)
|
|
56
|
+
|
|
57
|
+
# ── resize with NEAREST for hard pixel-art edges ─────────────────────
|
|
58
|
+
aspect = cropped.height / cropped.width
|
|
59
|
+
px_height = int(width * aspect)
|
|
60
|
+
# must be even for ▀/▄ pairing
|
|
61
|
+
if px_height % 2:
|
|
62
|
+
px_height += 1
|
|
63
|
+
|
|
64
|
+
resized = cropped.resize((width, px_height), Image.NEAREST)
|
|
65
|
+
px = np.array(resized)
|
|
66
|
+
|
|
67
|
+
ALPHA_THRESH = 80 # pixels below this alpha → transparent
|
|
68
|
+
|
|
69
|
+
lines = []
|
|
70
|
+
for y in range(0, px_height, 2):
|
|
71
|
+
row = ""
|
|
72
|
+
for x in range(width):
|
|
73
|
+
top = px[y, x]
|
|
74
|
+
bot = px[y + 1, x] if (y + 1) < px_height else (0, 0, 0, 0)
|
|
75
|
+
|
|
76
|
+
t_on = int(top[3]) >= ALPHA_THRESH
|
|
77
|
+
b_on = int(bot[3]) >= ALPHA_THRESH
|
|
78
|
+
|
|
79
|
+
if t_on and b_on:
|
|
80
|
+
tr, tg, tb = int(top[0]), int(top[1]), int(top[2])
|
|
81
|
+
br, bg_c, bb = int(bot[0]), int(bot[1]), int(bot[2])
|
|
82
|
+
row += (
|
|
83
|
+
_ansi_fg(tr, tg, tb)
|
|
84
|
+
+ _ansi_bg(br, bg_c, bb)
|
|
85
|
+
+ "▀"
|
|
86
|
+
+ RESET
|
|
87
|
+
)
|
|
88
|
+
elif t_on:
|
|
89
|
+
tr, tg, tb = int(top[0]), int(top[1]), int(top[2])
|
|
90
|
+
row += _ansi_fg(tr, tg, tb) + "▀" + RESET
|
|
91
|
+
elif b_on:
|
|
92
|
+
br, bg_c, bb = int(bot[0]), int(bot[1]), int(bot[2])
|
|
93
|
+
row += _ansi_fg(br, bg_c, bb) + "▄" + RESET
|
|
94
|
+
else:
|
|
95
|
+
row += " "
|
|
96
|
+
lines.append(row)
|
|
97
|
+
|
|
98
|
+
return lines
|
|
99
|
+
|
|
100
|
+
except Exception:
|
|
101
|
+
return _fallback_cresbee()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _fallback_cresbee() -> list[str]:
|
|
105
|
+
"""ASCII fallback when PIL unavailable or image missing."""
|
|
106
|
+
return [
|
|
107
|
+
" \033[38;2;130;110;200m╭──╮ ╭──╮\033[0m",
|
|
108
|
+
" \033[38;2;100;200;140m╭╯\033[0m\033[38;2;130;110;200m ╰─╯ \033[0m\033[38;2;100;200;140m╰╮\033[0m",
|
|
109
|
+
" \033[38;2;100;200;140m│\033[0m \033[1;37m◉\033[0m \033[1;37m◉\033[0m \033[38;2;100;200;140m│\033[0m",
|
|
110
|
+
" \033[38;2;100;200;140m│\033[0m \033[38;2;130;110;200m──\033[0m \033[38;2;100;200;140m│\033[0m",
|
|
111
|
+
" \033[38;2;100;200;140m╰──┬\033[0m\033[38;2;255;200;50m████\033[0m\033[38;2;100;200;140m┬──╯\033[0m",
|
|
112
|
+
" \033[38;2;100;200;140m│\033[0m\033[38;2;255;200;50m 🍯 \033[0m\033[38;2;100;200;140m│\033[0m",
|
|
113
|
+
" \033[38;2;100;200;140m╰────╯\033[0m",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_cresbee(image_path: str | None = None, width: int = 34):
|
|
118
|
+
"""Print Cresbee to terminal."""
|
|
119
|
+
path = image_path or "cresbee.png"
|
|
120
|
+
lines = render_cresbee(path, width=width)
|
|
121
|
+
for line in lines:
|
|
122
|
+
print(line)
|
|
123
|
+
print("\033[0m", end="")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ─── Header ───────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def print_header(image_path: str | None = None):
|
|
129
|
+
"""Full startup header with Cresbee + branding."""
|
|
130
|
+
console.print()
|
|
131
|
+
|
|
132
|
+
# Cresbee on left, branding on right
|
|
133
|
+
cresbee_lines = render_cresbee(image_path or "cresbee.png", width=30)
|
|
134
|
+
|
|
135
|
+
# build branding text
|
|
136
|
+
brand_lines = [
|
|
137
|
+
"",
|
|
138
|
+
"\033[1;38;2;100;200;140m ██████╗██████╗ ███████╗███████╗██████╗ ██████╗ \033[0m",
|
|
139
|
+
"\033[38;2;100;200;140m ██╔════╝ ██╔══██╗██╔════╝██╔════╝██╔══██╗██╔═══██╗\033[0m",
|
|
140
|
+
"\033[38;2;110;180;160m ██║ ██████╔╝█████╗ ███████╗██████╔╝██║ ██║\033[0m",
|
|
141
|
+
"\033[38;2;120;160;180m ██║ ██╔══██╗██╔══╝ ╚════██║██╔═══╝ ██║ ██║\033[0m",
|
|
142
|
+
"\033[38;2;130;110;200m ╚██████╗ ██║ ██║███████╗███████║██║ ╚██████╔╝\033[0m",
|
|
143
|
+
"\033[38;2;140;100;220m ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ \033[0m",
|
|
144
|
+
"",
|
|
145
|
+
"\033[38;2;100;200;140m Crisp repos. Sharp AI.\033[0m",
|
|
146
|
+
"\033[38;2;90;90;120m Give AI the blueprint, not the code.\033[0m",
|
|
147
|
+
"",
|
|
148
|
+
"\033[38;2;100;200;140m v1.0.0\033[0m \033[38;2;90;90;120m• MIT License • pip install crespo\033[0m",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# print side by side
|
|
152
|
+
max_lines = max(len(cresbee_lines), len(brand_lines))
|
|
153
|
+
|
|
154
|
+
for i in range(max_lines):
|
|
155
|
+
left = cresbee_lines[i] if i < len(cresbee_lines) else ""
|
|
156
|
+
right = brand_lines[i] if i < len(brand_lines) else ""
|
|
157
|
+
# pad left column to fixed width (accounting for ANSI codes)
|
|
158
|
+
visible_len = len(left.encode("utf-8").decode("utf-8"))
|
|
159
|
+
# rough padding — cresbee art is ~30 chars wide
|
|
160
|
+
print(f" {left} {right}")
|
|
161
|
+
|
|
162
|
+
print("\033[0m")
|
|
163
|
+
|
|
164
|
+
# separator
|
|
165
|
+
console.print(
|
|
166
|
+
Rule(style="#8c64dc"),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def print_usage():
|
|
171
|
+
"""Print usage panel."""
|
|
172
|
+
console.print(
|
|
173
|
+
Panel(
|
|
174
|
+
Text.from_markup(
|
|
175
|
+
"[bold green]Usage[/bold green]\n"
|
|
176
|
+
" [cyan]crespo[/cyan] [purple]<path|url>[/purple] "
|
|
177
|
+
"[[green]--mode[/green] structure|summary|concat] "
|
|
178
|
+
"[[green]--output[/green] file.xml]\n\n"
|
|
179
|
+
"[bold green]Examples[/bold green]\n"
|
|
180
|
+
" [green]crespo[/green] ./myproject\n"
|
|
181
|
+
" [green]crespo[/green] ./myproject [green]--mode[/green] summary\n"
|
|
182
|
+
" [green]crespo[/green] [green]--git[/green] https://github.com/user/repo\n"
|
|
183
|
+
" [green]crespo[/green] ./myproject [green]--mode[/green] concat "
|
|
184
|
+
"[green]--output[/green] full.xml\n\n"
|
|
185
|
+
"[bold green]Modes[/bold green]\n"
|
|
186
|
+
" [green]structure[/green] :\tAST skeleton only — ~84% token reduction "
|
|
187
|
+
"[dim](default)[/dim]\n"
|
|
188
|
+
" [purple]summary[/purple] :\tstructure + AI function descriptions via Groq\n"
|
|
189
|
+
" [cyan]concat[/cyan] :\tfull source + secrets redacted + structure header\n\n"
|
|
190
|
+
"[bold green]Options[/bold green]\n"
|
|
191
|
+
" [green]--mode[/green] structure | summary | concat\n"
|
|
192
|
+
" [green]--output[/green] Output filename [dim](default: blueprint.xml)[/dim]\n"
|
|
193
|
+
" [green]--groq[/green] Groq API key for summary mode [dim]or set CRESPO_GROQ_KEY env var[/dim]\n"
|
|
194
|
+
" [green]--git[/green] Using git URL.\n"
|
|
195
|
+
" [green]--help[/green] Show this message"
|
|
196
|
+
),
|
|
197
|
+
title="[bold green]Crespo[/bold green]",
|
|
198
|
+
border_style="green",
|
|
199
|
+
padding=(1, 2),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ─── Scan phase ───────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def print_scan_start(source: str, mode: str):
|
|
207
|
+
"""Print scan start info."""
|
|
208
|
+
mode_colors = {
|
|
209
|
+
"structure": "green",
|
|
210
|
+
"summary": "purple",
|
|
211
|
+
"concat": "cyan",
|
|
212
|
+
}
|
|
213
|
+
color = mode_colors.get(mode, "green")
|
|
214
|
+
console.print(
|
|
215
|
+
f" [dim]Source[/dim] [white]{source}[/white]"
|
|
216
|
+
)
|
|
217
|
+
console.print(
|
|
218
|
+
f" [dim]Mode[/dim] [{color}]{mode}[/{color}]"
|
|
219
|
+
)
|
|
220
|
+
console.print(Rule(title="[green][b]DETAILS[/b][/green]",style="#8c64dc"))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
LANG_COLORS = {
|
|
224
|
+
"py": "blue", "js": "yellow", "ts": "cyan",
|
|
225
|
+
"jsx": "yellow", "tsx": "cyan", "rs": "red",
|
|
226
|
+
"go": "cyan", "java": "red", "c": "green", "cpp": "green",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def _lang_badge(lang: str) -> str:
|
|
230
|
+
color = LANG_COLORS.get(lang, "white")
|
|
231
|
+
return f"[{color}]{lang}[/{color}]"
|
|
232
|
+
|
|
233
|
+
def _ext(relpath: str) -> str:
|
|
234
|
+
return Path(relpath).suffix.lstrip(".")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ── Style 1: Classic Rich Tree (├── branches) ─────────────────────────────────
|
|
238
|
+
|
|
239
|
+
def print_tree_classic(found: list[str],root:str = "project"):
|
|
240
|
+
"""Builds a Rich Tree with ├── / └── branch lines."""
|
|
241
|
+
tree = Tree(f"[bold green]{root}[/bold green]")
|
|
242
|
+
nodes: dict[str, Tree] = {}
|
|
243
|
+
|
|
244
|
+
def get_dir_node(parts: tuple) -> Tree:
|
|
245
|
+
if not parts:
|
|
246
|
+
return tree
|
|
247
|
+
if parts in nodes:
|
|
248
|
+
return nodes[parts]
|
|
249
|
+
parent = get_dir_node(parts[:-1])
|
|
250
|
+
node = parent.add(f"[#8c64dc][b]{parts[-1]}[b][/#8c64dc]")
|
|
251
|
+
nodes[parts] = node
|
|
252
|
+
return node
|
|
253
|
+
|
|
254
|
+
for relpath in sorted(found):
|
|
255
|
+
p = Path(relpath)
|
|
256
|
+
lang = _ext(relpath)
|
|
257
|
+
parent = get_dir_node(p.parts[:-1])
|
|
258
|
+
parent.add(f"{p.name} {_lang_badge(lang)}")
|
|
259
|
+
|
|
260
|
+
console.print(Panel(tree,box=box.SIMPLE,border_style="dim"))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ─── Parse phase ──────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
def run_with_progress(files: list[dict], label: str = "Parsing") -> None:
|
|
266
|
+
"""Show progress bar while parsing files."""
|
|
267
|
+
with Progress(
|
|
268
|
+
SpinnerColumn(style="green"),
|
|
269
|
+
TextColumn(" [green]{task.description}[/green]"),
|
|
270
|
+
BarColumn(bar_width=30, style="green", complete_style="bright_green"),
|
|
271
|
+
TaskProgressColumn(),
|
|
272
|
+
TextColumn("[dim]{task.fields[filename]}[/dim]"),
|
|
273
|
+
console=console,
|
|
274
|
+
transient=True,
|
|
275
|
+
) as progress:
|
|
276
|
+
task = progress.add_task(
|
|
277
|
+
label,
|
|
278
|
+
total=len(files),
|
|
279
|
+
filename=""
|
|
280
|
+
)
|
|
281
|
+
for file in files:
|
|
282
|
+
name = Path(file.get("relpath", "")).name
|
|
283
|
+
progress.update(task, advance=1, filename=name)
|
|
284
|
+
time.sleep(0.1) # remove this in real implementation
|
|
285
|
+
|
|
286
|
+
console.print()
|
|
287
|
+
console.print(Rule(style="#8c64dc"))
|
|
288
|
+
console.print(f" [green]✓[/green] [dim]Parsed {len(files)} files[/dim]")
|
|
289
|
+
console.print(Rule(style="#8c64dc"))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def run_summary_progress(files: list[dict]) -> None:
|
|
293
|
+
"""Show Groq API call progress for summary mode."""
|
|
294
|
+
|
|
295
|
+
console.print(" [purple]Calling Groq API...[/purple]")
|
|
296
|
+
console.print()
|
|
297
|
+
|
|
298
|
+
with Progress(
|
|
299
|
+
SpinnerColumn(style="purple"),
|
|
300
|
+
TextColumn(" [purple]{task.description}[/purple]"),
|
|
301
|
+
BarColumn(bar_width=25, style="purple", complete_style="bright_magenta"),
|
|
302
|
+
TaskProgressColumn(),
|
|
303
|
+
console=console,
|
|
304
|
+
transient=True,
|
|
305
|
+
) as progress:
|
|
306
|
+
task = progress.add_task("Summarising", total=len(files))
|
|
307
|
+
for file in files:
|
|
308
|
+
name = Path(file.get("relpath", "")).name
|
|
309
|
+
progress.update(task, description=f"summarising {name}", advance=1)
|
|
310
|
+
time.sleep(0.08)
|
|
311
|
+
|
|
312
|
+
console.print(f" [purple]✓[/purple] [dim]Summaries generated[/dim]")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ─── Security phase ───────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
def print_security_result(secrets_found: int, files_scanned: int):
|
|
318
|
+
"""Print security scan result."""
|
|
319
|
+
if secrets_found == 0:
|
|
320
|
+
console.print(
|
|
321
|
+
f" [green]✓[/green] [dim]Security scan — {files_scanned} files — "
|
|
322
|
+
f"no secrets detected[/dim]"
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
console.print(
|
|
326
|
+
f" [yellow]⚠[/yellow] [yellow]Security scan — "
|
|
327
|
+
f"{secrets_found} secret(s) redacted[/yellow]"
|
|
328
|
+
)
|
|
329
|
+
console.print(Rule(style="#8c64dc"))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ─── Stats ────────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
def print_token_stats(
|
|
335
|
+
original_tokens: int,
|
|
336
|
+
output_tokens: int,
|
|
337
|
+
mode: str,
|
|
338
|
+
output_file: str,
|
|
339
|
+
elapsed: float,
|
|
340
|
+
extra_stats: dict | None = None,
|
|
341
|
+
):
|
|
342
|
+
"""Print final token statistics table."""
|
|
343
|
+
reduction = round((1 - output_tokens / max(original_tokens, 1)) * 100)
|
|
344
|
+
|
|
345
|
+
console.print(Rule(title="[green][b]ANALYSIS[/b][/green]",style="#8c64dc"))
|
|
346
|
+
console.print()
|
|
347
|
+
|
|
348
|
+
# stats table
|
|
349
|
+
table = Table(
|
|
350
|
+
box=box.ROUNDED,
|
|
351
|
+
padding=(0, 2),
|
|
352
|
+
border_style="green",
|
|
353
|
+
)
|
|
354
|
+
table.add_column("Metric", style="dim", width=20)
|
|
355
|
+
table.add_column("Value", justify="right")
|
|
356
|
+
|
|
357
|
+
table.add_row(
|
|
358
|
+
"Original Tokens",
|
|
359
|
+
f"[red]{original_tokens:,}[/red]"
|
|
360
|
+
)
|
|
361
|
+
table.add_row(
|
|
362
|
+
"Crespo Output",
|
|
363
|
+
f"[green]{output_tokens:,}[/green]"
|
|
364
|
+
)
|
|
365
|
+
table.add_row(
|
|
366
|
+
"Token Reduction",
|
|
367
|
+
f"[bold purple]{reduction}%[/bold purple]"
|
|
368
|
+
)
|
|
369
|
+
table.add_row(
|
|
370
|
+
"Mode",
|
|
371
|
+
f"[cyan]{mode}[/cyan]"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if extra_stats:
|
|
375
|
+
for k, v in extra_stats.items():
|
|
376
|
+
table.add_row(k, str(v))
|
|
377
|
+
|
|
378
|
+
table.add_row("", "")
|
|
379
|
+
table.add_row(
|
|
380
|
+
"Output File",
|
|
381
|
+
f"[#8c64dc]{output_file}[/#8c64dc]"
|
|
382
|
+
)
|
|
383
|
+
table.add_row(
|
|
384
|
+
"Time Elapsed",
|
|
385
|
+
f"[dim]{elapsed:.1f}s[/dim]"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
console.print(Align(table, align="center", pad=True))
|
|
389
|
+
|
|
390
|
+
# reduction bar
|
|
391
|
+
console.print()
|
|
392
|
+
bar_width = 40
|
|
393
|
+
filled = max(1, int(bar_width * (1 - output_tokens / max(original_tokens, 1))))
|
|
394
|
+
empty = bar_width - filled
|
|
395
|
+
|
|
396
|
+
console.print(Align(
|
|
397
|
+
f" [dim]Original[/dim] "
|
|
398
|
+
f"[red]{'█' * bar_width}[/red] "
|
|
399
|
+
f"[cyan]{original_tokens:,}[/cyan]",align="center")
|
|
400
|
+
)
|
|
401
|
+
console.print("\n")
|
|
402
|
+
console.print(Align(
|
|
403
|
+
f" [dim]Crespo[/dim] "
|
|
404
|
+
f"[green]{'█' * empty}[/green][dim]{'░' * filled}[/dim] "
|
|
405
|
+
f"[green]{output_tokens:,}[/green]",align="center")
|
|
406
|
+
)
|
|
407
|
+
console.print()
|
|
408
|
+
|
|
409
|
+
# done
|
|
410
|
+
console.print(Align(
|
|
411
|
+
f"[green]Blueprint saved →[/green] "
|
|
412
|
+
f"[#8c64dc]{output_file}[/#8c64dc]","center")
|
|
413
|
+
)
|
|
414
|
+
console.print()
|
|
415
|
+
console.print(Rule(style="#8c64dc"))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ─── Error / warning helpers ──────────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
def print_error(message: str):
|
|
421
|
+
console.print(f"\n [red]✗[/red] [red]{message}[/red]\n")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def print_warning(message: str):
|
|
425
|
+
console.print(f" [yellow]⚠[/yellow] [yellow]{message}[/yellow]")
|
|
426
|
+
|
|
427
|
+
def print_loc(output_file):
|
|
428
|
+
console.print(Align(
|
|
429
|
+
f"[green]Blueprint saved →[/green] "
|
|
430
|
+
f"[#8c64dc]{output_file}[/#8c64dc]","center"))
|
|
431
|
+
console.print(Rule(style="#8c64dc"))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def print_info(message: str):
|
|
435
|
+
console.print(Align(f" [dim]{message}[/dim]","center"))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def print_groq_fallback():
|
|
439
|
+
"""Warn when Groq rate limit hit and falling back to structure."""
|
|
440
|
+
console.print()
|
|
441
|
+
console.print(
|
|
442
|
+
Panel(
|
|
443
|
+
"[yellow]Groq rate limit reached.[/yellow]\n"
|
|
444
|
+
"Remaining files will use [green]structure mode[/green] "
|
|
445
|
+
"(no summaries).\n"
|
|
446
|
+
"[dim]Files already summarised are preserved.[/dim]",
|
|
447
|
+
border_style="yellow",
|
|
448
|
+
padding=(0, 2),
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
console.print()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def print_no_groq_key():
|
|
455
|
+
"""Warn when no Groq key and falling back."""
|
|
456
|
+
console.print()
|
|
457
|
+
console.print(
|
|
458
|
+
Panel(
|
|
459
|
+
"[yellow]No Groq API key found.[/yellow]\n"
|
|
460
|
+
"Falling back to [green]structure mode[/green].\n\n"
|
|
461
|
+
"[dim]To use summary mode:[/dim]\n"
|
|
462
|
+
" [green]export CRESPO_GROQ_KEY=your_key_here[/green]\n"
|
|
463
|
+
" [dim]Get a free key at[/dim] [cyan]https://console.groq.com[/cyan]",
|
|
464
|
+
border_style="yellow",
|
|
465
|
+
padding=(0, 2),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
console.print()
|
|
469
|
+
|
|
470
|
+
@contextmanager
|
|
471
|
+
def summary_progress_context(total_files: int):
|
|
472
|
+
"""
|
|
473
|
+
Context manager that keeps the Groq spinner alive until the caller exits.
|
|
474
|
+
Yields an `advance(n)` callable so batch completions can tick the bar.
|
|
475
|
+
|
|
476
|
+
Usage:
|
|
477
|
+
with cli.summary_progress_context(len(files)) as advance:
|
|
478
|
+
for chunk in batches:
|
|
479
|
+
summaries = summariser.summarise_files_batch(chunk)
|
|
480
|
+
advance(len(chunk))
|
|
481
|
+
"""
|
|
482
|
+
console.print(" [purple]Calling Groq API...[/purple]")
|
|
483
|
+
console.print()
|
|
484
|
+
|
|
485
|
+
with Progress(
|
|
486
|
+
SpinnerColumn(style="purple"),
|
|
487
|
+
TextColumn(" [purple]{task.description}[/purple]"),
|
|
488
|
+
TaskProgressColumn(),
|
|
489
|
+
TextColumn("[dim]{task.fields[current]}[/dim]"),
|
|
490
|
+
console=console,
|
|
491
|
+
transient=True,
|
|
492
|
+
) as progress:
|
|
493
|
+
task = progress.add_task(
|
|
494
|
+
"Summarising",
|
|
495
|
+
total=total_files,
|
|
496
|
+
current=""
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
def advance(n: int, label: str = ""):
|
|
500
|
+
progress.update(task, advance=n, current=label)
|
|
501
|
+
|
|
502
|
+
yield advance # caller runs here; progress bar stays alive
|
|
503
|
+
|
|
504
|
+
console.print(f" [purple]✓[/purple] [dim]Summaries generated[/dim]")
|