pycodesage 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.
- codesage/__init__.py +4 -0
- codesage/__main__.py +6 -0
- codesage/cli/__init__.py +5 -0
- codesage/cli/main.py +461 -0
- codesage/core/__init__.py +6 -0
- codesage/core/indexer.py +229 -0
- codesage/core/suggester.py +182 -0
- codesage/llm/__init__.py +11 -0
- codesage/llm/embeddings.py +264 -0
- codesage/llm/prompts.py +71 -0
- codesage/llm/provider.py +257 -0
- codesage/models/__init__.py +6 -0
- codesage/models/code_element.py +106 -0
- codesage/models/suggestion.py +57 -0
- codesage/parsers/__init__.py +11 -0
- codesage/parsers/base.py +72 -0
- codesage/parsers/python_parser.py +220 -0
- codesage/parsers/registry.py +92 -0
- codesage/storage/__init__.py +6 -0
- codesage/storage/database.py +350 -0
- codesage/storage/vector_store.py +210 -0
- codesage/utils/__init__.py +5 -0
- codesage/utils/config.py +189 -0
- codesage/utils/health.py +247 -0
- codesage/utils/logging.py +227 -0
- codesage/utils/rate_limiter.py +129 -0
- codesage/utils/retry.py +204 -0
- pycodesage-0.1.0.dist-info/METADATA +242 -0
- pycodesage-0.1.0.dist-info/RECORD +33 -0
- pycodesage-0.1.0.dist-info/WHEEL +5 -0
- pycodesage-0.1.0.dist-info/entry_points.txt +2 -0
- pycodesage-0.1.0.dist-info/licenses/LICENSE +21 -0
- pycodesage-0.1.0.dist-info/top_level.txt +1 -0
codesage/__init__.py
ADDED
codesage/__main__.py
ADDED
codesage/cli/__init__.py
ADDED
codesage/cli/main.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""CodeSage CLI - Main entry point."""
|
|
2
|
+
|
|
3
|
+
# Suppress urllib3 SSL warning on macOS with LibreSSL
|
|
4
|
+
import warnings
|
|
5
|
+
warnings.filterwarnings("ignore", message="urllib3 v2 only supports OpenSSL")
|
|
6
|
+
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
import atexit
|
|
10
|
+
from typing import Callable, List
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from codesage import __version__
|
|
19
|
+
|
|
20
|
+
# Input validation constants
|
|
21
|
+
MAX_QUERY_LENGTH = 2000
|
|
22
|
+
MAX_PATH_LENGTH = 4096
|
|
23
|
+
|
|
24
|
+
# Graceful shutdown infrastructure
|
|
25
|
+
_cleanup_handlers: List[Callable[[], None]] = []
|
|
26
|
+
_shutdown_in_progress = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_cleanup(handler: Callable[[], None]) -> None:
|
|
30
|
+
"""Register a cleanup handler to be called on shutdown.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
handler: Function to call during cleanup
|
|
34
|
+
"""
|
|
35
|
+
if handler not in _cleanup_handlers:
|
|
36
|
+
_cleanup_handlers.append(handler)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def unregister_cleanup(handler: Callable[[], None]) -> None:
|
|
40
|
+
"""Unregister a cleanup handler.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
handler: Function to remove from cleanup list
|
|
44
|
+
"""
|
|
45
|
+
if handler in _cleanup_handlers:
|
|
46
|
+
_cleanup_handlers.remove(handler)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run_cleanup() -> None:
|
|
50
|
+
"""Run all registered cleanup handlers."""
|
|
51
|
+
for handler in reversed(_cleanup_handlers):
|
|
52
|
+
try:
|
|
53
|
+
handler()
|
|
54
|
+
except Exception as e:
|
|
55
|
+
# Don't let cleanup errors prevent other cleanups
|
|
56
|
+
console.print(f"[dim]Cleanup warning: {e}[/dim]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _shutdown_handler(signum: int, frame) -> None:
|
|
60
|
+
"""Handle shutdown signals gracefully.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
signum: Signal number received
|
|
64
|
+
frame: Current stack frame
|
|
65
|
+
"""
|
|
66
|
+
global _shutdown_in_progress
|
|
67
|
+
|
|
68
|
+
if _shutdown_in_progress:
|
|
69
|
+
# Force exit if already shutting down (user pressed Ctrl+C twice)
|
|
70
|
+
console.print("\n[red]Force shutdown...[/red]")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
_shutdown_in_progress = True
|
|
74
|
+
signal_name = signal.Signals(signum).name if hasattr(signal, 'Signals') else str(signum)
|
|
75
|
+
console.print(f"\n[yellow]⏳ Received {signal_name}, shutting down gracefully...[/yellow]")
|
|
76
|
+
|
|
77
|
+
_run_cleanup()
|
|
78
|
+
|
|
79
|
+
console.print("[green]✓ Shutdown complete[/green]")
|
|
80
|
+
sys.exit(0)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _setup_signal_handlers() -> None:
|
|
84
|
+
"""Set up signal handlers for graceful shutdown."""
|
|
85
|
+
# Handle SIGINT (Ctrl+C) and SIGTERM
|
|
86
|
+
signal.signal(signal.SIGINT, _shutdown_handler)
|
|
87
|
+
signal.signal(signal.SIGTERM, _shutdown_handler)
|
|
88
|
+
|
|
89
|
+
# Also register cleanup with atexit for normal exits
|
|
90
|
+
atexit.register(_run_cleanup)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Initialize signal handlers on module import
|
|
94
|
+
_setup_signal_handlers()
|
|
95
|
+
|
|
96
|
+
app = typer.Typer(
|
|
97
|
+
name="codesage",
|
|
98
|
+
help="Local-first code intelligence CLI with LangChain-powered RAG",
|
|
99
|
+
add_completion=False,
|
|
100
|
+
no_args_is_help=True,
|
|
101
|
+
)
|
|
102
|
+
console = Console()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def init(
|
|
107
|
+
path: str = typer.Argument(".", help="Project directory to initialize"),
|
|
108
|
+
model: str = typer.Option(
|
|
109
|
+
"qwen2.5-coder:7b",
|
|
110
|
+
"--model", "-m",
|
|
111
|
+
help="Ollama model to use for analysis"
|
|
112
|
+
),
|
|
113
|
+
embedding_model: str = typer.Option(
|
|
114
|
+
"mxbai-embed-large",
|
|
115
|
+
"--embedding-model", "-e",
|
|
116
|
+
help="Model to use for embeddings (mxbai-embed-large recommended for code)"
|
|
117
|
+
),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Initialize CodeSage in a project directory.
|
|
120
|
+
|
|
121
|
+
Creates .codesage/ directory with configuration.
|
|
122
|
+
"""
|
|
123
|
+
from codesage.utils.config import initialize_project
|
|
124
|
+
|
|
125
|
+
console.print(f"\n[bold blue]🚀 Initializing CodeSage[/bold blue]\n")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
project_path = Path(path).resolve()
|
|
129
|
+
config = initialize_project(project_path, model, embedding_model)
|
|
130
|
+
|
|
131
|
+
console.print(f"[green]✓[/green] Created .codesage directory")
|
|
132
|
+
console.print(f"[green]✓[/green] Configuration saved")
|
|
133
|
+
|
|
134
|
+
console.print(Panel(
|
|
135
|
+
f"""[bold]Project:[/bold] {config.project_name}
|
|
136
|
+
[bold]Model:[/bold] {config.llm.model}
|
|
137
|
+
[bold]Embedding:[/bold] {config.llm.embedding_model}
|
|
138
|
+
[bold]Language:[/bold] {config.language}""",
|
|
139
|
+
title="📋 Configuration",
|
|
140
|
+
border_style="blue",
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
console.print("\n[bold]Next steps:[/bold]")
|
|
144
|
+
console.print(" 1. Ensure Ollama is running: [cyan]ollama serve[/cyan]")
|
|
145
|
+
console.print(" 2. Pull required models:")
|
|
146
|
+
console.print(f" [cyan]ollama pull {model}[/cyan]")
|
|
147
|
+
console.print(f" [cyan]ollama pull {embedding_model}[/cyan]")
|
|
148
|
+
console.print(" 3. Index your codebase: [cyan]codesage index[/cyan]")
|
|
149
|
+
console.print(" 4. Search for code: [cyan]codesage suggest 'your query'[/cyan]\n")
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
console.print(f"[red]✗[/red] Error: {e}")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def index(
|
|
158
|
+
path: str = typer.Argument(".", help="Project directory to index"),
|
|
159
|
+
incremental: bool = typer.Option(
|
|
160
|
+
True,
|
|
161
|
+
"--incremental/--full",
|
|
162
|
+
help="Only index changed files (default) or full reindex"
|
|
163
|
+
),
|
|
164
|
+
clear: bool = typer.Option(
|
|
165
|
+
False,
|
|
166
|
+
"--clear",
|
|
167
|
+
help="Clear existing index before indexing"
|
|
168
|
+
),
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Index the codebase for semantic search.
|
|
171
|
+
|
|
172
|
+
Parses code files and generates embeddings.
|
|
173
|
+
"""
|
|
174
|
+
from codesage.utils.config import Config
|
|
175
|
+
from codesage.core.indexer import Indexer
|
|
176
|
+
|
|
177
|
+
project_path = Path(path).resolve()
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
config = Config.load(project_path)
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
console.print("[red]✗[/red] Project not initialized.")
|
|
183
|
+
console.print(" Run: [cyan]codesage init[/cyan]")
|
|
184
|
+
raise typer.Exit(1)
|
|
185
|
+
|
|
186
|
+
console.print(f"\n[bold cyan]📂 Indexing {config.project_name}[/bold cyan]\n")
|
|
187
|
+
|
|
188
|
+
if not incremental:
|
|
189
|
+
console.print("[dim]Running full reindex...[/dim]")
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
indexer = Indexer(config)
|
|
193
|
+
|
|
194
|
+
# Register cleanup handler for graceful shutdown
|
|
195
|
+
def _cleanup_indexer():
|
|
196
|
+
try:
|
|
197
|
+
indexer.db.close()
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
register_cleanup(_cleanup_indexer)
|
|
202
|
+
|
|
203
|
+
if clear:
|
|
204
|
+
console.print("[yellow]Clearing existing index...[/yellow]")
|
|
205
|
+
indexer.clear_index()
|
|
206
|
+
|
|
207
|
+
stats = indexer.index_repository(incremental=incremental)
|
|
208
|
+
|
|
209
|
+
# Unregister cleanup since we completed successfully
|
|
210
|
+
unregister_cleanup(_cleanup_indexer)
|
|
211
|
+
|
|
212
|
+
console.print()
|
|
213
|
+
console.print(Panel(
|
|
214
|
+
f"""[bold]Files scanned:[/bold] {stats['files_scanned']}
|
|
215
|
+
[bold]Files indexed:[/bold] {stats['files_indexed']}
|
|
216
|
+
[bold]Files skipped:[/bold] {stats['files_skipped']} (unchanged)
|
|
217
|
+
[bold]Code elements:[/bold] {stats['elements_found']}
|
|
218
|
+
[bold]Errors:[/bold] {stats['errors']}""",
|
|
219
|
+
title="📊 Indexing Complete",
|
|
220
|
+
border_style="green",
|
|
221
|
+
))
|
|
222
|
+
|
|
223
|
+
if stats['elements_found'] > 0:
|
|
224
|
+
console.print("\n[green]✓[/green] Ready for suggestions!")
|
|
225
|
+
console.print(" Try: [cyan]codesage suggest 'your query'[/cyan]\n")
|
|
226
|
+
else:
|
|
227
|
+
console.print("\n[yellow]⚠[/yellow] No code elements found.")
|
|
228
|
+
console.print(" Check that you have Python files in your project.\n")
|
|
229
|
+
|
|
230
|
+
except KeyboardInterrupt:
|
|
231
|
+
console.print("\n[yellow]⚠[/yellow] Indexing interrupted by user.")
|
|
232
|
+
raise typer.Exit(130) # Standard exit code for SIGINT
|
|
233
|
+
except Exception as e:
|
|
234
|
+
console.print(f"[red]✗[/red] Error: {e}")
|
|
235
|
+
raise typer.Exit(1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command()
|
|
239
|
+
def suggest(
|
|
240
|
+
query: str = typer.Argument(..., help="What are you looking for?"),
|
|
241
|
+
path: str = typer.Option(".", "--path", "-p", help="Project directory"),
|
|
242
|
+
limit: int = typer.Option(5, "--limit", "-n", help="Number of suggestions"),
|
|
243
|
+
min_similarity: float = typer.Option(
|
|
244
|
+
0.2,
|
|
245
|
+
"--min-similarity", "-s",
|
|
246
|
+
help="Minimum similarity threshold (0-1)"
|
|
247
|
+
),
|
|
248
|
+
no_explain: bool = typer.Option(
|
|
249
|
+
False,
|
|
250
|
+
"--no-explain",
|
|
251
|
+
help="Skip LLM explanations (faster)"
|
|
252
|
+
),
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Get code suggestions based on natural language query.
|
|
255
|
+
|
|
256
|
+
Uses semantic search to find relevant code.
|
|
257
|
+
"""
|
|
258
|
+
from codesage.utils.config import Config
|
|
259
|
+
from codesage.core.suggester import Suggester
|
|
260
|
+
|
|
261
|
+
# Validate query
|
|
262
|
+
query = query.strip()
|
|
263
|
+
if not query:
|
|
264
|
+
console.print("[red]✗[/red] Query cannot be empty")
|
|
265
|
+
raise typer.Exit(1)
|
|
266
|
+
if len(query) > MAX_QUERY_LENGTH:
|
|
267
|
+
console.print(f"[red]✗[/red] Query too long (max {MAX_QUERY_LENGTH} chars)")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
project_path = Path(path).resolve()
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
config = Config.load(project_path)
|
|
274
|
+
except FileNotFoundError:
|
|
275
|
+
console.print("[red]✗[/red] Project not initialized.")
|
|
276
|
+
console.print(" Run: [cyan]codesage init[/cyan]")
|
|
277
|
+
raise typer.Exit(1)
|
|
278
|
+
|
|
279
|
+
console.print(f"\n[dim]Searching for:[/dim] {query}\n")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
suggester = Suggester(config)
|
|
283
|
+
suggestions = suggester.find_similar(
|
|
284
|
+
query=query,
|
|
285
|
+
limit=limit,
|
|
286
|
+
min_similarity=min_similarity,
|
|
287
|
+
include_explanations=not no_explain,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if not suggestions:
|
|
291
|
+
console.print("[yellow]No suggestions found.[/yellow]")
|
|
292
|
+
console.print("\n[dim]Tips:[/dim]")
|
|
293
|
+
console.print(" • Try different search terms")
|
|
294
|
+
console.print(" • Lower --min-similarity threshold")
|
|
295
|
+
console.print(" • Run [cyan]codesage index[/cyan] to update the index\n")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
for i, suggestion in enumerate(suggestions, 1):
|
|
299
|
+
# Header
|
|
300
|
+
console.print(
|
|
301
|
+
f"[bold blue]{i}. {suggestion.file}:{suggestion.line}[/bold blue]"
|
|
302
|
+
)
|
|
303
|
+
console.print(
|
|
304
|
+
f"[dim]Similarity: {suggestion.similarity:.0%} | "
|
|
305
|
+
f"Type: {suggestion.element_type}"
|
|
306
|
+
f"{' | ' + suggestion.name if suggestion.name else ''}[/dim]"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Code with syntax highlighting
|
|
310
|
+
syntax = Syntax(
|
|
311
|
+
suggestion.code,
|
|
312
|
+
suggestion.language,
|
|
313
|
+
theme="monokai",
|
|
314
|
+
line_numbers=True,
|
|
315
|
+
start_line=suggestion.line,
|
|
316
|
+
word_wrap=True,
|
|
317
|
+
)
|
|
318
|
+
console.print(syntax)
|
|
319
|
+
|
|
320
|
+
# Explanation
|
|
321
|
+
if suggestion.explanation:
|
|
322
|
+
console.print(f"[italic]💡 {suggestion.explanation}[/italic]")
|
|
323
|
+
|
|
324
|
+
console.print()
|
|
325
|
+
|
|
326
|
+
console.print(f"[dim]Found {len(suggestions)} suggestion(s)[/dim]\n")
|
|
327
|
+
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
console.print("\n[yellow]⚠[/yellow] Search interrupted by user.")
|
|
330
|
+
raise typer.Exit(130)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
console.print(f"[red]✗[/red] Error: {e}")
|
|
333
|
+
raise typer.Exit(1)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.command()
|
|
337
|
+
def stats(
|
|
338
|
+
path: str = typer.Argument(".", help="Project directory"),
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Show index statistics."""
|
|
341
|
+
from codesage.utils.config import Config
|
|
342
|
+
from codesage.storage.database import Database
|
|
343
|
+
|
|
344
|
+
project_path = Path(path).resolve()
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
config = Config.load(project_path)
|
|
348
|
+
except FileNotFoundError:
|
|
349
|
+
console.print("[red]✗[/red] Project not initialized.")
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
|
|
352
|
+
db = Database(config.storage.db_path)
|
|
353
|
+
stats = db.get_stats()
|
|
354
|
+
|
|
355
|
+
console.print(Panel(
|
|
356
|
+
f"""[bold]Project:[/bold] {config.project_name}
|
|
357
|
+
[bold]Files indexed:[/bold] {stats['files']}
|
|
358
|
+
[bold]Code elements:[/bold] {stats['elements']}
|
|
359
|
+
[bold]Last indexed:[/bold] {stats['last_indexed'] or 'Never'}
|
|
360
|
+
[bold]Model:[/bold] {config.llm.model}
|
|
361
|
+
[bold]Embedding:[/bold] {config.llm.embedding_model}""",
|
|
362
|
+
title="📊 CodeSage Statistics",
|
|
363
|
+
border_style="blue",
|
|
364
|
+
))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@app.command()
|
|
368
|
+
def health(
|
|
369
|
+
path: str = typer.Argument(".", help="Project directory"),
|
|
370
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Check system health and dependencies.
|
|
373
|
+
|
|
374
|
+
Verifies Ollama, database, and vector store are working.
|
|
375
|
+
"""
|
|
376
|
+
from codesage.utils.config import Config
|
|
377
|
+
from codesage.utils.health import check_system_health
|
|
378
|
+
import json
|
|
379
|
+
|
|
380
|
+
project_path = Path(path).resolve()
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
config = Config.load(project_path)
|
|
384
|
+
except FileNotFoundError:
|
|
385
|
+
if json_output:
|
|
386
|
+
console.print(json.dumps({
|
|
387
|
+
"healthy": False,
|
|
388
|
+
"error": "Project not initialized"
|
|
389
|
+
}))
|
|
390
|
+
else:
|
|
391
|
+
console.print("[red]✗[/red] Project not initialized.")
|
|
392
|
+
console.print(" Run: [cyan]codesage init[/cyan]")
|
|
393
|
+
raise typer.Exit(1)
|
|
394
|
+
|
|
395
|
+
status = check_system_health(config)
|
|
396
|
+
|
|
397
|
+
if json_output:
|
|
398
|
+
console.print(json.dumps(status.to_dict(), indent=2))
|
|
399
|
+
else:
|
|
400
|
+
console.print("\n[bold]System Health Check[/bold]\n")
|
|
401
|
+
|
|
402
|
+
# Ollama status
|
|
403
|
+
if status.ollama_available:
|
|
404
|
+
latency = f" ({status.ollama_latency_ms:.0f}ms)" if status.ollama_latency_ms else ""
|
|
405
|
+
console.print(f"[green]✓[/green] Ollama{latency}")
|
|
406
|
+
else:
|
|
407
|
+
console.print("[red]✗[/red] Ollama")
|
|
408
|
+
|
|
409
|
+
# Database status
|
|
410
|
+
if status.database_accessible:
|
|
411
|
+
size = f" ({status.database_size_mb:.1f}MB)" if status.database_size_mb else ""
|
|
412
|
+
console.print(f"[green]✓[/green] Database{size}")
|
|
413
|
+
else:
|
|
414
|
+
console.print("[red]✗[/red] Database")
|
|
415
|
+
|
|
416
|
+
# Vector store status
|
|
417
|
+
if status.vector_store_accessible:
|
|
418
|
+
count = f" ({status.vector_count} vectors)" if status.vector_count else ""
|
|
419
|
+
console.print(f"[green]✓[/green] Vector Store{count}")
|
|
420
|
+
else:
|
|
421
|
+
console.print("[red]✗[/red] Vector Store")
|
|
422
|
+
|
|
423
|
+
# Disk space
|
|
424
|
+
if status.disk_space_ok:
|
|
425
|
+
console.print("[green]✓[/green] Disk Space")
|
|
426
|
+
else:
|
|
427
|
+
console.print("[yellow]⚠[/yellow] Disk Space")
|
|
428
|
+
|
|
429
|
+
# Errors and warnings
|
|
430
|
+
if status.errors:
|
|
431
|
+
console.print("\n[red]Errors:[/red]")
|
|
432
|
+
for error in status.errors:
|
|
433
|
+
console.print(f" • {error}")
|
|
434
|
+
|
|
435
|
+
if status.warnings:
|
|
436
|
+
console.print("\n[yellow]Warnings:[/yellow]")
|
|
437
|
+
for warning in status.warnings:
|
|
438
|
+
console.print(f" • {warning}")
|
|
439
|
+
|
|
440
|
+
# Summary
|
|
441
|
+
console.print()
|
|
442
|
+
if status.is_healthy:
|
|
443
|
+
console.print("[green]✓ System is healthy[/green]")
|
|
444
|
+
else:
|
|
445
|
+
console.print("[red]✗ System has issues[/red]")
|
|
446
|
+
raise typer.Exit(1)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@app.command()
|
|
450
|
+
def version() -> None:
|
|
451
|
+
"""Show CodeSage version."""
|
|
452
|
+
console.print(f"[bold]CodeSage[/bold] version {__version__}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def main() -> None:
|
|
456
|
+
"""Main entry point."""
|
|
457
|
+
app()
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
if __name__ == "__main__":
|
|
461
|
+
main()
|