scc-cli 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

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