scc-cli 1.4.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.
Potentially problematic release.
This version of scc-cli might be problematic. Click here for more details.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +259 -0
- scc_cli/cli_admin.py +683 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1400 -0
- scc_cli/cli_org.py +1433 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +858 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +603 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1082 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/exit_codes.py +55 -0
- scc_cli/git.py +1405 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +238 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +502 -0
- scc_cli/marketplace/sync.py +257 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +1034 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +582 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +339 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +669 -0
- scc_cli/ui/dashboard/loaders.py +369 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +337 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +521 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +490 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.0.dist-info/METADATA +369 -0
- scc_cli-1.4.0.dist-info/RECORD +112 -0
- scc_cli-1.4.0.dist-info/WHEEL +4 -0
- scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.0.dist-info/licenses/LICENSE +21 -0
scc_cli/doctor/render.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Orchestration and rendering functions for the doctor module.
|
|
2
|
+
|
|
3
|
+
This module contains:
|
|
4
|
+
- run_doctor(): Main orchestrator that runs all health checks
|
|
5
|
+
- build_doctor_json_data(): JSON serialization for CLI output
|
|
6
|
+
- render_doctor_results(): Rich terminal UI rendering
|
|
7
|
+
- render_doctor_compact(): Compact inline status display
|
|
8
|
+
- render_quick_status(): Single-line pass/fail indicator
|
|
9
|
+
- quick_check(): Fast prerequisite validation
|
|
10
|
+
- is_first_run(): First-run detection
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
|
|
24
|
+
from scc_cli import __version__
|
|
25
|
+
|
|
26
|
+
from .checks import (
|
|
27
|
+
check_config_directory,
|
|
28
|
+
check_docker,
|
|
29
|
+
check_docker_running,
|
|
30
|
+
check_docker_sandbox,
|
|
31
|
+
check_git,
|
|
32
|
+
check_user_config_valid,
|
|
33
|
+
check_workspace_path,
|
|
34
|
+
check_wsl2,
|
|
35
|
+
)
|
|
36
|
+
from .types import DoctorResult
|
|
37
|
+
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
# JSON Serialization
|
|
40
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_doctor_json_data(result: DoctorResult) -> dict[str, Any]:
|
|
44
|
+
"""Build JSON-serializable data from DoctorResult.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
result: The DoctorResult to convert.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary suitable for JSON envelope data field.
|
|
51
|
+
"""
|
|
52
|
+
checks_data = []
|
|
53
|
+
for check in result.checks:
|
|
54
|
+
check_dict: dict[str, Any] = {
|
|
55
|
+
"name": check.name,
|
|
56
|
+
"passed": check.passed,
|
|
57
|
+
"message": check.message,
|
|
58
|
+
"severity": check.severity,
|
|
59
|
+
}
|
|
60
|
+
if check.version:
|
|
61
|
+
check_dict["version"] = check.version
|
|
62
|
+
if check.fix_hint:
|
|
63
|
+
check_dict["fix_hint"] = check.fix_hint
|
|
64
|
+
if check.fix_url:
|
|
65
|
+
check_dict["fix_url"] = check.fix_url
|
|
66
|
+
if check.fix_commands:
|
|
67
|
+
check_dict["fix_commands"] = check.fix_commands
|
|
68
|
+
if check.code_frame:
|
|
69
|
+
check_dict["code_frame"] = check.code_frame
|
|
70
|
+
checks_data.append(check_dict)
|
|
71
|
+
|
|
72
|
+
# Calculate summary stats
|
|
73
|
+
total = len(result.checks)
|
|
74
|
+
passed = sum(1 for c in result.checks if c.passed)
|
|
75
|
+
errors = sum(1 for c in result.checks if not c.passed and c.severity == "error")
|
|
76
|
+
warnings = sum(1 for c in result.checks if not c.passed and c.severity == "warning")
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"checks": checks_data,
|
|
80
|
+
"summary": {
|
|
81
|
+
"total": total,
|
|
82
|
+
"passed": passed,
|
|
83
|
+
"errors": errors,
|
|
84
|
+
"warnings": warnings,
|
|
85
|
+
"all_ok": result.all_ok,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
91
|
+
# Main Doctor Orchestrator
|
|
92
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_doctor(workspace: Path | None = None) -> DoctorResult:
|
|
96
|
+
"""Run all health checks and return comprehensive results.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
workspace: Optional workspace path to check for optimization
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
DoctorResult with all check results
|
|
103
|
+
"""
|
|
104
|
+
result = DoctorResult()
|
|
105
|
+
|
|
106
|
+
# Git check
|
|
107
|
+
git_check = check_git()
|
|
108
|
+
result.checks.append(git_check)
|
|
109
|
+
result.git_ok = git_check.passed
|
|
110
|
+
result.git_version = git_check.version
|
|
111
|
+
|
|
112
|
+
# Docker check
|
|
113
|
+
docker_check = check_docker()
|
|
114
|
+
result.checks.append(docker_check)
|
|
115
|
+
result.docker_ok = docker_check.passed
|
|
116
|
+
result.docker_version = docker_check.version
|
|
117
|
+
|
|
118
|
+
# Docker daemon check (only if Docker is installed)
|
|
119
|
+
if result.docker_ok:
|
|
120
|
+
daemon_check = check_docker_running()
|
|
121
|
+
result.checks.append(daemon_check)
|
|
122
|
+
if not daemon_check.passed:
|
|
123
|
+
result.docker_ok = False
|
|
124
|
+
|
|
125
|
+
# Docker sandbox check (only if Docker is OK)
|
|
126
|
+
if result.docker_ok:
|
|
127
|
+
sandbox_check = check_docker_sandbox()
|
|
128
|
+
result.checks.append(sandbox_check)
|
|
129
|
+
result.sandbox_ok = sandbox_check.passed
|
|
130
|
+
else:
|
|
131
|
+
result.sandbox_ok = False
|
|
132
|
+
|
|
133
|
+
# WSL2 check
|
|
134
|
+
wsl2_check, is_wsl2 = check_wsl2()
|
|
135
|
+
result.checks.append(wsl2_check)
|
|
136
|
+
result.wsl2_detected = is_wsl2
|
|
137
|
+
|
|
138
|
+
# Workspace path check (if WSL2 and workspace provided)
|
|
139
|
+
if workspace:
|
|
140
|
+
path_check = check_workspace_path(workspace)
|
|
141
|
+
result.checks.append(path_check)
|
|
142
|
+
result.windows_path_warning = not path_check.passed and path_check.severity == "warning"
|
|
143
|
+
|
|
144
|
+
# Config directory check
|
|
145
|
+
config_check = check_config_directory()
|
|
146
|
+
result.checks.append(config_check)
|
|
147
|
+
|
|
148
|
+
# User config JSON validation check
|
|
149
|
+
user_config_check = check_user_config_valid()
|
|
150
|
+
result.checks.append(user_config_check)
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
156
|
+
# Rich Terminal UI Rendering
|
|
157
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def render_doctor_results(console: Console, result: DoctorResult) -> None:
|
|
161
|
+
"""Render doctor results with beautiful Rich formatting.
|
|
162
|
+
|
|
163
|
+
Uses consistent styling with the rest of the CLI:
|
|
164
|
+
- Cyan for info/brand
|
|
165
|
+
- Green for success
|
|
166
|
+
- Yellow for warnings
|
|
167
|
+
- Red for errors
|
|
168
|
+
"""
|
|
169
|
+
# Header
|
|
170
|
+
console.print()
|
|
171
|
+
|
|
172
|
+
# Build results table
|
|
173
|
+
table = Table(
|
|
174
|
+
box=box.ROUNDED,
|
|
175
|
+
show_header=True,
|
|
176
|
+
header_style="bold cyan",
|
|
177
|
+
border_style="dim",
|
|
178
|
+
padding=(0, 1),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
table.add_column("Status", width=8, justify="center")
|
|
182
|
+
table.add_column("Check", min_width=20)
|
|
183
|
+
table.add_column("Details", min_width=30)
|
|
184
|
+
|
|
185
|
+
for check in result.checks:
|
|
186
|
+
# Status icon with color
|
|
187
|
+
if check.passed:
|
|
188
|
+
status = Text(" ", style="bold green")
|
|
189
|
+
elif check.severity == "warning":
|
|
190
|
+
status = Text(" ", style="bold yellow")
|
|
191
|
+
else:
|
|
192
|
+
status = Text(" ", style="bold red")
|
|
193
|
+
|
|
194
|
+
# Check name
|
|
195
|
+
name = Text(check.name, style="white")
|
|
196
|
+
|
|
197
|
+
# Details with version and message
|
|
198
|
+
details = Text()
|
|
199
|
+
if check.version:
|
|
200
|
+
details.append(f"{check.version}\n", style="cyan")
|
|
201
|
+
details.append(check.message, style="dim" if check.passed else "white")
|
|
202
|
+
|
|
203
|
+
if not check.passed and check.fix_hint:
|
|
204
|
+
details.append(f"\n{check.fix_hint}", style="yellow")
|
|
205
|
+
|
|
206
|
+
table.add_row(status, name, details)
|
|
207
|
+
|
|
208
|
+
# Wrap table in panel
|
|
209
|
+
title_style = "bold green" if result.all_ok else "bold red"
|
|
210
|
+
version_suffix = f" (scc-cli v{__version__})"
|
|
211
|
+
title_text = (
|
|
212
|
+
f"System Health Check{version_suffix}"
|
|
213
|
+
if result.all_ok
|
|
214
|
+
else f"System Health Check - Issues Found{version_suffix}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
panel = Panel(
|
|
218
|
+
table,
|
|
219
|
+
title=f"[{title_style}]{title_text}[/{title_style}]",
|
|
220
|
+
border_style="green" if result.all_ok else "red",
|
|
221
|
+
padding=(1, 1),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
console.print(panel)
|
|
225
|
+
|
|
226
|
+
# Display code frames for any checks with syntax errors (beautiful error display)
|
|
227
|
+
code_frame_checks = [c for c in result.checks if c.code_frame and not c.passed]
|
|
228
|
+
for check in code_frame_checks:
|
|
229
|
+
if check.code_frame is not None: # Type guard for mypy
|
|
230
|
+
console.print()
|
|
231
|
+
# Create a panel for the code frame with Rich styling
|
|
232
|
+
code_panel = Panel(
|
|
233
|
+
check.code_frame,
|
|
234
|
+
title=f"[bold red]⚠️ JSON Syntax Error: {check.name}[/bold red]",
|
|
235
|
+
border_style="red",
|
|
236
|
+
padding=(1, 2),
|
|
237
|
+
)
|
|
238
|
+
console.print(code_panel)
|
|
239
|
+
|
|
240
|
+
# Summary line
|
|
241
|
+
if result.all_ok:
|
|
242
|
+
console.print()
|
|
243
|
+
console.print(
|
|
244
|
+
" [bold green]All prerequisites met![/bold green] [dim]Ready to run Claude Code.[/dim]"
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
console.print()
|
|
248
|
+
summary_parts = []
|
|
249
|
+
if result.error_count > 0:
|
|
250
|
+
summary_parts.append(f"[bold red]{result.error_count} error(s)[/bold red]")
|
|
251
|
+
if result.warning_count > 0:
|
|
252
|
+
summary_parts.append(f"[bold yellow]{result.warning_count} warning(s)[/bold yellow]")
|
|
253
|
+
|
|
254
|
+
console.print(f" Found {' and '.join(summary_parts)}. ", end="")
|
|
255
|
+
console.print("[dim]Fix the issues above to continue.[/dim]")
|
|
256
|
+
|
|
257
|
+
# Next Steps section with fix_commands
|
|
258
|
+
checks_with_commands = [c for c in result.checks if not c.passed and c.fix_commands]
|
|
259
|
+
if checks_with_commands:
|
|
260
|
+
console.print()
|
|
261
|
+
console.print(" [bold cyan]Next Steps[/bold cyan]")
|
|
262
|
+
console.print(" [dim]────────────────────────────────────────────────────[/dim]")
|
|
263
|
+
console.print()
|
|
264
|
+
|
|
265
|
+
for check in checks_with_commands:
|
|
266
|
+
console.print(f" [bold white]{check.name}:[/bold white]")
|
|
267
|
+
if check.fix_hint:
|
|
268
|
+
console.print(f" [dim]{check.fix_hint}[/dim]")
|
|
269
|
+
if check.fix_commands:
|
|
270
|
+
for i, cmd in enumerate(check.fix_commands, 1):
|
|
271
|
+
console.print(f" [cyan]{i}.[/cyan] [white]{cmd}[/white]")
|
|
272
|
+
console.print()
|
|
273
|
+
|
|
274
|
+
console.print()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def render_doctor_compact(console: Console, result: DoctorResult) -> None:
|
|
278
|
+
"""Render compact doctor results for inline display.
|
|
279
|
+
|
|
280
|
+
Used during startup to show quick status.
|
|
281
|
+
"""
|
|
282
|
+
checks = []
|
|
283
|
+
|
|
284
|
+
# Git
|
|
285
|
+
if result.git_ok:
|
|
286
|
+
checks.append("[green]Git[/green]")
|
|
287
|
+
else:
|
|
288
|
+
checks.append("[red]Git[/red]")
|
|
289
|
+
|
|
290
|
+
# Docker
|
|
291
|
+
if result.docker_ok:
|
|
292
|
+
checks.append("[green]Docker[/green]")
|
|
293
|
+
else:
|
|
294
|
+
checks.append("[red]Docker[/red]")
|
|
295
|
+
|
|
296
|
+
# Sandbox
|
|
297
|
+
if result.sandbox_ok:
|
|
298
|
+
checks.append("[green]Sandbox[/green]")
|
|
299
|
+
else:
|
|
300
|
+
checks.append("[red]Sandbox[/red]")
|
|
301
|
+
|
|
302
|
+
console.print(f" [dim]Prerequisites:[/dim] {' | '.join(checks)}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def render_quick_status(console: Console, result: DoctorResult) -> None:
|
|
306
|
+
"""Render a single-line status for quick checks.
|
|
307
|
+
|
|
308
|
+
Returns immediately with pass/fail indicator.
|
|
309
|
+
"""
|
|
310
|
+
if result.all_ok:
|
|
311
|
+
console.print("[green] All systems operational[/green]")
|
|
312
|
+
else:
|
|
313
|
+
failed = [c.name for c in result.checks if not c.passed and c.severity == "error"]
|
|
314
|
+
console.print(f"[red] Issues detected:[/red] {', '.join(failed)}")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
318
|
+
# Quick Check Utilities
|
|
319
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def quick_check() -> bool:
|
|
323
|
+
"""Perform a quick prerequisite check.
|
|
324
|
+
|
|
325
|
+
Returns True if all critical prerequisites are met.
|
|
326
|
+
Used for fast startup validation.
|
|
327
|
+
"""
|
|
328
|
+
result = run_doctor()
|
|
329
|
+
return result.all_ok
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def is_first_run() -> bool:
|
|
333
|
+
"""Check if this is the first run of scc.
|
|
334
|
+
|
|
335
|
+
Returns True if config directory doesn't exist or is empty.
|
|
336
|
+
"""
|
|
337
|
+
from scc_cli import config
|
|
338
|
+
|
|
339
|
+
config_dir = config.CONFIG_DIR
|
|
340
|
+
|
|
341
|
+
if not config_dir.exists():
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
# Check if config file exists
|
|
345
|
+
config_file = config.CONFIG_FILE
|
|
346
|
+
return not config_file.exists()
|
scc_cli/doctor/types.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Define data types for the doctor health check module.
|
|
2
|
+
|
|
3
|
+
Provide dataclasses for representing check results, validation results,
|
|
4
|
+
and overall doctor diagnostic results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CheckResult:
|
|
15
|
+
"""Result of a single health check."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
passed: bool
|
|
19
|
+
message: str
|
|
20
|
+
version: str | None = None
|
|
21
|
+
fix_hint: str | None = None
|
|
22
|
+
fix_url: str | None = None
|
|
23
|
+
severity: str = "error" # "error", "warning", "info"
|
|
24
|
+
code_frame: str | None = None # Optional code frame for syntax errors
|
|
25
|
+
fix_commands: list[str] | None = None # Copy-pasteable fix commands
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class JsonValidationResult:
|
|
30
|
+
"""Result of JSON file validation with error details."""
|
|
31
|
+
|
|
32
|
+
valid: bool
|
|
33
|
+
error_message: str | None = None
|
|
34
|
+
line: int | None = None
|
|
35
|
+
column: int | None = None
|
|
36
|
+
file_path: Path | None = None
|
|
37
|
+
code_frame: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DoctorResult:
|
|
42
|
+
"""Complete health check results."""
|
|
43
|
+
|
|
44
|
+
git_ok: bool = False
|
|
45
|
+
git_version: str | None = None
|
|
46
|
+
docker_ok: bool = False
|
|
47
|
+
docker_version: str | None = None
|
|
48
|
+
sandbox_ok: bool = False
|
|
49
|
+
wsl2_detected: bool = False
|
|
50
|
+
windows_path_warning: bool = False
|
|
51
|
+
checks: list[CheckResult] = field(default_factory=list)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def all_ok(self) -> bool:
|
|
55
|
+
"""Check if all critical prerequisites pass."""
|
|
56
|
+
return self.git_ok and self.docker_ok and self.sandbox_ok
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def error_count(self) -> int:
|
|
60
|
+
"""Return the count of failed critical checks."""
|
|
61
|
+
return sum(1 for c in self.checks if not c.passed and c.severity == "error")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def warning_count(self) -> int:
|
|
65
|
+
"""Return the count of warnings."""
|
|
66
|
+
return sum(1 for c in self.checks if not c.passed and c.severity == "warning")
|
scc_cli/errors.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed exceptions for SCC - Sandboxed Claude CLI.
|
|
3
|
+
|
|
4
|
+
Error handling philosophy: "One message, one action"
|
|
5
|
+
- Each error has a clear user_message (what went wrong)
|
|
6
|
+
- Each error has a suggested_action (what to do next)
|
|
7
|
+
- Debug context is available with --debug flag
|
|
8
|
+
|
|
9
|
+
Exit codes:
|
|
10
|
+
- 0: Success
|
|
11
|
+
- 2: Invalid usage / bad input
|
|
12
|
+
- 3: Missing prerequisites (Docker, Git)
|
|
13
|
+
- 4: External tool failure (docker/git command failed)
|
|
14
|
+
- 5: Internal error (bug)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SCCError(Exception):
|
|
22
|
+
"""Base error with user-friendly messaging."""
|
|
23
|
+
|
|
24
|
+
user_message: str
|
|
25
|
+
suggested_action: str = ""
|
|
26
|
+
debug_context: str | None = None
|
|
27
|
+
exit_code: int = 1
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return self.user_message
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class UsageError(SCCError):
|
|
35
|
+
"""Invalid usage or bad input."""
|
|
36
|
+
|
|
37
|
+
exit_code: int = field(default=2, init=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PrerequisiteError(SCCError):
|
|
42
|
+
"""Docker/Git missing or wrong version."""
|
|
43
|
+
|
|
44
|
+
exit_code: int = field(default=3, init=False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class DockerNotFoundError(PrerequisiteError):
|
|
49
|
+
"""Docker is not installed or not in PATH."""
|
|
50
|
+
|
|
51
|
+
user_message: str = field(default="Docker is not installed or not in PATH")
|
|
52
|
+
suggested_action: str = field(
|
|
53
|
+
default="Install Docker Desktop from https://docker.com/products/docker-desktop"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class DockerVersionError(PrerequisiteError):
|
|
59
|
+
"""Docker version is too old for sandbox feature."""
|
|
60
|
+
|
|
61
|
+
current_version: str = ""
|
|
62
|
+
required_version: str = "4.50.0"
|
|
63
|
+
user_message: str = field(default="")
|
|
64
|
+
suggested_action: str = field(
|
|
65
|
+
default="Update Docker Desktop from https://docker.com/products/docker-desktop"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
if not self.user_message:
|
|
70
|
+
self.user_message = (
|
|
71
|
+
f"Docker Desktop {self.required_version}+ required for sandbox support\n"
|
|
72
|
+
f"Current: {self.current_version or 'unknown'} | Required: {self.required_version}+"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class SandboxNotAvailableError(PrerequisiteError):
|
|
78
|
+
"""Docker sandbox feature is not available."""
|
|
79
|
+
|
|
80
|
+
user_message: str = field(default="Docker sandbox feature is not available")
|
|
81
|
+
suggested_action: str = field(
|
|
82
|
+
default="Ensure Docker Desktop is version 4.50+ and sandbox feature is enabled"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class GitNotFoundError(PrerequisiteError):
|
|
88
|
+
"""Git is not installed or not in PATH."""
|
|
89
|
+
|
|
90
|
+
user_message: str = field(default="Git is not installed or not in PATH")
|
|
91
|
+
suggested_action: str = field(default="Install Git from https://git-scm.com/downloads")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ToolError(SCCError):
|
|
96
|
+
"""External tool (Docker/Git) command failed."""
|
|
97
|
+
|
|
98
|
+
exit_code: int = field(default=4, init=False)
|
|
99
|
+
command: str | None = None
|
|
100
|
+
stderr: str | None = None
|
|
101
|
+
|
|
102
|
+
def __post_init__(self) -> None:
|
|
103
|
+
if self.command or self.stderr:
|
|
104
|
+
parts = []
|
|
105
|
+
if self.command:
|
|
106
|
+
parts.append(f"Command: {self.command}")
|
|
107
|
+
if self.stderr:
|
|
108
|
+
parts.append(f"Error: {self.stderr}")
|
|
109
|
+
self.debug_context = "\n".join(parts)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class WorkspaceError(ToolError):
|
|
114
|
+
"""Invalid workspace path or clone failed."""
|
|
115
|
+
|
|
116
|
+
user_message: str = field(default="Workspace error")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class WorkspaceNotFoundError(WorkspaceError):
|
|
121
|
+
"""Workspace path does not exist."""
|
|
122
|
+
|
|
123
|
+
path: str = ""
|
|
124
|
+
user_message: str = field(default="")
|
|
125
|
+
suggested_action: str = field(default="Check the path exists or create the directory")
|
|
126
|
+
|
|
127
|
+
def __post_init__(self) -> None:
|
|
128
|
+
super().__post_init__()
|
|
129
|
+
if not self.user_message and self.path:
|
|
130
|
+
self.user_message = f"Workspace not found: {self.path}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class NotAGitRepoError(WorkspaceError):
|
|
135
|
+
"""Path is not a git repository."""
|
|
136
|
+
|
|
137
|
+
path: str = ""
|
|
138
|
+
user_message: str = field(default="")
|
|
139
|
+
suggested_action: str = field(default="Initialize git with 'git init' or clone a repository")
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
super().__post_init__()
|
|
143
|
+
if not self.user_message and self.path:
|
|
144
|
+
self.user_message = f"Not a git repository: {self.path}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class CloneError(WorkspaceError):
|
|
149
|
+
"""Git clone failed."""
|
|
150
|
+
|
|
151
|
+
url: str = ""
|
|
152
|
+
user_message: str = field(default="")
|
|
153
|
+
suggested_action: str = field(default="Check the repository URL and your network connection")
|
|
154
|
+
|
|
155
|
+
def __post_init__(self) -> None:
|
|
156
|
+
super().__post_init__()
|
|
157
|
+
if not self.user_message and self.url:
|
|
158
|
+
self.user_message = f"Failed to clone repository: {self.url}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class GitWorktreeError(ToolError):
|
|
163
|
+
"""Worktree creation/cleanup failed."""
|
|
164
|
+
|
|
165
|
+
user_message: str = field(default="Git worktree operation failed")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class WorktreeExistsError(GitWorktreeError):
|
|
170
|
+
"""Worktree already exists."""
|
|
171
|
+
|
|
172
|
+
path: str = ""
|
|
173
|
+
user_message: str = field(default="")
|
|
174
|
+
suggested_action: str = field(
|
|
175
|
+
default="Use existing worktree, remove it first, or choose a different name"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
super().__post_init__()
|
|
180
|
+
if not self.user_message and self.path:
|
|
181
|
+
self.user_message = f"Worktree already exists: {self.path}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class WorktreeCreationError(GitWorktreeError):
|
|
186
|
+
"""Failed to create worktree."""
|
|
187
|
+
|
|
188
|
+
name: str = ""
|
|
189
|
+
user_message: str = field(default="")
|
|
190
|
+
suggested_action: str = field(
|
|
191
|
+
default="Check if the branch already exists or if there are uncommitted changes"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def __post_init__(self) -> None:
|
|
195
|
+
super().__post_init__()
|
|
196
|
+
if not self.user_message and self.name:
|
|
197
|
+
self.user_message = f"Failed to create worktree: {self.name}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class SandboxLaunchError(ToolError):
|
|
202
|
+
"""Docker sandbox failed to start."""
|
|
203
|
+
|
|
204
|
+
exit_code: int = field(default=5, init=False)
|
|
205
|
+
user_message: str = field(default="Failed to start Docker sandbox")
|
|
206
|
+
suggested_action: str = field(
|
|
207
|
+
default="Check Docker Desktop is running and has available resources"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class ContainerNotFoundError(ToolError):
|
|
213
|
+
"""Container does not exist (for resume operations)."""
|
|
214
|
+
|
|
215
|
+
container_name: str = ""
|
|
216
|
+
user_message: str = field(default="")
|
|
217
|
+
suggested_action: str = field(
|
|
218
|
+
default="Start a new session or check 'scc list' for available containers"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def __post_init__(self) -> None:
|
|
222
|
+
super().__post_init__()
|
|
223
|
+
if not self.user_message and self.container_name:
|
|
224
|
+
self.user_message = f"Container not found: {self.container_name}"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class InternalError(SCCError):
|
|
229
|
+
"""Internal error (bug in the CLI)."""
|
|
230
|
+
|
|
231
|
+
exit_code: int = field(default=5, init=False)
|
|
232
|
+
suggested_action: str = field(
|
|
233
|
+
default="Please report this issue at https://github.com/CCimen/scc/issues"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass
|
|
238
|
+
class ConfigError(SCCError):
|
|
239
|
+
"""Configuration error."""
|
|
240
|
+
|
|
241
|
+
exit_code: int = field(default=2, init=False)
|
|
242
|
+
user_message: str = field(default="Configuration error")
|
|
243
|
+
suggested_action: str = field(default="Run 'scc config --show' to view current configuration")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ProfileNotFoundError(ConfigError):
|
|
248
|
+
"""Team profile not found."""
|
|
249
|
+
|
|
250
|
+
profile_name: str = ""
|
|
251
|
+
user_message: str = field(default="")
|
|
252
|
+
suggested_action: str = field(default="Run 'scc team list' to see available profiles")
|
|
253
|
+
|
|
254
|
+
def __post_init__(self) -> None:
|
|
255
|
+
if not self.user_message and self.profile_name:
|
|
256
|
+
self.user_message = f"Team profile not found: {self.profile_name}"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@dataclass
|
|
260
|
+
class PolicyViolationError(ConfigError):
|
|
261
|
+
"""Security policy violation during config processing.
|
|
262
|
+
|
|
263
|
+
Raised when a plugin, MCP server, or other item is blocked by
|
|
264
|
+
organization security policies.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
item: str = ""
|
|
268
|
+
blocked_by: str = ""
|
|
269
|
+
item_type: str = "plugin" # Default to plugin
|
|
270
|
+
user_message: str = field(default="")
|
|
271
|
+
suggested_action: str = field(default="")
|
|
272
|
+
|
|
273
|
+
def __post_init__(self) -> None:
|
|
274
|
+
if not self.user_message and self.item:
|
|
275
|
+
if self.blocked_by:
|
|
276
|
+
self.user_message = (
|
|
277
|
+
f"Security policy violation: '{self.item}' is blocked "
|
|
278
|
+
f"by pattern '{self.blocked_by}'"
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
self.user_message = f"Security policy violation: '{self.item}' is blocked"
|
|
282
|
+
|
|
283
|
+
# Generate fix-it command for suggested action
|
|
284
|
+
if not self.suggested_action and self.item:
|
|
285
|
+
from scc_cli.utils.fixit import generate_policy_exception_command
|
|
286
|
+
|
|
287
|
+
cmd = generate_policy_exception_command(self.item, self.item_type)
|
|
288
|
+
self.suggested_action = f"To request a policy exception (requires PR approval): {cmd}"
|