scc-cli 1.4.1__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.

Files changed (113) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +706 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1521 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
@@ -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()
@@ -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}"