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.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +259 -0
- scc_cli/cli_admin.py +706 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/exit_codes.py +55 -0
- scc_cli/git.py +1521 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/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 +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +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/sessions.py +425 -0
- scc_cli/setup.py +588 -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 +382 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +538 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +675 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/console.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Centralized console infrastructure with capability detection.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- TerminalCaps: Unified capability detection computed once per command
|
|
5
|
+
- Gated console factory respecting NO_COLOR, JSON mode, TTY state
|
|
6
|
+
- PlainTextStatus: Graceful degradation for non-TTY environments
|
|
7
|
+
- err_line(): Centralized stderr output (uses sys.stderr.write, not print)
|
|
8
|
+
|
|
9
|
+
The key rule: TerminalCaps.detect() is called ONCE at command entry point,
|
|
10
|
+
then passed down to all functions. No scattered TTY checks.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# At command entry:
|
|
14
|
+
caps = TerminalCaps.detect(json_mode=ctx.json_mode)
|
|
15
|
+
|
|
16
|
+
# In functions:
|
|
17
|
+
with human_status("Processing...", caps) as status:
|
|
18
|
+
do_work()
|
|
19
|
+
|
|
20
|
+
# For direct stderr output (use sparingly):
|
|
21
|
+
err_line("→ Starting operation...")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
from contextlib import contextmanager
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol
|
|
31
|
+
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Generator
|
|
36
|
+
from typing import TextIO
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
40
|
+
# Centralized stderr output - single choke point for plain text
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def err_line(text: str) -> None:
|
|
45
|
+
"""Write a line to stderr.
|
|
46
|
+
|
|
47
|
+
This is the single choke point for plain text stderr output.
|
|
48
|
+
Uses sys.stderr.write() directly to avoid print() entirely.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
text: The text to write to stderr (newline appended automatically).
|
|
52
|
+
"""
|
|
53
|
+
sys.stderr.write(text + "\n")
|
|
54
|
+
sys.stderr.flush() # Ensure immediate visibility in CI/buffered environments
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
# Stream-aware capability detection
|
|
59
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _supports_colors_for_stream(stream: TextIO) -> bool:
|
|
63
|
+
"""Check if colors should be enabled for a specific stream.
|
|
64
|
+
|
|
65
|
+
CRITICAL: This checks the SPECIFIC stream's TTY status, not stdout.
|
|
66
|
+
With our stderr contract (stdout=JSON, stderr=human UI), stdout may be
|
|
67
|
+
piped while stderr is still a TTY. Checking stdout would incorrectly
|
|
68
|
+
disable colors in `scc start --json | jq` scenarios.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
stream: The stream to check (typically sys.stderr for Rich output).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if colors should be enabled for this stream.
|
|
75
|
+
"""
|
|
76
|
+
if os.environ.get("NO_COLOR"):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
if os.environ.get("FORCE_COLOR"):
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
return hasattr(stream, "isatty") and stream.isatty()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _is_ci_environment() -> bool:
|
|
86
|
+
"""Detect if running in a CI environment.
|
|
87
|
+
|
|
88
|
+
Checks common CI environment variables set by various CI systems:
|
|
89
|
+
- CI (GitHub Actions, GitLab CI, CircleCI, Travis CI, etc.)
|
|
90
|
+
- CONTINUOUS_INTEGRATION (Travis CI)
|
|
91
|
+
- BUILD_NUMBER (Jenkins)
|
|
92
|
+
- GITHUB_ACTIONS (GitHub Actions specific)
|
|
93
|
+
- GITLAB_CI (GitLab CI specific)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if CI environment detected, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
ci_vars = ["CI", "CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "GITHUB_ACTIONS", "GITLAB_CI"]
|
|
99
|
+
|
|
100
|
+
for var in ci_vars:
|
|
101
|
+
value = os.getenv(var, "").lower()
|
|
102
|
+
if value in ("1", "true", "yes"):
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _supports_unicode_for_stream(stream: TextIO) -> bool:
|
|
109
|
+
"""Check if a stream supports Unicode characters.
|
|
110
|
+
|
|
111
|
+
This is stream-aware (matching _supports_colors_for_stream pattern)
|
|
112
|
+
to handle cases where different streams have different encodings.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
stream: The stream to check (typically sys.stderr for Rich output).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if UTF-8 encoding is available on the stream.
|
|
119
|
+
"""
|
|
120
|
+
encoding = getattr(stream, "encoding", None) or ""
|
|
121
|
+
if encoding.lower() in ("utf-8", "utf8"):
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
# Check locale environment variables as fallback (LC_ALL > LC_CTYPE > LANG)
|
|
125
|
+
locale_var = (
|
|
126
|
+
os.environ.get("LC_ALL") or os.environ.get("LC_CTYPE") or os.environ.get("LANG", "")
|
|
127
|
+
)
|
|
128
|
+
return "utf-8" in locale_var.lower() or "utf8" in locale_var.lower()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
132
|
+
# Terminal Capabilities Dataclass
|
|
133
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class TerminalCaps:
|
|
138
|
+
"""All terminal capability checks in one place.
|
|
139
|
+
|
|
140
|
+
This dataclass is IMMUTABLE (frozen=True) and should be computed ONCE
|
|
141
|
+
at command entry point, then passed down to all functions.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
can_render: Whether Rich UI (panels, tables) can display.
|
|
145
|
+
True when stderr is TTY and not in JSON mode.
|
|
146
|
+
can_animate: Whether spinners/progress bars can display.
|
|
147
|
+
True when can_render AND TERM != dumb.
|
|
148
|
+
can_prompt: Whether interactive prompts are allowed.
|
|
149
|
+
True when stdin is TTY, not JSON, not CI, not --no-interactive.
|
|
150
|
+
colors: Whether colors should be enabled for stderr output.
|
|
151
|
+
unicode: Whether Unicode characters are supported.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
can_render: bool
|
|
155
|
+
can_animate: bool
|
|
156
|
+
can_prompt: bool
|
|
157
|
+
colors: bool
|
|
158
|
+
unicode: bool
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def detect(
|
|
162
|
+
cls,
|
|
163
|
+
json_mode: bool = False,
|
|
164
|
+
no_interactive: bool = False,
|
|
165
|
+
) -> TerminalCaps:
|
|
166
|
+
"""Detect all capabilities once at command entry.
|
|
167
|
+
|
|
168
|
+
This method should be called ONCE at the beginning of a command,
|
|
169
|
+
and the resulting TerminalCaps object passed to all functions.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
json_mode: Whether --json flag is set.
|
|
173
|
+
no_interactive: Whether --no-interactive flag is set.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
TerminalCaps with all capabilities computed.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
caps = TerminalCaps.detect(json_mode=args.json)
|
|
180
|
+
with human_status("Processing...", caps):
|
|
181
|
+
do_work()
|
|
182
|
+
"""
|
|
183
|
+
stderr_tty = sys.stderr.isatty()
|
|
184
|
+
stdin_tty = sys.stdin.isatty()
|
|
185
|
+
# Check for dumb, unknown, or empty TERM values
|
|
186
|
+
term = (os.environ.get("TERM") or "").lower()
|
|
187
|
+
term_dumb = term in ("dumb", "unknown", "")
|
|
188
|
+
|
|
189
|
+
# can_render: Rich UI (panels, tables) can display
|
|
190
|
+
can_render = stderr_tty and not json_mode
|
|
191
|
+
|
|
192
|
+
return cls(
|
|
193
|
+
can_render=can_render,
|
|
194
|
+
can_animate=can_render and not term_dumb,
|
|
195
|
+
# can_prompt requires BOTH stdin AND stderr to be TTYs:
|
|
196
|
+
# - stdin: for reading user input
|
|
197
|
+
# - stderr: for displaying prompts (Rich prompts go to stderr)
|
|
198
|
+
can_prompt=stdin_tty
|
|
199
|
+
and stderr_tty
|
|
200
|
+
and not json_mode
|
|
201
|
+
and not _is_ci_environment()
|
|
202
|
+
and not no_interactive,
|
|
203
|
+
colors=_supports_colors_for_stream(sys.stderr),
|
|
204
|
+
unicode=_supports_unicode_for_stream(sys.stderr),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def for_json_mode(cls) -> TerminalCaps:
|
|
209
|
+
"""Create TerminalCaps for JSON output mode.
|
|
210
|
+
|
|
211
|
+
Convenience method that returns capabilities with all rendering disabled.
|
|
212
|
+
Unicode detection still uses stderr in case internal strings need formatting.
|
|
213
|
+
"""
|
|
214
|
+
return cls(
|
|
215
|
+
can_render=False,
|
|
216
|
+
can_animate=False,
|
|
217
|
+
can_prompt=False,
|
|
218
|
+
colors=False,
|
|
219
|
+
unicode=_supports_unicode_for_stream(sys.stderr),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def for_testing(
|
|
224
|
+
cls,
|
|
225
|
+
*,
|
|
226
|
+
can_render: bool = True,
|
|
227
|
+
can_animate: bool = True,
|
|
228
|
+
can_prompt: bool = True,
|
|
229
|
+
colors: bool = True,
|
|
230
|
+
unicode: bool = True,
|
|
231
|
+
) -> TerminalCaps:
|
|
232
|
+
"""Create TerminalCaps with explicit values for testing.
|
|
233
|
+
|
|
234
|
+
Use this in tests to control capabilities without environment dependencies.
|
|
235
|
+
"""
|
|
236
|
+
return cls(
|
|
237
|
+
can_render=can_render,
|
|
238
|
+
can_animate=can_animate,
|
|
239
|
+
can_prompt=can_prompt,
|
|
240
|
+
colors=colors,
|
|
241
|
+
unicode=unicode,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
246
|
+
# Status Objects - Unified interface for all status display modes
|
|
247
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class StatusLike(Protocol):
|
|
251
|
+
"""Protocol for status objects yielded by human_status().
|
|
252
|
+
|
|
253
|
+
All status implementations (Rich Status, PlainTextStatus, NullStatus)
|
|
254
|
+
support this interface, enabling type-safe usage with autocomplete.
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
with human_status("Working...", caps) as status:
|
|
258
|
+
status.update("Step 1") # IDE autocomplete works here
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def update(self, message: str) -> None:
|
|
262
|
+
"""Update the displayed status message."""
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class NullStatus:
|
|
267
|
+
"""No-op status object that provides the same interface as Rich Status.
|
|
268
|
+
|
|
269
|
+
Used when status display is disabled in JSON mode or other contexts
|
|
270
|
+
where we don't want any output. For static text mode that needs
|
|
271
|
+
completion messages, use StaticRichStatus instead.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def update(self, message: str) -> None:
|
|
275
|
+
"""No-op update method for interface compatibility.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
message: Ignored - no display update occurs.
|
|
279
|
+
"""
|
|
280
|
+
pass # Intentionally empty - no output wanted
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class StaticRichStatus:
|
|
284
|
+
"""Static status indicator using Rich formatting (no animation).
|
|
285
|
+
|
|
286
|
+
Used when can_render=True but can_animate=False (e.g., TERM=dumb).
|
|
287
|
+
Provides start/completion messages using Rich markup, preventing
|
|
288
|
+
CI logs from appearing "hung" when animations are disabled.
|
|
289
|
+
|
|
290
|
+
Output format:
|
|
291
|
+
[dim]{message}...[/dim] (on enter)
|
|
292
|
+
[green]✓ {message} done[/green] (on success)
|
|
293
|
+
[red]✗ {message} failed[/red] (on error)
|
|
294
|
+
|
|
295
|
+
This ensures static mode has the same completion semantics as
|
|
296
|
+
PlainTextStatus, but with Rich formatting.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, message: str, console: Console) -> None:
|
|
300
|
+
"""Initialize StaticRichStatus.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
message: The status message to display.
|
|
304
|
+
console: Rich Console to write output to.
|
|
305
|
+
"""
|
|
306
|
+
self.message = message
|
|
307
|
+
self.console = console
|
|
308
|
+
# Import from theme to get Unicode-aware indicators
|
|
309
|
+
from .theme import Indicators
|
|
310
|
+
|
|
311
|
+
self.check = Indicators.get("PASS") # "✓" or "OK"
|
|
312
|
+
self.cross = Indicators.get("FAIL") # "✗" or "FAIL"
|
|
313
|
+
|
|
314
|
+
def update(self, message: str) -> None:
|
|
315
|
+
"""Update the status message (prints new message if changed).
|
|
316
|
+
|
|
317
|
+
Only prints if the message actually changed to prevent log spam
|
|
318
|
+
in CI environments where loops might call update() frequently.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
message: The new status message to display.
|
|
322
|
+
"""
|
|
323
|
+
if message != self.message:
|
|
324
|
+
self.message = message
|
|
325
|
+
self.console.print(f"[dim]{message}...[/dim]")
|
|
326
|
+
|
|
327
|
+
def __enter__(self) -> StaticRichStatus:
|
|
328
|
+
"""Print start message to console."""
|
|
329
|
+
self.console.print(f"[dim]{self.message}...[/dim]")
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def __exit__(
|
|
333
|
+
self,
|
|
334
|
+
exc_type: type[BaseException] | None,
|
|
335
|
+
exc_val: BaseException | None,
|
|
336
|
+
exc_tb: Any,
|
|
337
|
+
) -> Literal[False]:
|
|
338
|
+
"""Print completion or failure message to console."""
|
|
339
|
+
if exc_val:
|
|
340
|
+
self.console.print(f"[red]{self.cross} {self.message} failed[/red]")
|
|
341
|
+
# Print brief error summary (one line)
|
|
342
|
+
error_str = str(exc_val)
|
|
343
|
+
if error_str:
|
|
344
|
+
self.console.print(f"[dim] Error: {error_str}[/dim]")
|
|
345
|
+
else:
|
|
346
|
+
self.console.print(f"[green]{self.check} {self.message} done[/green]")
|
|
347
|
+
return False # Don't suppress exceptions
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class PlainTextStatus:
|
|
351
|
+
"""Plain text status indicator for non-TTY environments.
|
|
352
|
+
|
|
353
|
+
When Rich spinners can't be used (non-TTY, JSON mode), this class provides
|
|
354
|
+
a simple text-based alternative that shows progress without silence.
|
|
355
|
+
|
|
356
|
+
Output format:
|
|
357
|
+
→ {message}... (on enter)
|
|
358
|
+
✓ {message} done (on success)
|
|
359
|
+
✗ {message} failed (on error)
|
|
360
|
+
|
|
361
|
+
This prevents CI logs from appearing "hung" when Rich output is disabled.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def __init__(self, message: str, *, use_unicode: bool = True) -> None:
|
|
365
|
+
"""Initialize PlainTextStatus.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
message: The status message to display.
|
|
369
|
+
use_unicode: Whether to use Unicode symbols (→, ✓, ✗) or ASCII (>, OK, FAIL).
|
|
370
|
+
"""
|
|
371
|
+
self.message = message
|
|
372
|
+
self.arrow = "→" if use_unicode else ">"
|
|
373
|
+
self.check = "✓" if use_unicode else "OK"
|
|
374
|
+
self.cross = "✗" if use_unicode else "FAIL"
|
|
375
|
+
|
|
376
|
+
def update(self, message: str) -> None:
|
|
377
|
+
"""Update the status message (compatibility with Rich Status interface).
|
|
378
|
+
|
|
379
|
+
Only prints if the message actually changed to prevent log spam
|
|
380
|
+
in CI environments where loops might call update() frequently.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
message: The new status message to display.
|
|
384
|
+
"""
|
|
385
|
+
if message != self.message:
|
|
386
|
+
self.message = message
|
|
387
|
+
err_line(f"{self.arrow} {message}...")
|
|
388
|
+
|
|
389
|
+
def __enter__(self) -> PlainTextStatus:
|
|
390
|
+
"""Print start message to stderr."""
|
|
391
|
+
err_line(f"{self.arrow} {self.message}...")
|
|
392
|
+
return self
|
|
393
|
+
|
|
394
|
+
def __exit__(
|
|
395
|
+
self,
|
|
396
|
+
exc_type: type[BaseException] | None,
|
|
397
|
+
exc_val: BaseException | None,
|
|
398
|
+
exc_tb: Any,
|
|
399
|
+
) -> Literal[False]:
|
|
400
|
+
"""Print completion or failure message to stderr."""
|
|
401
|
+
if exc_val:
|
|
402
|
+
err_line(f"{self.cross} {self.message} failed")
|
|
403
|
+
# Print brief error summary (one line)
|
|
404
|
+
error_str = str(exc_val)
|
|
405
|
+
if error_str:
|
|
406
|
+
err_line(f" Error: {error_str}")
|
|
407
|
+
else:
|
|
408
|
+
err_line(f"{self.check} {self.message} done")
|
|
409
|
+
return False # Don't suppress exceptions
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
413
|
+
# Gated Console Factory
|
|
414
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
415
|
+
|
|
416
|
+
# Singleton consoles - created once, reused everywhere
|
|
417
|
+
_console: Console | None = None
|
|
418
|
+
_err_console: Console | None = None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _resolve_force_terminal() -> bool | None:
|
|
422
|
+
"""Resolve force_terminal value from environment variables.
|
|
423
|
+
|
|
424
|
+
Respects the standard FORCE_COLOR and NO_COLOR environment variables:
|
|
425
|
+
- NO_COLOR takes precedence: disables colors AND terminal features
|
|
426
|
+
- FORCE_COLOR enables terminal features even when not a TTY
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
False if NO_COLOR is set (disable terminal features).
|
|
430
|
+
True if FORCE_COLOR is set (and NO_COLOR is not).
|
|
431
|
+
None otherwise (let Rich auto-detect).
|
|
432
|
+
"""
|
|
433
|
+
if os.environ.get("NO_COLOR"):
|
|
434
|
+
return False # Explicitly disable terminal features
|
|
435
|
+
if os.environ.get("FORCE_COLOR"):
|
|
436
|
+
return True # Force terminal features on
|
|
437
|
+
return None # Auto-detect
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_console() -> Console:
|
|
441
|
+
"""Get the gated stdout console.
|
|
442
|
+
|
|
443
|
+
This console respects NO_COLOR/FORCE_COLOR and is primarily for compatibility.
|
|
444
|
+
Most output should go to stderr via get_err_console().
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Console configured for stdout.
|
|
448
|
+
"""
|
|
449
|
+
global _console
|
|
450
|
+
if _console is None:
|
|
451
|
+
_console = Console(
|
|
452
|
+
force_terminal=_resolve_force_terminal(),
|
|
453
|
+
no_color=bool(os.environ.get("NO_COLOR")),
|
|
454
|
+
)
|
|
455
|
+
return _console
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def get_err_console() -> Console:
|
|
459
|
+
"""Get the gated stderr console.
|
|
460
|
+
|
|
461
|
+
This is the PRIMARY console for all human-readable Rich output.
|
|
462
|
+
With our stderr contract:
|
|
463
|
+
- stdout: JSON only (or nothing in human mode)
|
|
464
|
+
- stderr: All Rich panels, spinners, prompts
|
|
465
|
+
|
|
466
|
+
Applies the SCC theme for semantic style names (scc.success, scc.error, etc.).
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Console configured for stderr with SCC theme applied.
|
|
470
|
+
"""
|
|
471
|
+
global _err_console
|
|
472
|
+
if _err_console is None:
|
|
473
|
+
# Lazy import to keep module load fast
|
|
474
|
+
from scc_cli.theme import get_scc_theme
|
|
475
|
+
|
|
476
|
+
_err_console = Console(
|
|
477
|
+
stderr=True,
|
|
478
|
+
force_terminal=_resolve_force_terminal(),
|
|
479
|
+
no_color=bool(os.environ.get("NO_COLOR")),
|
|
480
|
+
theme=get_scc_theme(),
|
|
481
|
+
)
|
|
482
|
+
return _err_console
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _reset_consoles_for_testing() -> None:
|
|
486
|
+
"""Reset console singletons for test isolation.
|
|
487
|
+
|
|
488
|
+
Call this in test fixtures when toggling environment variables
|
|
489
|
+
(NO_COLOR, FORCE_COLOR) to ensure fresh console instances that
|
|
490
|
+
respect the new environment state.
|
|
491
|
+
|
|
492
|
+
WARNING: Only use in tests! Production code should never call this.
|
|
493
|
+
"""
|
|
494
|
+
global _console, _err_console
|
|
495
|
+
_console = None
|
|
496
|
+
_err_console = None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
500
|
+
# Human Status Wrapper
|
|
501
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@contextmanager
|
|
505
|
+
def human_status(
|
|
506
|
+
message: str,
|
|
507
|
+
caps: TerminalCaps,
|
|
508
|
+
*,
|
|
509
|
+
spinner: str = "dots",
|
|
510
|
+
) -> Generator[StatusLike, None, None]:
|
|
511
|
+
"""Context manager for status display with graceful degradation.
|
|
512
|
+
|
|
513
|
+
When can_animate is True, displays a Rich spinner.
|
|
514
|
+
When can_animate is False but can_render is True, displays static text
|
|
515
|
+
with start/completion messages (using StaticRichStatus).
|
|
516
|
+
When can_render is False, uses PlainTextStatus for minimal feedback.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
message: The status message to display.
|
|
520
|
+
caps: Terminal capabilities (from TerminalCaps.detect()).
|
|
521
|
+
spinner: Rich spinner name (default "dots").
|
|
522
|
+
|
|
523
|
+
Yields:
|
|
524
|
+
A StatusLike object: Rich Status (animating), StaticRichStatus (static),
|
|
525
|
+
or PlainTextStatus (non-TTY).
|
|
526
|
+
|
|
527
|
+
Example:
|
|
528
|
+
with human_status("Processing files", caps) as status:
|
|
529
|
+
for file in files:
|
|
530
|
+
process(file)
|
|
531
|
+
"""
|
|
532
|
+
if caps.can_animate:
|
|
533
|
+
# Full Rich spinner
|
|
534
|
+
console = get_err_console()
|
|
535
|
+
with console.status(message, spinner=spinner) as status:
|
|
536
|
+
yield status
|
|
537
|
+
elif caps.can_render:
|
|
538
|
+
# Static text (no animation, but Rich formatting works)
|
|
539
|
+
# Use StaticRichStatus for start/completion semantics matching PlainTextStatus
|
|
540
|
+
console = get_err_console()
|
|
541
|
+
with StaticRichStatus(message, console) as status:
|
|
542
|
+
yield status
|
|
543
|
+
else:
|
|
544
|
+
# Plain text fallback for non-TTY
|
|
545
|
+
with PlainTextStatus(message, use_unicode=caps.unicode) as status:
|
|
546
|
+
yield status
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
550
|
+
# No-op context manager for JSON mode
|
|
551
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@contextmanager
|
|
555
|
+
def silent_status() -> Generator[None, None, None]:
|
|
556
|
+
"""No-op context manager for JSON mode.
|
|
557
|
+
|
|
558
|
+
Use this ONLY when json_mode=True and you need a status context
|
|
559
|
+
but all output should be suppressed. For non-JSON modes, use
|
|
560
|
+
human_status() which handles graceful degradation automatically.
|
|
561
|
+
"""
|
|
562
|
+
yield None
|
scc_cli/constants.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend-specific constants for SCC-CLI.
|
|
3
|
+
|
|
4
|
+
Centralized location for all backend-specific values that identify the
|
|
5
|
+
AI coding assistant being sandboxed. Currently supports Claude Code.
|
|
6
|
+
|
|
7
|
+
This module enables future extensibility to support other AI coding CLIs
|
|
8
|
+
(e.g., Codex, Gemini) by providing a single location to update when
|
|
9
|
+
adding new backend support.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from scc_cli.constants import AGENT_NAME, SANDBOX_IMAGE
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
# Agent Configuration
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
# The agent binary name inside the container
|
|
20
|
+
# This is passed to `docker sandbox run` and `docker exec`
|
|
21
|
+
AGENT_NAME = "claude"
|
|
22
|
+
|
|
23
|
+
# The Docker sandbox template image
|
|
24
|
+
SANDBOX_IMAGE = "docker/sandbox-templates:claude-code"
|
|
25
|
+
|
|
26
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
# Credential & Storage Paths
|
|
28
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
# Directory name inside user home for agent config/credentials
|
|
31
|
+
# Maps to ~/.claude/ on host and /home/agent/.claude/ in container
|
|
32
|
+
AGENT_CONFIG_DIR = ".claude"
|
|
33
|
+
|
|
34
|
+
# Docker volume for persistent sandbox data
|
|
35
|
+
SANDBOX_DATA_VOLUME = "docker-claude-sandbox-data"
|
|
36
|
+
|
|
37
|
+
# Mount point inside the container for the data volume
|
|
38
|
+
SANDBOX_DATA_MOUNT = "/mnt/claude-data"
|
|
39
|
+
|
|
40
|
+
# Safety net policy injection
|
|
41
|
+
# This is the filename for the extracted security.safety_net blob (NOT full org config)
|
|
42
|
+
SAFETY_NET_POLICY_FILENAME = "effective_policy.json"
|
|
43
|
+
|
|
44
|
+
# Credential file paths (relative to agent home directory)
|
|
45
|
+
CREDENTIAL_PATHS = (
|
|
46
|
+
f"/home/agent/{AGENT_CONFIG_DIR}/.credentials.json",
|
|
47
|
+
f"/home/agent/{AGENT_CONFIG_DIR}/credentials.json",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# OAuth credential key in credentials file
|
|
51
|
+
OAUTH_CREDENTIAL_KEY = "claudeAiOauth"
|
|
52
|
+
|
|
53
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
# Git Integration
|
|
55
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
# Branch prefix for worktrees created by SCC
|
|
58
|
+
WORKTREE_BRANCH_PREFIX = "claude/"
|
|
59
|
+
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
# Default Plugin Marketplace
|
|
62
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
# Default GitHub repo for plugins marketplace
|
|
65
|
+
DEFAULT_MARKETPLACE_REPO = "sundsvall/claude-plugins-marketplace"
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
# Version Information
|
|
69
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
# Current CLI version (must match pyproject.toml)
|
|
72
|
+
CLI_VERSION = "1.2.4"
|
|
73
|
+
|
|
74
|
+
# Schema versions this CLI can understand
|
|
75
|
+
# v1: Full-featured format with delegation, security policies, marketplace
|
|
76
|
+
SUPPORTED_SCHEMA_VERSIONS = ("v1",)
|
|
77
|
+
|
|
78
|
+
# Current schema version used for validation
|
|
79
|
+
CURRENT_SCHEMA_VERSION = "1.0.0"
|