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.

Files changed (153) 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 +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. 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)