scc-cli 1.5.3__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 +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -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 +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -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/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -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 +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -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 +960 -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/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -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 +383 -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 +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -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 +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
scc_cli/core/errors.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
import shlex
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SCCError(Exception):
|
|
23
|
+
"""Base error with user-friendly messaging."""
|
|
24
|
+
|
|
25
|
+
user_message: str
|
|
26
|
+
suggested_action: str = ""
|
|
27
|
+
debug_context: str | None = None
|
|
28
|
+
exit_code: int = 1
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
return self.user_message
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class UsageError(SCCError):
|
|
36
|
+
"""Invalid usage or bad input."""
|
|
37
|
+
|
|
38
|
+
exit_code: int = field(default=2, init=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PrerequisiteError(SCCError):
|
|
43
|
+
"""Docker/Git missing or wrong version."""
|
|
44
|
+
|
|
45
|
+
exit_code: int = field(default=3, init=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class DockerNotFoundError(PrerequisiteError):
|
|
50
|
+
"""Docker is not installed or not in PATH."""
|
|
51
|
+
|
|
52
|
+
user_message: str = field(default="Docker is not installed or not in PATH")
|
|
53
|
+
suggested_action: str = field(
|
|
54
|
+
default="Install Docker Desktop from https://docker.com/products/docker-desktop"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class DockerVersionError(PrerequisiteError):
|
|
60
|
+
"""Docker version is too old for sandbox feature."""
|
|
61
|
+
|
|
62
|
+
current_version: str = ""
|
|
63
|
+
required_version: str = "4.50.0"
|
|
64
|
+
user_message: str = field(default="")
|
|
65
|
+
suggested_action: str = field(
|
|
66
|
+
default="Update Docker Desktop from https://docker.com/products/docker-desktop"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __post_init__(self) -> None:
|
|
70
|
+
if not self.user_message:
|
|
71
|
+
self.user_message = (
|
|
72
|
+
f"Docker Desktop {self.required_version}+ required for sandbox support\n"
|
|
73
|
+
f"Current: {self.current_version or 'unknown'} | Required: {self.required_version}+"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class SandboxNotAvailableError(PrerequisiteError):
|
|
79
|
+
"""Docker sandbox feature is not available."""
|
|
80
|
+
|
|
81
|
+
user_message: str = field(default="Docker sandbox feature is not available")
|
|
82
|
+
suggested_action: str = field(
|
|
83
|
+
default="Ensure Docker Desktop is version 4.50+ and sandbox feature is enabled"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class GitNotFoundError(PrerequisiteError):
|
|
89
|
+
"""Git is not installed or not in PATH."""
|
|
90
|
+
|
|
91
|
+
user_message: str = field(default="Git is not installed or not in PATH")
|
|
92
|
+
suggested_action: str = field(default="Install Git from https://git-scm.com/downloads")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ToolError(SCCError):
|
|
97
|
+
"""External tool (Docker/Git) command failed."""
|
|
98
|
+
|
|
99
|
+
exit_code: int = field(default=4, init=False)
|
|
100
|
+
command: str | None = None
|
|
101
|
+
stderr: str | None = None
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
if self.command or self.stderr:
|
|
105
|
+
parts = []
|
|
106
|
+
if self.command:
|
|
107
|
+
parts.append(f"Command: {self.command}")
|
|
108
|
+
if self.stderr:
|
|
109
|
+
parts.append(f"Error: {self.stderr}")
|
|
110
|
+
self.debug_context = "\n".join(parts)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class WorkspaceError(ToolError):
|
|
115
|
+
"""Invalid workspace path or clone failed."""
|
|
116
|
+
|
|
117
|
+
user_message: str = field(default="Workspace error")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class WorkspaceNotFoundError(WorkspaceError):
|
|
122
|
+
"""Workspace path does not exist."""
|
|
123
|
+
|
|
124
|
+
path: str = ""
|
|
125
|
+
user_message: str = field(default="")
|
|
126
|
+
suggested_action: str = field(default="Check the path exists or create the directory")
|
|
127
|
+
|
|
128
|
+
def __post_init__(self) -> None:
|
|
129
|
+
super().__post_init__()
|
|
130
|
+
if not self.user_message and self.path:
|
|
131
|
+
self.user_message = f"Workspace not found: {self.path}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class NotAGitRepoError(WorkspaceError):
|
|
136
|
+
"""Path is not a git repository."""
|
|
137
|
+
|
|
138
|
+
path: str = ""
|
|
139
|
+
user_message: str = field(default="")
|
|
140
|
+
suggested_action: str = field(default="Initialize git with 'git init' or clone a repository")
|
|
141
|
+
|
|
142
|
+
def __post_init__(self) -> None:
|
|
143
|
+
super().__post_init__()
|
|
144
|
+
if not self.user_message and self.path:
|
|
145
|
+
self.user_message = f"Not a git repository: {self.path}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class CloneError(WorkspaceError):
|
|
150
|
+
"""Git clone failed."""
|
|
151
|
+
|
|
152
|
+
url: str = ""
|
|
153
|
+
user_message: str = field(default="")
|
|
154
|
+
suggested_action: str = field(default="Check the repository URL and your network connection")
|
|
155
|
+
|
|
156
|
+
def __post_init__(self) -> None:
|
|
157
|
+
super().__post_init__()
|
|
158
|
+
if not self.user_message and self.url:
|
|
159
|
+
self.user_message = f"Failed to clone repository: {self.url}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class GitWorktreeError(ToolError):
|
|
164
|
+
"""Worktree creation/cleanup failed."""
|
|
165
|
+
|
|
166
|
+
user_message: str = field(default="Git worktree operation failed")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class WorktreeExistsError(GitWorktreeError):
|
|
171
|
+
"""Worktree already exists."""
|
|
172
|
+
|
|
173
|
+
path: str = ""
|
|
174
|
+
user_message: str = field(default="")
|
|
175
|
+
suggested_action: str = field(
|
|
176
|
+
default="Use existing worktree, remove it first, or choose a different name"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def __post_init__(self) -> None:
|
|
180
|
+
super().__post_init__()
|
|
181
|
+
if not self.user_message and self.path:
|
|
182
|
+
self.user_message = f"Worktree already exists: {self.path}"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class WorktreeCreationError(GitWorktreeError):
|
|
187
|
+
"""Failed to create worktree."""
|
|
188
|
+
|
|
189
|
+
name: str = ""
|
|
190
|
+
user_message: str = field(default="")
|
|
191
|
+
suggested_action: str = field(
|
|
192
|
+
default="Check if the branch already exists or if there are uncommitted changes"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def __post_init__(self) -> None:
|
|
196
|
+
super().__post_init__()
|
|
197
|
+
if not self.user_message and self.name:
|
|
198
|
+
self.user_message = f"Failed to create worktree: {self.name}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class SandboxLaunchError(ToolError):
|
|
203
|
+
"""Docker sandbox failed to start."""
|
|
204
|
+
|
|
205
|
+
exit_code: int = field(default=5, init=False)
|
|
206
|
+
user_message: str = field(default="Failed to start Docker sandbox")
|
|
207
|
+
suggested_action: str = field(
|
|
208
|
+
default="Check Docker Desktop is running and has available resources"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class ContainerNotFoundError(ToolError):
|
|
214
|
+
"""Container does not exist (for resume operations)."""
|
|
215
|
+
|
|
216
|
+
container_name: str = ""
|
|
217
|
+
user_message: str = field(default="")
|
|
218
|
+
suggested_action: str = field(
|
|
219
|
+
default="Start a new session or check 'scc list' for available containers"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def __post_init__(self) -> None:
|
|
223
|
+
super().__post_init__()
|
|
224
|
+
if not self.user_message and self.container_name:
|
|
225
|
+
self.user_message = f"Container not found: {self.container_name}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@dataclass
|
|
229
|
+
class InternalError(SCCError):
|
|
230
|
+
"""Internal error (bug in the CLI)."""
|
|
231
|
+
|
|
232
|
+
exit_code: int = field(default=5, init=False)
|
|
233
|
+
suggested_action: str = field(
|
|
234
|
+
default="Please report this issue at https://github.com/CCimen/scc/issues"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class ConfigError(SCCError):
|
|
240
|
+
"""Configuration error."""
|
|
241
|
+
|
|
242
|
+
exit_code: int = field(default=2, init=False)
|
|
243
|
+
user_message: str = field(default="Configuration error")
|
|
244
|
+
suggested_action: str = field(default="Run 'scc config --show' to view current configuration")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass
|
|
248
|
+
class ProfileNotFoundError(ConfigError):
|
|
249
|
+
"""Team profile not found."""
|
|
250
|
+
|
|
251
|
+
profile_name: str = ""
|
|
252
|
+
user_message: str = field(default="")
|
|
253
|
+
suggested_action: str = field(default="Run 'scc team list' to see available profiles")
|
|
254
|
+
|
|
255
|
+
def __post_init__(self) -> None:
|
|
256
|
+
if not self.user_message and self.profile_name:
|
|
257
|
+
self.user_message = f"Team profile not found: {self.profile_name}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass
|
|
261
|
+
class PolicyViolationError(ConfigError):
|
|
262
|
+
"""Security policy violation during config processing.
|
|
263
|
+
|
|
264
|
+
Raised when a plugin, MCP server, or other item is blocked by
|
|
265
|
+
organization security policies.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
item: str = ""
|
|
269
|
+
blocked_by: str = ""
|
|
270
|
+
item_type: str = "plugin" # Default to plugin
|
|
271
|
+
user_message: str = field(default="")
|
|
272
|
+
suggested_action: str = field(default="")
|
|
273
|
+
|
|
274
|
+
def __post_init__(self) -> None:
|
|
275
|
+
if not self.user_message and self.item:
|
|
276
|
+
if self.blocked_by:
|
|
277
|
+
self.user_message = (
|
|
278
|
+
f"Security policy violation: '{self.item}' is blocked "
|
|
279
|
+
f"by pattern '{self.blocked_by}'"
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
self.user_message = f"Security policy violation: '{self.item}' is blocked"
|
|
283
|
+
|
|
284
|
+
# Generate fix-it command for suggested action (inline to keep core/ dependency-free)
|
|
285
|
+
if not self.suggested_action and self.item:
|
|
286
|
+
type_to_flag = {
|
|
287
|
+
"plugin": "--allow-plugin",
|
|
288
|
+
"mcp_server": "--allow-mcp",
|
|
289
|
+
"base_image": "--allow-image",
|
|
290
|
+
}
|
|
291
|
+
flag = type_to_flag.get(self.item_type, f"--allow-{self.item_type}")
|
|
292
|
+
quoted_item = shlex.quote(self.item)
|
|
293
|
+
cmd = (
|
|
294
|
+
"scc exceptions create --policy --id INC-... "
|
|
295
|
+
f'{flag} {quoted_item} --ttl 8h --reason "..."'
|
|
296
|
+
)
|
|
297
|
+
self.suggested_action = f"To request a policy exception (requires PR approval): {cmd}"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exit codes for SCC CLI.
|
|
3
|
+
|
|
4
|
+
Standardized exit codes following Unix conventions with semantic meaning.
|
|
5
|
+
All commands MUST use these constants for consistency.
|
|
6
|
+
|
|
7
|
+
Exit Code Semantics:
|
|
8
|
+
0: Success - command completed successfully
|
|
9
|
+
1: Not Found - target not found (worktree name, session id, workspace missing)
|
|
10
|
+
2: Usage Error - bad flags, invalid inputs, missing required args
|
|
11
|
+
3: Config Error - config problems, network errors
|
|
12
|
+
4: Tool Error - external tool failed (git error, docker error, not a git repo)
|
|
13
|
+
5: Prerequisite Error - missing tools (Docker, Git not installed)
|
|
14
|
+
6: Governance Error - blocked by policy
|
|
15
|
+
130: Cancelled - user cancelled operation (SIGINT)
|
|
16
|
+
|
|
17
|
+
Preferred Usage:
|
|
18
|
+
- Use specific codes (EXIT_NOT_FOUND, EXIT_TOOL, EXIT_CONFIG, etc.)
|
|
19
|
+
- Avoid EXIT_ERROR - use a specific code instead
|
|
20
|
+
- EXIT_VALIDATION maps to EXIT_TOOL (validation is a tool concern)
|
|
21
|
+
- EXIT_INTERNAL maps to EXIT_PREREQ (internal errors are system issues)
|
|
22
|
+
|
|
23
|
+
Note: Click/Typer argument parsing errors (EXIT_USAGE) occur before
|
|
24
|
+
commands run, so they emit to stderr without JSON envelope.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Success
|
|
28
|
+
EXIT_SUCCESS = 0 # Command completed successfully
|
|
29
|
+
|
|
30
|
+
# Primary exit codes (1-6) - use these in new code
|
|
31
|
+
EXIT_NOT_FOUND = 1 # Target not found (worktree, session, workspace)
|
|
32
|
+
EXIT_USAGE = 2 # Invalid usage/arguments (Click default)
|
|
33
|
+
EXIT_CONFIG = 3 # Config or network error
|
|
34
|
+
EXIT_TOOL = 4 # External tool failed (git error, docker error, not a git repo)
|
|
35
|
+
EXIT_PREREQ = 5 # Prerequisites not met (Docker, Git not installed)
|
|
36
|
+
EXIT_GOVERNANCE = 6 # Blocked by governance policy
|
|
37
|
+
|
|
38
|
+
# Cancellation (SIGINT convention)
|
|
39
|
+
EXIT_CANCELLED = 130 # User cancelled operation (SIGINT)
|
|
40
|
+
|
|
41
|
+
# Deprecated aliases - prefer specific codes above
|
|
42
|
+
# These exist for backward compatibility and will be removed in a future version
|
|
43
|
+
EXIT_ERROR = EXIT_NOT_FOUND # DEPRECATED: Use EXIT_NOT_FOUND or a specific code
|
|
44
|
+
EXIT_VALIDATION = EXIT_TOOL # DEPRECATED: Use EXIT_TOOL instead
|
|
45
|
+
EXIT_INTERNAL = EXIT_PREREQ # DEPRECATED: Use EXIT_PREREQ instead
|
|
46
|
+
|
|
47
|
+
# Map exception types to exit codes (for json_command decorator)
|
|
48
|
+
# Note: Import from errors module only when needed to avoid circular imports
|
|
49
|
+
EXIT_CODE_MAP = {
|
|
50
|
+
# Tool errors (external commands failed)
|
|
51
|
+
"ToolError": EXIT_TOOL,
|
|
52
|
+
"WorkspaceError": EXIT_TOOL,
|
|
53
|
+
"WorkspaceNotFoundError": EXIT_TOOL,
|
|
54
|
+
"NotAGitRepoError": EXIT_TOOL,
|
|
55
|
+
"GitWorktreeError": EXIT_TOOL,
|
|
56
|
+
# Config errors
|
|
57
|
+
"ConfigError": EXIT_CONFIG,
|
|
58
|
+
"ProfileNotFoundError": EXIT_CONFIG,
|
|
59
|
+
# Validation errors (treated as tool errors)
|
|
60
|
+
"ValidationError": EXIT_TOOL,
|
|
61
|
+
# Governance errors
|
|
62
|
+
"PolicyViolationError": EXIT_GOVERNANCE,
|
|
63
|
+
# Prerequisite errors
|
|
64
|
+
"PrerequisiteError": EXIT_PREREQ,
|
|
65
|
+
"DockerNotFoundError": EXIT_PREREQ,
|
|
66
|
+
"GitNotFoundError": EXIT_PREREQ,
|
|
67
|
+
# Internal errors (treated as prerequisite/system errors)
|
|
68
|
+
"InternalError": EXIT_PREREQ,
|
|
69
|
+
# Usage errors
|
|
70
|
+
"UsageError": EXIT_USAGE,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_exit_code_for_exception(exc: Exception) -> int:
|
|
75
|
+
"""Return the appropriate exit code for an exception type.
|
|
76
|
+
|
|
77
|
+
Walk up the exception's MRO to find a matching type in EXIT_CODE_MAP.
|
|
78
|
+
Fall back to EXIT_NOT_FOUND if no specific mapping exists.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
exc: The exception instance to map.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The standardized exit code for the exception type.
|
|
85
|
+
"""
|
|
86
|
+
for cls in type(exc).__mro__:
|
|
87
|
+
if cls.__name__ in EXIT_CODE_MAP:
|
|
88
|
+
return EXIT_CODE_MAP[cls.__name__]
|
|
89
|
+
|
|
90
|
+
# Fallback for unmapped exceptions
|
|
91
|
+
return EXIT_NOT_FOUND
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Workspace resolution domain types.
|
|
2
|
+
|
|
3
|
+
This module contains pure data types for workspace resolution results.
|
|
4
|
+
These types are used by the services layer to communicate resolution outcomes.
|
|
5
|
+
|
|
6
|
+
Key concepts:
|
|
7
|
+
- WR (workspace_root): The stable identity for sessions (git root or .scc.yaml parent)
|
|
8
|
+
- ED (entry_dir): Where the user invoked from (preserved for container cwd)
|
|
9
|
+
- MR (mount_root): Host path to mount into the container (may expand for worktrees)
|
|
10
|
+
- CW (container_workdir): The working directory inside the container (mirrored host path)
|
|
11
|
+
|
|
12
|
+
All paths in ResolverResult are absolute and resolved (symlinks expanded).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ResolverResult:
|
|
23
|
+
"""Complete workspace resolution output.
|
|
24
|
+
|
|
25
|
+
All paths are absolute and resolved (symlinks expanded via resolve()).
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
workspace_root: WR - stable identity for sessions (git root or .scc.yaml parent)
|
|
29
|
+
entry_dir: ED - where user invoked from
|
|
30
|
+
mount_root: MR - host path mounted into container
|
|
31
|
+
container_workdir: CW - mirrored host absolute path inside container
|
|
32
|
+
is_auto_detected: True if found via git/.scc.yaml (not explicit --workspace)
|
|
33
|
+
is_suspicious: Whether WR is in a suspicious location (home, /tmp, etc.)
|
|
34
|
+
is_mount_expanded: Whether MR was expanded for worktree gitdir access
|
|
35
|
+
reason: Debug explanation of how resolution was performed
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
workspace_root: Path # WR - stable identity for sessions
|
|
39
|
+
entry_dir: Path # ED - where user invoked from
|
|
40
|
+
mount_root: Path # MR - host path mounted into container
|
|
41
|
+
container_workdir: str # CW - mirrored host absolute path
|
|
42
|
+
is_auto_detected: bool # True if found via git/.scc.yaml
|
|
43
|
+
is_suspicious: bool # Whether WR is in a suspicious location
|
|
44
|
+
is_mount_expanded: bool = False # Whether MR was expanded for worktree
|
|
45
|
+
reason: str = "" # Debug explanation
|
|
46
|
+
|
|
47
|
+
def is_auto_eligible(self) -> bool:
|
|
48
|
+
"""Whether this result is eligible for auto-launch.
|
|
49
|
+
|
|
50
|
+
Auto-launch requires:
|
|
51
|
+
1. Workspace was auto-detected (not from explicit --workspace arg)
|
|
52
|
+
2. Workspace is not in a suspicious location (home, /tmp, system dirs)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if workspace can be auto-launched without user confirmation.
|
|
56
|
+
"""
|
|
57
|
+
return self.is_auto_detected and not self.is_suspicious
|
scc_cli/deprecation.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Provide deprecation warning infrastructure.
|
|
2
|
+
|
|
3
|
+
Provide consistent deprecation warnings that respect output modes.
|
|
4
|
+
Suppress warnings in JSON mode to maintain clean machine output.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from scc_cli.deprecation import warn_deprecated
|
|
8
|
+
|
|
9
|
+
# In command handler:
|
|
10
|
+
warn_deprecated("old-cmd", "new-cmd", remove_version="2.0")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from .output_mode import is_json_mode
|
|
18
|
+
|
|
19
|
+
# Stderr console for deprecation warnings
|
|
20
|
+
_stderr_console = Console(stderr=True)
|
|
21
|
+
|
|
22
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
# Deprecation Warnings
|
|
24
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def warn_deprecated(
|
|
28
|
+
old_cmd: str,
|
|
29
|
+
new_cmd: str,
|
|
30
|
+
remove_version: str = "2.0",
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Print deprecation warning to stderr.
|
|
33
|
+
|
|
34
|
+
Warnings are suppressed when:
|
|
35
|
+
- JSON output mode is active (clean machine output)
|
|
36
|
+
- SCC_NO_DEPRECATION_WARN=1 environment variable is set
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
old_cmd: The deprecated command/option name
|
|
40
|
+
new_cmd: The replacement command/option name
|
|
41
|
+
remove_version: The version when old_cmd will be removed
|
|
42
|
+
"""
|
|
43
|
+
# Suppress in JSON mode for clean machine output
|
|
44
|
+
if is_json_mode():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Allow users to suppress deprecation warnings
|
|
48
|
+
if os.environ.get("SCC_NO_DEPRECATION_WARN") == "1":
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
_stderr_console.print(
|
|
52
|
+
f"[yellow]DEPRECATION:[/yellow] '{old_cmd}' is deprecated. "
|
|
53
|
+
f"Use '{new_cmd}' instead. Will be removed in v{remove_version}."
|
|
54
|
+
)
|
scc_cli/deps.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provide dependency detection and installation for project workspaces.
|
|
3
|
+
|
|
4
|
+
Offer opt-in dependency installation that:
|
|
5
|
+
- Is opt-in (--install-deps flag)
|
|
6
|
+
- Never blocks scc start by default
|
|
7
|
+
- Supports strict mode for CI/automation that needs hard failures
|
|
8
|
+
|
|
9
|
+
Supported package managers:
|
|
10
|
+
- JavaScript: npm, pnpm, yarn, bun
|
|
11
|
+
- Python: poetry, uv, pip
|
|
12
|
+
- Java: maven, gradle
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
# Exception Classes
|
|
20
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DependencyInstallError(Exception):
|
|
24
|
+
"""Raised when dependency installation fails in strict mode."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, package_manager: str, message: str):
|
|
27
|
+
self.package_manager = package_manager
|
|
28
|
+
self.message = message
|
|
29
|
+
super().__init__(f"{package_manager}: {message}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
# Package Manager Detection
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
# Detection order matters - lock files take priority over manifest files
|
|
37
|
+
DETECTION_ORDER = [
|
|
38
|
+
# JavaScript lock files (priority)
|
|
39
|
+
("pnpm-lock.yaml", "pnpm"),
|
|
40
|
+
("yarn.lock", "yarn"),
|
|
41
|
+
("bun.lockb", "bun"),
|
|
42
|
+
("package-lock.json", "npm"),
|
|
43
|
+
# Python lock files (priority)
|
|
44
|
+
("uv.lock", "uv"),
|
|
45
|
+
("poetry.lock", "poetry"),
|
|
46
|
+
# Java build files
|
|
47
|
+
("pom.xml", "maven"),
|
|
48
|
+
("build.gradle.kts", "gradle"),
|
|
49
|
+
("build.gradle", "gradle"),
|
|
50
|
+
# Fallback manifest files
|
|
51
|
+
("package.json", "npm"), # JS fallback
|
|
52
|
+
("pyproject.toml", "pip"), # Python fallback
|
|
53
|
+
("requirements.txt", "pip"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_package_manager(workspace: Path) -> str | None:
|
|
58
|
+
"""Detect the package manager from project files.
|
|
59
|
+
|
|
60
|
+
Base detection on the presence of lock files and manifest files.
|
|
61
|
+
Give lock files priority over manifest files.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
workspace: Path to the project workspace
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Package manager name or None if not detected.
|
|
68
|
+
Possible values: 'npm', 'pnpm', 'yarn', 'bun', 'poetry', 'uv', 'pip', 'maven', 'gradle'
|
|
69
|
+
"""
|
|
70
|
+
for filename, package_manager in DETECTION_ORDER:
|
|
71
|
+
if (workspace / filename).exists():
|
|
72
|
+
return package_manager
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
78
|
+
# Install Commands
|
|
79
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
INSTALL_COMMANDS = {
|
|
82
|
+
# JavaScript
|
|
83
|
+
"npm": ["npm", "install"],
|
|
84
|
+
"pnpm": ["pnpm", "install"],
|
|
85
|
+
"yarn": ["yarn", "install"],
|
|
86
|
+
"bun": ["bun", "install"],
|
|
87
|
+
# Python
|
|
88
|
+
"poetry": ["poetry", "install"],
|
|
89
|
+
"uv": ["uv", "sync"],
|
|
90
|
+
"pip": ["pip", "install", "-r", "requirements.txt"],
|
|
91
|
+
# Java
|
|
92
|
+
"maven": ["mvn", "install", "-DskipTests"],
|
|
93
|
+
"gradle": ["gradle", "dependencies"],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_install_command(package_manager: str) -> list[str] | None:
|
|
98
|
+
"""Return the install command for a package manager.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
package_manager: Name of the package manager
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of command arguments or None if unknown
|
|
105
|
+
"""
|
|
106
|
+
return INSTALL_COMMANDS.get(package_manager)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
110
|
+
# Dependency Installation
|
|
111
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def install_dependencies(
|
|
115
|
+
workspace: Path,
|
|
116
|
+
package_manager: str,
|
|
117
|
+
strict: bool = False,
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""Run the dependency installation command.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
workspace: Path to project workspace
|
|
123
|
+
package_manager: Detected package manager name
|
|
124
|
+
strict: If True, raise on failure. If False (default), warn and continue.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if install succeeded, False if failed (only when strict=False)
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
DependencyInstallError: If strict=True and installation fails
|
|
131
|
+
"""
|
|
132
|
+
cmd = get_install_command(package_manager)
|
|
133
|
+
|
|
134
|
+
if cmd is None:
|
|
135
|
+
if strict:
|
|
136
|
+
raise DependencyInstallError(package_manager, "Unknown package manager")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
result = subprocess.run(
|
|
141
|
+
cmd,
|
|
142
|
+
cwd=workspace,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if result.returncode == 0:
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# Installation failed
|
|
151
|
+
error_msg = result.stderr or result.stdout or "Unknown error"
|
|
152
|
+
if strict:
|
|
153
|
+
raise DependencyInstallError(package_manager, f"Command failed: {error_msg}")
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
except FileNotFoundError:
|
|
158
|
+
# Package manager not installed
|
|
159
|
+
if strict:
|
|
160
|
+
raise DependencyInstallError(
|
|
161
|
+
package_manager,
|
|
162
|
+
f"'{cmd[0]}' not found. Is {package_manager} installed?",
|
|
163
|
+
)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def auto_install_dependencies(workspace: Path, strict: bool = False) -> bool:
|
|
168
|
+
"""Detect the package manager and install dependencies.
|
|
169
|
+
|
|
170
|
+
Combine detection and installation as a convenience function.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
workspace: Path to project workspace
|
|
174
|
+
strict: If True, raise on failure. If False (default), warn and continue.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if install succeeded, False if failed or no package manager detected
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
DependencyInstallError: If strict=True and installation fails
|
|
181
|
+
"""
|
|
182
|
+
package_manager = detect_package_manager(workspace)
|
|
183
|
+
|
|
184
|
+
if package_manager is None:
|
|
185
|
+
if strict:
|
|
186
|
+
raise DependencyInstallError("unknown", "No package manager detected")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
return install_dependencies(workspace, package_manager, strict=strict)
|