vibeguard-cli 1.0.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.
Files changed (81) hide show
  1. vibeguard/__init__.py +3 -0
  2. vibeguard/cli/__init__.py +1 -0
  3. vibeguard/cli/apply.py +413 -0
  4. vibeguard/cli/auth_cmd.py +318 -0
  5. vibeguard/cli/baseline_cmd.py +286 -0
  6. vibeguard/cli/config_cmd.py +252 -0
  7. vibeguard/cli/display.py +356 -0
  8. vibeguard/cli/doctor.py +228 -0
  9. vibeguard/cli/fix.py +977 -0
  10. vibeguard/cli/import_cmd.py +180 -0
  11. vibeguard/cli/init_cmd.py +113 -0
  12. vibeguard/cli/keys.py +193 -0
  13. vibeguard/cli/live_cmd.py +564 -0
  14. vibeguard/cli/main.py +667 -0
  15. vibeguard/cli/patch.py +805 -0
  16. vibeguard/cli/report.py +106 -0
  17. vibeguard/cli/scan.py +1227 -0
  18. vibeguard/core/__init__.py +1 -0
  19. vibeguard/core/auth.py +402 -0
  20. vibeguard/core/baseline.py +212 -0
  21. vibeguard/core/bootstrap.py +303 -0
  22. vibeguard/core/cache.py +77 -0
  23. vibeguard/core/config.py +99 -0
  24. vibeguard/core/dedup.py +168 -0
  25. vibeguard/core/downloader.py +222 -0
  26. vibeguard/core/example_detector.py +159 -0
  27. vibeguard/core/exit_codes.py +19 -0
  28. vibeguard/core/ignore.py +243 -0
  29. vibeguard/core/keyring.py +188 -0
  30. vibeguard/core/license.py +166 -0
  31. vibeguard/core/llm.py +206 -0
  32. vibeguard/core/path_classifier.py +152 -0
  33. vibeguard/core/repo_detector.py +143 -0
  34. vibeguard/core/sarif_import.py +342 -0
  35. vibeguard/core/triage.py +205 -0
  36. vibeguard/core/url_validator.py +259 -0
  37. vibeguard/core/validate.py +174 -0
  38. vibeguard/models/__init__.py +24 -0
  39. vibeguard/models/auth.py +92 -0
  40. vibeguard/models/baseline.py +105 -0
  41. vibeguard/models/finding.py +78 -0
  42. vibeguard/models/patch.py +164 -0
  43. vibeguard/models/scan_result.py +190 -0
  44. vibeguard/models/triage.py +53 -0
  45. vibeguard/reporters/__init__.py +7 -0
  46. vibeguard/reporters/badge.py +103 -0
  47. vibeguard/reporters/html.py +920 -0
  48. vibeguard/reporters/sarif.py +175 -0
  49. vibeguard/scanners/__init__.py +130 -0
  50. vibeguard/scanners/manifests/bandit.toml +39 -0
  51. vibeguard/scanners/manifests/cargo_audit.toml +31 -0
  52. vibeguard/scanners/manifests/checkov.toml +37 -0
  53. vibeguard/scanners/manifests/dockle.toml +46 -0
  54. vibeguard/scanners/manifests/gitleaks.toml +48 -0
  55. vibeguard/scanners/manifests/npm_audit.toml +31 -0
  56. vibeguard/scanners/manifests/nuclei.toml +58 -0
  57. vibeguard/scanners/manifests/pip_audit.toml +36 -0
  58. vibeguard/scanners/manifests/semgrep.toml +43 -0
  59. vibeguard/scanners/manifests/trivy.toml +50 -0
  60. vibeguard/scanners/manifests/trufflehog.toml +48 -0
  61. vibeguard/scanners/parsers/__init__.py +1 -0
  62. vibeguard/scanners/parsers/bandit.py +94 -0
  63. vibeguard/scanners/parsers/cargo_audit.py +185 -0
  64. vibeguard/scanners/parsers/checkov.py +179 -0
  65. vibeguard/scanners/parsers/dockle.py +185 -0
  66. vibeguard/scanners/parsers/gitleaks.py +95 -0
  67. vibeguard/scanners/parsers/npm_audit.py +219 -0
  68. vibeguard/scanners/parsers/nuclei.py +247 -0
  69. vibeguard/scanners/parsers/pip_audit.py +166 -0
  70. vibeguard/scanners/parsers/semgrep.py +110 -0
  71. vibeguard/scanners/parsers/trivy.py +86 -0
  72. vibeguard/scanners/parsers/trufflehog.py +150 -0
  73. vibeguard/scanners/runners/__init__.py +7 -0
  74. vibeguard/scanners/runners/base.py +41 -0
  75. vibeguard/scanners/runners/docker.py +86 -0
  76. vibeguard/scanners/runners/local.py +144 -0
  77. vibeguard_cli-1.0.0.dist-info/METADATA +223 -0
  78. vibeguard_cli-1.0.0.dist-info/RECORD +81 -0
  79. vibeguard_cli-1.0.0.dist-info/WHEEL +4 -0
  80. vibeguard_cli-1.0.0.dist-info/entry_points.txt +2 -0
  81. vibeguard_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,318 @@
1
+ """Auth command - manage Pro license authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ from datetime import UTC, datetime
8
+
9
+ import typer
10
+ from rich.live import Live
11
+ from rich.panel import Panel
12
+ from rich.spinner import Spinner
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+
16
+ from vibeguard.cli.display import BRAND_COLOR, VIBEGUARD_SPINNER_NAME, get_console
17
+ from vibeguard.core.auth import (
18
+ AuthError,
19
+ LicenseError,
20
+ NetworkError,
21
+ activate_license,
22
+ clear_auth_cache,
23
+ get_cached_token,
24
+ get_or_create_machine_id,
25
+ get_token_time_remaining,
26
+ mask_license_key,
27
+ save_token_to_cache,
28
+ should_refresh_token,
29
+ )
30
+ from vibeguard.core.exit_codes import ExitCode
31
+ from vibeguard.core.keyring import get_configured_providers
32
+
33
+ app = typer.Typer(
34
+ name="auth",
35
+ help="Manage Pro license authentication.",
36
+ invoke_without_command=True,
37
+ no_args_is_help=True,
38
+ )
39
+
40
+ console = get_console()
41
+
42
+
43
+ @app.callback(invoke_without_command=True)
44
+ def auth_callback(ctx: typer.Context) -> None:
45
+ """Manage Pro license authentication.
46
+
47
+ Activate your Pro license to unlock premium features like
48
+ automated patch generation and application.
49
+
50
+ Examples:
51
+ vibeguard auth login VGPRO-XXXX-XXXX-XXXX # Activate license
52
+ vibeguard auth status # Check license status
53
+ vibeguard auth logout # Deactivate this machine
54
+ """
55
+ if ctx.invoked_subcommand is None:
56
+ console.print(ctx.get_help())
57
+
58
+
59
+ def _activate_with_spinner(license_key: str) -> tuple[bool, str, object | None]:
60
+ """Run activation in background thread with spinner.
61
+
62
+ Returns:
63
+ Tuple of (success, message, response_or_error)
64
+ """
65
+ result: tuple[bool, str, object | None] = (False, "Activation failed", None)
66
+ done = threading.Event()
67
+
68
+ def do_activate() -> None:
69
+ nonlocal result
70
+ try:
71
+ response = asyncio.run(activate_license(license_key))
72
+ result = (True, "License activated successfully!", response)
73
+ except LicenseError as e:
74
+ result = (False, str(e), e)
75
+ except NetworkError as e:
76
+ result = (False, str(e), e)
77
+ except Exception as e:
78
+ result = (False, f"Unexpected error: {e}", e)
79
+ finally:
80
+ done.set()
81
+
82
+ thread = threading.Thread(target=do_activate, daemon=True)
83
+ thread.start()
84
+
85
+ spinner = Spinner(VIBEGUARD_SPINNER_NAME, text=Text(" Activating license...", style=BRAND_COLOR))
86
+ with Live(spinner, console=console, transient=True):
87
+ done.wait(timeout=45)
88
+
89
+ if not done.is_set():
90
+ return (False, "Activation timed out. Please try again.", None)
91
+
92
+ return result
93
+
94
+
95
+ @app.command("login")
96
+ def login(
97
+ license_key: str | None = typer.Argument(
98
+ None,
99
+ help="Your VibeGuard Pro license key (e.g., VGPRO-XXXX-XXXX-XXXX)",
100
+ ),
101
+ ) -> None:
102
+ """Activate Pro license for this machine.
103
+
104
+ Your license key can be found in your account at https://vibeguard.co/account
105
+
106
+ Example:
107
+ vibeguard auth login VGPRO-XXXX-XXXX-XXXX
108
+ """
109
+ # Handle missing argument
110
+ if license_key is None:
111
+ console.print("[red]Error:[/red] Missing license key.\n")
112
+ console.print("[bold]Usage:[/bold] vibeguard auth login <license-key>\n")
113
+ console.print("[bold]Example:[/bold] vibeguard auth login VGPRO-XXXX-XXXX-XXXX\n")
114
+ console.print("Get your license key at: [link=https://vibeguard.co/pricing]https://vibeguard.co/pricing[/link]")
115
+ raise typer.Exit(ExitCode.CONFIG_ERROR)
116
+
117
+ # Check if already logged in
118
+ existing_token = get_cached_token()
119
+ if existing_token is not None:
120
+ console.print("[yellow]You are already logged in.[/yellow]")
121
+ console.print(f"Current plan: [bold]{existing_token.plan or 'Pro'}[/bold]")
122
+ remaining = get_token_time_remaining(existing_token)
123
+ if remaining.total_seconds() > 0:
124
+ days = remaining.days
125
+ hours = remaining.seconds // 3600
126
+ console.print(f"Token expires in: {days} days, {hours} hours")
127
+ console.print("\nTo switch accounts, run [bold]vibeguard auth logout[/bold] first.")
128
+ raise typer.Exit(ExitCode.SUCCESS)
129
+
130
+ # Activate
131
+ console.print(f"Activating license: [dim]{mask_license_key(license_key)}[/dim]\n")
132
+
133
+ success, message, response = _activate_with_spinner(license_key)
134
+
135
+ if success and response is not None:
136
+ # Save token to cache
137
+ token = save_token_to_cache(response)
138
+
139
+ console.print(f"[green]{message}[/green]\n")
140
+
141
+ # Show details
142
+ table = Table(show_header=False, box=None)
143
+ table.add_column("Field", style="dim")
144
+ table.add_column("Value")
145
+
146
+ table.add_row("Plan", token.plan or "Pro")
147
+ table.add_row("Machine ID", get_or_create_machine_id()[:8] + "...")
148
+
149
+ remaining = get_token_time_remaining(token)
150
+ days = remaining.days
151
+ hours = remaining.seconds // 3600
152
+ table.add_row("Token expires", f"in {days} days, {hours} hours")
153
+
154
+ if token.entitlements:
155
+ table.add_row("Entitlements", ", ".join(token.entitlements))
156
+
157
+ console.print(table)
158
+ console.print()
159
+
160
+ # Check if LLM key is configured
161
+ providers = get_configured_providers()
162
+ if not providers:
163
+ console.print(
164
+ Panel(
165
+ "[yellow]Pro license activated![/yellow]\n\n"
166
+ "To use patch generation, configure an LLM API key:\n"
167
+ " [bold]vibeguard keys set openai <your-api-key>[/bold]\n"
168
+ " [bold]vibeguard keys set anthropic <your-api-key>[/bold]",
169
+ title="Next Step",
170
+ border_style="yellow",
171
+ )
172
+ )
173
+ else:
174
+ console.print("[green]You're all set![/green] Try [bold]vibeguard patch[/bold] to generate fixes.")
175
+
176
+ else:
177
+ console.print(f"[red]Activation failed:[/red] {message}")
178
+
179
+ # Provide helpful suggestions
180
+ if isinstance(response, NetworkError):
181
+ console.print("\n[dim]Tips:[/dim]")
182
+ console.print(" - Check your internet connection")
183
+ console.print(" - Try again in a few moments")
184
+ console.print(" - Contact support@vibeguard.co if the issue persists")
185
+ elif isinstance(response, LicenseError):
186
+ console.print("\n[dim]Tips:[/dim]")
187
+ console.print(" - Verify your license key is correct")
188
+ console.print(" - Check your account at https://vibeguard.co/account")
189
+ console.print(" - Contact support@vibeguard.co for help")
190
+
191
+ raise typer.Exit(ExitCode.CONFIG_ERROR)
192
+
193
+
194
+ @app.command("status")
195
+ def status() -> None:
196
+ """Show current license status.
197
+
198
+ Displays:
199
+ - License activation status
200
+ - Token expiry
201
+ - Current plan
202
+ - Available entitlements
203
+ - Configured LLM providers
204
+
205
+ Example:
206
+ vibeguard auth status
207
+ """
208
+ token = get_cached_token()
209
+ providers = get_configured_providers()
210
+ machine_id = get_or_create_machine_id()
211
+
212
+ # Build status table
213
+ table = Table(title="VibeGuard License Status", show_header=False)
214
+ table.add_column("Field", style="cyan")
215
+ table.add_column("Value")
216
+
217
+ if token is not None:
218
+ table.add_row("Status", "[green]Licensed (Pro)[/green]")
219
+ table.add_row("Plan", token.plan or "Pro")
220
+
221
+ # Token expiry
222
+ remaining = get_token_time_remaining(token)
223
+ if remaining.total_seconds() > 0:
224
+ days = remaining.days
225
+ hours = remaining.seconds // 3600
226
+ if days > 1:
227
+ expiry_str = f"in {days} days, {hours} hours"
228
+ elif days == 1:
229
+ expiry_str = f"in 1 day, {hours} hours"
230
+ else:
231
+ expiry_str = f"in {hours} hours"
232
+
233
+ if should_refresh_token(token):
234
+ expiry_str += " [yellow](refresh pending)[/yellow]"
235
+
236
+ table.add_row("Token expires", expiry_str)
237
+ else:
238
+ table.add_row("Token expires", "[red]Expired[/red]")
239
+
240
+ # Entitlements
241
+ if token.entitlements:
242
+ table.add_row("Entitlements", ", ".join(token.entitlements))
243
+ else:
244
+ table.add_row("Entitlements", "[dim]None[/dim]")
245
+
246
+ # Last refresh
247
+ if token.last_refresh:
248
+ refresh_str = token.last_refresh.strftime("%Y-%m-%d %H:%M UTC")
249
+ table.add_row("Last refresh", refresh_str)
250
+
251
+ else:
252
+ table.add_row("Status", "[yellow]Not licensed (Free tier)[/yellow]")
253
+ table.add_row("Plan", "Free")
254
+
255
+ # Machine ID
256
+ table.add_row("Machine ID", machine_id[:12] + "...")
257
+
258
+ # LLM providers
259
+ if providers:
260
+ table.add_row("LLM providers", ", ".join(providers))
261
+ else:
262
+ table.add_row("LLM providers", "[dim]None configured[/dim]")
263
+
264
+ console.print(table)
265
+ console.print()
266
+
267
+ # Actionable guidance
268
+ if token is None:
269
+ console.print(
270
+ Panel(
271
+ "Activate your Pro license to unlock patch generation:\n"
272
+ " [bold]vibeguard auth login <your-license-key>[/bold]\n\n"
273
+ "Get a license at: [link=https://vibeguard.co/pricing]https://vibeguard.co/pricing[/link]",
274
+ title="Upgrade to Pro",
275
+ border_style="yellow",
276
+ )
277
+ )
278
+ elif not providers:
279
+ console.print(
280
+ Panel(
281
+ "Configure an LLM API key to use patch generation:\n"
282
+ " [bold]vibeguard keys set openai <your-api-key>[/bold]\n"
283
+ " [bold]vibeguard keys set anthropic <your-api-key>[/bold]",
284
+ title="Configure LLM",
285
+ border_style="yellow",
286
+ )
287
+ )
288
+ else:
289
+ console.print("[green]Ready to use Pro features![/green] Try [bold]vibeguard patch[/bold]")
290
+
291
+
292
+ @app.command("logout")
293
+ def logout() -> None:
294
+ """Deactivate license and clear cached token.
295
+
296
+ Your license key remains valid and can be activated
297
+ on another machine.
298
+
299
+ Example:
300
+ vibeguard auth logout
301
+ """
302
+ token = get_cached_token()
303
+
304
+ if token is None:
305
+ console.print("[yellow]You are not currently logged in.[/yellow]")
306
+ raise typer.Exit(ExitCode.SUCCESS)
307
+
308
+ # Confirm logout
309
+ console.print(f"Current plan: [bold]{token.plan or 'Pro'}[/bold]")
310
+ console.print()
311
+
312
+ if clear_auth_cache():
313
+ console.print("[green]Logged out successfully.[/green]")
314
+ console.print()
315
+ console.print("[dim]Your license key is still valid.[/dim]")
316
+ console.print("[dim]You can reactivate on this or another machine.[/dim]")
317
+ else:
318
+ console.print("[yellow]No active session to clear.[/yellow]")
@@ -0,0 +1,286 @@
1
+ """Baseline command - manage security baselines for regression checking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.table import Table
9
+
10
+ from vibeguard.cli.display import get_console
11
+ from vibeguard.core import cache
12
+ from vibeguard.core.baseline import (
13
+ delete_baseline,
14
+ list_baselines,
15
+ load_baseline,
16
+ save_baseline,
17
+ )
18
+ from vibeguard.core.exit_codes import ExitCode
19
+
20
+ app = typer.Typer(
21
+ name="baseline",
22
+ help="Manage security baselines for regression checking.",
23
+ invoke_without_command=True,
24
+ no_args_is_help=True,
25
+ )
26
+
27
+ console = get_console()
28
+
29
+
30
+ @app.callback(invoke_without_command=True)
31
+ def baseline_callback(ctx: typer.Context) -> None:
32
+ """Manage security baselines for regression checking.
33
+
34
+ Baselines capture your security findings at a point in time,
35
+ allowing you to detect regressions (new issues) in future scans.
36
+
37
+ Examples:
38
+ vibeguard baseline save # Save as "default"
39
+ vibeguard baseline save release-1.0 # Save with custom name
40
+ vibeguard baseline list # List all baselines
41
+ vibeguard baseline show default # Show baseline details
42
+ vibeguard baseline delete old-baseline # Delete a baseline
43
+
44
+ To scan against a baseline:
45
+ vibeguard scan . --baseline default
46
+ """
47
+ if ctx.invoked_subcommand is None:
48
+ # Show help when no subcommand given
49
+ console.print(ctx.get_help())
50
+
51
+
52
+ @app.command("save")
53
+ def save_cmd(
54
+ name: str = typer.Argument(
55
+ "default",
56
+ help="Name for the baseline (default: 'default')",
57
+ ),
58
+ path: Path = typer.Option(
59
+ Path("."),
60
+ "--path",
61
+ "-p",
62
+ help="Repository path (default: current directory)",
63
+ ),
64
+ force: bool = typer.Option(
65
+ False,
66
+ "--force",
67
+ "-f",
68
+ help="Overwrite existing baseline without confirmation",
69
+ ),
70
+ ) -> None:
71
+ """Save current scan as a baseline.
72
+
73
+ Saves the most recent scan results as a named baseline.
74
+ Future scans can compare against this baseline to detect regressions.
75
+
76
+ Examples:
77
+ vibeguard baseline save
78
+ vibeguard baseline save release-1.0
79
+ vibeguard baseline save --path /path/to/repo
80
+ vibeguard baseline save release-1.0 --force
81
+ """
82
+ target = path.resolve()
83
+
84
+ # Load latest scan
85
+ result = cache.load_latest_scan(target)
86
+ if result is None:
87
+ console.print("[red]Error:[/red] No cached scan found.")
88
+ console.print("[dim]Run 'vibeguard scan .' first.[/dim]")
89
+ raise typer.Exit(ExitCode.NO_CACHE)
90
+
91
+ # Check if baseline exists
92
+ existing = load_baseline(target, name)
93
+ if existing and not force:
94
+ console.print(f"[yellow]Warning:[/yellow] Baseline '{name}' already exists.")
95
+ console.print(f" Created: {existing.created_at.strftime('%Y-%m-%d %H:%M')}")
96
+ console.print(f" Findings: {existing.actionable_count}")
97
+ overwrite = typer.confirm("Overwrite?", default=False)
98
+ if not overwrite:
99
+ console.print("[dim]Cancelled.[/dim]")
100
+ raise typer.Exit(ExitCode.SUCCESS)
101
+
102
+ # Save baseline
103
+ baseline_path = save_baseline(result, target, name)
104
+
105
+ actionable_count = len(result.actionable_findings)
106
+ console.print(f"[green]Baseline saved:[/green] {name}")
107
+ console.print(f" Findings: {actionable_count} actionable")
108
+ console.print(f" Scanners: {', '.join(result.scanners_run)}")
109
+ console.print(f" Path: {baseline_path}")
110
+ console.print()
111
+ console.print("[dim]Compare future scans with:[/dim]")
112
+ console.print(f"[dim] vibeguard scan . --baseline {name}[/dim]")
113
+
114
+
115
+ @app.command("list")
116
+ def list_cmd(
117
+ path: Path = typer.Option(
118
+ Path("."),
119
+ "--path",
120
+ "-p",
121
+ help="Repository path",
122
+ ),
123
+ ) -> None:
124
+ """List all saved baselines.
125
+
126
+ Example:
127
+ vibeguard baseline list
128
+ """
129
+ target = path.resolve()
130
+ baselines = list_baselines(target)
131
+
132
+ if not baselines:
133
+ console.print("[yellow]No baselines found.[/yellow]")
134
+ console.print("[dim]Create one with: vibeguard baseline save[/dim]")
135
+ return
136
+
137
+ table = Table(title="Saved Baselines")
138
+ table.add_column("Name", style="cyan")
139
+ table.add_column("Created", style="dim")
140
+ table.add_column("Findings", justify="right")
141
+ table.add_column("Scanners")
142
+
143
+ for baseline in baselines:
144
+ created = baseline.created_at.strftime("%Y-%m-%d %H:%M")
145
+ scanners = ", ".join(baseline.scanners_used[:3])
146
+ if len(baseline.scanners_used) > 3:
147
+ scanners += "..."
148
+
149
+ table.add_row(
150
+ baseline.name,
151
+ created,
152
+ str(baseline.actionable_count),
153
+ scanners,
154
+ )
155
+
156
+ console.print(table)
157
+ console.print()
158
+ console.print("[dim]Compare scans with: vibeguard scan . --baseline <name>[/dim]")
159
+
160
+
161
+ @app.command("show")
162
+ def show_cmd(
163
+ name: str = typer.Argument(
164
+ ...,
165
+ help="Baseline name to show",
166
+ ),
167
+ path: Path = typer.Option(
168
+ Path("."),
169
+ "--path",
170
+ "-p",
171
+ help="Repository path",
172
+ ),
173
+ ) -> None:
174
+ """Show details of a baseline.
175
+
176
+ Example:
177
+ vibeguard baseline show default
178
+ vibeguard baseline show release-1.0
179
+ """
180
+ target = path.resolve()
181
+ baseline = load_baseline(target, name)
182
+
183
+ if baseline is None:
184
+ console.print(f"[red]Error:[/red] Baseline '{name}' not found.")
185
+
186
+ # Show available baselines
187
+ available = list_baselines(target)
188
+ if available:
189
+ console.print("\n[dim]Available baselines:[/dim]")
190
+ for b in available:
191
+ console.print(f" - {b.name}")
192
+ else:
193
+ console.print("\n[dim]No baselines saved. Create one with:[/dim]")
194
+ console.print("[dim] vibeguard baseline save[/dim]")
195
+
196
+ raise typer.Exit(ExitCode.NO_CACHE)
197
+
198
+ console.print(f"[bold]Baseline: {baseline.name}[/bold]")
199
+ console.print(f" Created: {baseline.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
200
+ console.print(f" VibeGuard Version: {baseline.vibeguard_version}")
201
+ console.print(f" Total Findings: {baseline.total_count}")
202
+ console.print(f" Actionable: {baseline.actionable_count}")
203
+ console.print(f" Scanners: {', '.join(baseline.scanners_used)}")
204
+
205
+ if baseline.findings:
206
+ console.print()
207
+
208
+ # Show severity breakdown
209
+ severity_counts: dict[str, int] = {}
210
+ for f in baseline.findings:
211
+ sev = f.severity.value.upper()
212
+ severity_counts[sev] = severity_counts.get(sev, 0) + 1
213
+
214
+ console.print("[bold]Severity Breakdown:[/bold]")
215
+ for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
216
+ if sev in severity_counts:
217
+ color = {
218
+ "CRITICAL": "red bold",
219
+ "HIGH": "red",
220
+ "MEDIUM": "yellow",
221
+ "LOW": "blue",
222
+ "INFO": "dim",
223
+ }.get(sev, "white")
224
+ console.print(f" [{color}]{sev}:[/{color}] {severity_counts[sev]}")
225
+
226
+ console.print()
227
+ console.print("[dim]Compare against this baseline with:[/dim]")
228
+ console.print(f"[dim] vibeguard scan . --baseline {baseline.name}[/dim]")
229
+
230
+
231
+ @app.command("delete")
232
+ def delete_cmd(
233
+ name: str = typer.Argument(
234
+ ...,
235
+ help="Baseline name to delete",
236
+ ),
237
+ path: Path = typer.Option(
238
+ Path("."),
239
+ "--path",
240
+ "-p",
241
+ help="Repository path",
242
+ ),
243
+ force: bool = typer.Option(
244
+ False,
245
+ "--force",
246
+ "-f",
247
+ help="Delete without confirmation",
248
+ ),
249
+ ) -> None:
250
+ """Delete a baseline.
251
+
252
+ Example:
253
+ vibeguard baseline delete old-baseline
254
+ vibeguard baseline delete old-baseline --force
255
+ """
256
+ target = path.resolve()
257
+
258
+ # Check if baseline exists
259
+ baseline = load_baseline(target, name)
260
+ if baseline is None:
261
+ console.print(f"[red]Error:[/red] Baseline '{name}' not found.")
262
+
263
+ # Show available baselines
264
+ available = list_baselines(target)
265
+ if available:
266
+ console.print("\n[dim]Available baselines:[/dim]")
267
+ for b in available:
268
+ console.print(f" - {b.name}")
269
+
270
+ raise typer.Exit(ExitCode.NO_CACHE)
271
+
272
+ if not force:
273
+ console.print(f"[yellow]About to delete baseline:[/yellow] {name}")
274
+ console.print(f" Created: {baseline.created_at.strftime('%Y-%m-%d %H:%M')}")
275
+ console.print(f" Findings: {baseline.actionable_count}")
276
+
277
+ confirm = typer.confirm("Delete this baseline?", default=False)
278
+ if not confirm:
279
+ console.print("[dim]Cancelled.[/dim]")
280
+ raise typer.Exit(ExitCode.SUCCESS)
281
+
282
+ if delete_baseline(target, name):
283
+ console.print(f"[green]Deleted baseline:[/green] {name}")
284
+ else:
285
+ console.print("[red]Error:[/red] Failed to delete baseline.")
286
+ raise typer.Exit(ExitCode.CONFIG_ERROR)