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/cli.py ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SCC - Sandboxed Claude CLI
4
+
5
+ A command-line tool for safely running Claude Code in Docker sandboxes
6
+ with team-specific configurations and worktree management.
7
+
8
+ This module serves as the thin orchestrator that composes commands from:
9
+ - commands/launch.py: Start command and interactive mode
10
+ - commands/worktree.py: Worktree, session, and container management
11
+ - commands/config.py: Teams, setup, and configuration commands
12
+ - commands/admin.py: Doctor, update, statusline, and stats commands
13
+ """
14
+
15
+ from importlib.metadata import PackageNotFoundError
16
+ from importlib.metadata import version as get_installed_version
17
+
18
+ import typer
19
+
20
+ from .cli_common import console, state
21
+ from .commands.admin import (
22
+ doctor_cmd,
23
+ stats_app,
24
+ status_cmd,
25
+ statusline_cmd,
26
+ update_cmd,
27
+ )
28
+ from .commands.audit import audit_app
29
+ from .commands.config import (
30
+ config_cmd,
31
+ setup_cmd,
32
+ )
33
+ from .commands.exceptions import exceptions_app, unblock_cmd
34
+ from .commands.init import init_cmd
35
+
36
+ # Import command functions from domain modules
37
+ from .commands.launch import start
38
+ from .commands.org import org_app
39
+ from .commands.support import support_app
40
+ from .commands.team import team_app
41
+ from .commands.worktree import (
42
+ container_app,
43
+ context_app,
44
+ list_cmd,
45
+ prune_cmd,
46
+ session_app,
47
+ sessions_cmd,
48
+ stop_cmd,
49
+ worktree_app,
50
+ )
51
+
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+ # App Configuration
54
+ # ─────────────────────────────────────────────────────────────────────────────
55
+
56
+ app = typer.Typer(
57
+ name="scc-cli",
58
+ help="Safely run Claude Code with team configurations and worktree management.",
59
+ no_args_is_help=False,
60
+ rich_markup_mode="rich",
61
+ context_settings={"help_option_names": ["-h", "--help"]},
62
+ )
63
+
64
+
65
+ # ─────────────────────────────────────────────────────────────────────────────
66
+ # Global Callback (--debug flag)
67
+ # ─────────────────────────────────────────────────────────────────────────────
68
+
69
+
70
+ @app.callback(invoke_without_command=True)
71
+ def main_callback(
72
+ ctx: typer.Context,
73
+ debug: bool = typer.Option(
74
+ False,
75
+ "--debug",
76
+ help="Show detailed error information for troubleshooting.",
77
+ is_eager=True,
78
+ ),
79
+ version: bool = typer.Option(
80
+ False,
81
+ "--version",
82
+ "-v",
83
+ help="Show version and exit.",
84
+ is_eager=True,
85
+ ),
86
+ interactive: bool = typer.Option(
87
+ False,
88
+ "-i",
89
+ "--interactive",
90
+ help="Force interactive workspace picker (shortcut for 'scc start -i').",
91
+ ),
92
+ ) -> None:
93
+ """
94
+ [bold cyan]SCC[/bold cyan] - Sandboxed Claude CLI
95
+
96
+ Safely run Claude Code in Docker sandboxes with team configurations.
97
+ """
98
+ state.debug = debug
99
+
100
+ if version:
101
+ from .ui.branding import get_version_header
102
+
103
+ try:
104
+ pkg_version = get_installed_version("scc-cli")
105
+ except PackageNotFoundError:
106
+ pkg_version = "unknown"
107
+ console.print(get_version_header(pkg_version))
108
+ raise typer.Exit()
109
+
110
+ # If no command provided and not showing version, use context-aware routing
111
+ if ctx.invoked_subcommand is None:
112
+ from pathlib import Path
113
+
114
+ from rich.prompt import Prompt
115
+
116
+ from . import config as scc_config
117
+ from . import setup as scc_setup
118
+ from .services.workspace import resolve_launch_context
119
+ from .ui.gate import is_interactive_allowed
120
+
121
+ # Use strong-signal resolver (git or .scc.yaml) for parity with 'scc start'
122
+ # Weak markers (package.json, etc.) are NOT used for auto-launch
123
+ cwd = Path.cwd()
124
+ result = resolve_launch_context(cwd, workspace_arg=None)
125
+ workspace_detected = result is not None and result.is_auto_eligible()
126
+
127
+ if is_interactive_allowed():
128
+ # If no org is configured and standalone isn't explicit, offer setup
129
+ user_cfg = scc_config.load_user_config()
130
+ org_source = user_cfg.get("organization_source") or {}
131
+ has_org = bool(org_source.get("url"))
132
+ if not has_org and not user_cfg.get("standalone"):
133
+ choice = Prompt.ask(
134
+ "[yellow]No organization configured.[/yellow] Choose setup mode",
135
+ choices=["setup", "standalone", "quit"],
136
+ default="setup",
137
+ )
138
+ if choice == "setup":
139
+ if not scc_setup.run_setup_wizard(console):
140
+ raise typer.Exit(0)
141
+ elif choice == "standalone":
142
+ user_cfg["standalone"] = True
143
+ scc_config.save_user_config(user_cfg)
144
+ else:
145
+ raise typer.Exit(0)
146
+
147
+ # Offer to start immediately after setup/standalone choice
148
+ start_now = Prompt.ask(
149
+ "[cyan]Start a session now?[/cyan]",
150
+ choices=["yes", "no"],
151
+ default="yes",
152
+ )
153
+ if start_now == "yes":
154
+ ctx.invoke(
155
+ start,
156
+ workspace=str(cwd) if workspace_detected else None,
157
+ team=None,
158
+ session_name=None,
159
+ resume=False,
160
+ select=False,
161
+ continue_session=False,
162
+ worktree_name=None,
163
+ fresh=False,
164
+ install_deps=False,
165
+ offline=False,
166
+ standalone=False,
167
+ dry_run=False,
168
+ json_output=False,
169
+ pretty=False,
170
+ )
171
+ return
172
+
173
+ if interactive:
174
+ # -i flag: force interactive workspace picker via start -i
175
+ ctx.invoke(
176
+ start,
177
+ workspace=None,
178
+ team=None,
179
+ session_name=None,
180
+ resume=False,
181
+ select=False,
182
+ continue_session=False,
183
+ worktree_name=None,
184
+ fresh=False,
185
+ install_deps=False,
186
+ offline=False,
187
+ standalone=False,
188
+ dry_run=False,
189
+ json_output=False,
190
+ pretty=False,
191
+ )
192
+ elif workspace_detected:
193
+ # Strong signal found (git repo or .scc.yaml) → use smart start flow
194
+ # This shows Quick Resume (if sessions exist) or launches immediately
195
+ ctx.invoke(
196
+ start,
197
+ workspace=str(cwd),
198
+ team=None,
199
+ session_name=None,
200
+ resume=False,
201
+ select=False,
202
+ continue_session=False,
203
+ worktree_name=None,
204
+ fresh=False,
205
+ install_deps=False,
206
+ offline=False,
207
+ standalone=False,
208
+ dry_run=False,
209
+ json_output=False,
210
+ pretty=False,
211
+ )
212
+ else:
213
+ # No strong signal (not in git repo, no .scc.yaml) → show dashboard
214
+ from .ui.dashboard import run_dashboard
215
+
216
+ run_dashboard()
217
+ else:
218
+ # Non-interactive - invoke start with defaults (will fail F1/F2 if no signal)
219
+ # NOTE: Must pass ALL defaults explicitly - ctx.invoke() doesn't resolve
220
+ # typer.Argument/Option defaults, it passes raw ArgumentInfo/OptionInfo
221
+ ctx.invoke(
222
+ start,
223
+ workspace=str(cwd) if workspace_detected else None,
224
+ team=None,
225
+ session_name=None,
226
+ resume=False,
227
+ select=False,
228
+ continue_session=False,
229
+ worktree_name=None,
230
+ fresh=False,
231
+ install_deps=False,
232
+ offline=False,
233
+ standalone=False,
234
+ dry_run=False,
235
+ json_output=False,
236
+ pretty=False,
237
+ )
238
+
239
+
240
+ # ─────────────────────────────────────────────────────────────────────────────
241
+ # Help Panel Group Names
242
+ # ─────────────────────────────────────────────────────────────────────────────
243
+
244
+ PANEL_SESSION = "Session Management"
245
+ PANEL_WORKSPACE = "Workspace"
246
+ PANEL_CONFIG = "Configuration"
247
+ PANEL_ADMIN = "Administration"
248
+ PANEL_GOVERNANCE = "Governance"
249
+
250
+ # ─────────────────────────────────────────────────────────────────────────────
251
+ # Register Commands from Domain Modules
252
+ # ─────────────────────────────────────────────────────────────────────────────
253
+
254
+ # Launch commands
255
+ app.command(rich_help_panel=PANEL_SESSION)(start)
256
+
257
+ # Worktree command group
258
+ app.add_typer(worktree_app, name="worktree", rich_help_panel=PANEL_WORKSPACE)
259
+
260
+ # Session and container commands
261
+ app.command(name="sessions", rich_help_panel=PANEL_SESSION)(sessions_cmd)
262
+ app.command(name="list", rich_help_panel=PANEL_SESSION)(list_cmd)
263
+ app.command(name="stop", rich_help_panel=PANEL_SESSION)(stop_cmd)
264
+ app.command(name="prune", rich_help_panel=PANEL_SESSION)(prune_cmd)
265
+
266
+ # Configuration commands
267
+ app.add_typer(team_app, name="team", rich_help_panel=PANEL_CONFIG)
268
+ app.command(name="setup", rich_help_panel=PANEL_CONFIG)(setup_cmd)
269
+ app.command(name="config", rich_help_panel=PANEL_CONFIG)(config_cmd)
270
+ app.command(name="init", rich_help_panel=PANEL_CONFIG)(init_cmd)
271
+
272
+ # Admin commands
273
+ app.command(name="doctor", rich_help_panel=PANEL_ADMIN)(doctor_cmd)
274
+ app.command(name="update", rich_help_panel=PANEL_ADMIN)(update_cmd)
275
+ app.command(name="status", rich_help_panel=PANEL_ADMIN)(status_cmd)
276
+ app.command(name="statusline", rich_help_panel=PANEL_ADMIN)(statusline_cmd)
277
+
278
+ # Add stats sub-app
279
+ app.add_typer(stats_app, name="stats", rich_help_panel=PANEL_ADMIN)
280
+
281
+ # Exception management commands
282
+ app.add_typer(exceptions_app, name="exceptions", rich_help_panel=PANEL_GOVERNANCE)
283
+ app.command(name="unblock", rich_help_panel=PANEL_GOVERNANCE)(unblock_cmd)
284
+
285
+ # Audit commands
286
+ app.add_typer(audit_app, name="audit", rich_help_panel=PANEL_GOVERNANCE)
287
+
288
+ # Support commands
289
+ app.add_typer(support_app, name="support", rich_help_panel=PANEL_GOVERNANCE)
290
+
291
+ # Org admin commands
292
+ app.add_typer(org_app, name="org", rich_help_panel=PANEL_GOVERNANCE)
293
+
294
+ # Symmetric alias apps (Phase 8)
295
+ app.add_typer(session_app, name="session", rich_help_panel=PANEL_WORKSPACE)
296
+ app.add_typer(container_app, name="container", rich_help_panel=PANEL_WORKSPACE)
297
+ app.add_typer(context_app, name="context", rich_help_panel=PANEL_WORKSPACE)
298
+
299
+
300
+ # ─────────────────────────────────────────────────────────────────────────────
301
+ # Entry Point
302
+ # ─────────────────────────────────────────────────────────────────────────────
303
+
304
+
305
+ def main() -> None:
306
+ """Entry point for the CLI."""
307
+ app()
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()
scc_cli/cli_common.py ADDED
@@ -0,0 +1,190 @@
1
+ """
2
+ CLI Common Utilities.
3
+
4
+ Shared utilities, constants, and decorators used across all CLI modules.
5
+ This module is extracted to prevent circular imports and enable clean composition.
6
+ """
7
+
8
+ from collections.abc import Callable
9
+ from functools import wraps
10
+ from typing import Any, TypeVar, cast
11
+
12
+ import typer
13
+ from rich import box
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ from .core.errors import SCCError
18
+ from .core.exit_codes import EXIT_CANCELLED
19
+ from .output_mode import is_json_command_mode
20
+ from .panels import create_warning_panel
21
+ from .ui.prompts import render_error
22
+
23
+ F = TypeVar("F", bound=Callable[..., Any])
24
+
25
+ # ─────────────────────────────────────────────────────────────────────────────
26
+ # Display Constants
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ # Maximum length for displaying file paths before truncation
30
+ MAX_DISPLAY_PATH_LENGTH = 50
31
+ # Characters to keep when truncating (MAX - 3 for "...")
32
+ PATH_TRUNCATE_LENGTH = 47
33
+ # Terminal width threshold for wide mode tables
34
+ WIDE_MODE_THRESHOLD = 110
35
+
36
+
37
+ # ─────────────────────────────────────────────────────────────────────────────
38
+ # Shared Console and State
39
+ # ─────────────────────────────────────────────────────────────────────────────
40
+
41
+ console = Console()
42
+ err_console = Console(stderr=True)
43
+
44
+
45
+ class AppState:
46
+ """Global application state for CLI flags."""
47
+
48
+ debug: bool = False
49
+
50
+
51
+ state = AppState()
52
+
53
+
54
+ # ─────────────────────────────────────────────────────────────────────────────
55
+ # Error Boundary Decorator
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+
58
+
59
+ def handle_errors(func: F) -> F:
60
+ """Catch SCCError exceptions and render user-friendly error output.
61
+
62
+ Wrap CLI command functions to provide consistent error handling:
63
+ - SCCError: Render with render_error and exit with error's exit_code
64
+ - KeyboardInterrupt: Print cancellation message and exit 130
65
+ - Other exceptions: Show warning panel (or full traceback with --debug)
66
+
67
+ JSON Mode: This is the SINGLE LOCATION for JSON error envelope output.
68
+ All errors in JSON mode are handled here to ensure consistency.
69
+
70
+ Args:
71
+ func: The CLI command function to wrap.
72
+
73
+ Returns:
74
+ Wrapped function with error handling.
75
+ """
76
+
77
+ @wraps(func)
78
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
79
+ try:
80
+ return func(*args, **kwargs)
81
+ except SCCError as e:
82
+ if is_json_command_mode():
83
+ # JSON mode: emit structured error envelope to stdout
84
+ from .core.exit_codes import get_exit_code_for_exception
85
+ from .json_output import build_error_envelope
86
+ from .output_mode import print_json
87
+
88
+ envelope = build_error_envelope(e)
89
+ print_json(envelope)
90
+ raise typer.Exit(get_exit_code_for_exception(e))
91
+ # Human mode: use stderr for errors (stdout purity for shell wrappers)
92
+ render_error(err_console, e, debug=state.debug)
93
+ raise typer.Exit(e.exit_code)
94
+ except KeyboardInterrupt:
95
+ if is_json_command_mode():
96
+ # JSON mode: emit cancellation envelope
97
+ from .json_output import build_error_envelope
98
+ from .output_mode import print_json
99
+
100
+ # Create a pseudo-exception for the envelope
101
+ cancel_exc = Exception("Operation cancelled by user")
102
+ envelope = build_error_envelope(cancel_exc)
103
+ print_json(envelope)
104
+ raise typer.Exit(EXIT_CANCELLED)
105
+ # Human mode: use stderr
106
+ err_console.print("\n[dim]Operation cancelled.[/dim]")
107
+ raise typer.Exit(EXIT_CANCELLED)
108
+ except (typer.Exit, SystemExit):
109
+ # Let typer exits pass through
110
+ raise
111
+ except Exception as e:
112
+ if is_json_command_mode():
113
+ # JSON mode: emit structured error envelope for unexpected errors
114
+ from .core.exit_codes import EXIT_INTERNAL
115
+ from .json_output import build_error_envelope
116
+ from .output_mode import print_json
117
+
118
+ envelope = build_error_envelope(e)
119
+ print_json(envelope)
120
+ raise typer.Exit(EXIT_INTERNAL)
121
+ # Human mode: unexpected errors to stderr
122
+ if state.debug:
123
+ err_console.print_exception()
124
+ else:
125
+ err_console.print(
126
+ create_warning_panel(
127
+ "Unexpected Error",
128
+ str(e),
129
+ "Run with --debug for full traceback",
130
+ )
131
+ )
132
+ raise typer.Exit(5)
133
+
134
+ return cast(F, wrapper)
135
+
136
+
137
+ # ─────────────────────────────────────────────────────────────────────────────
138
+ # UI Helpers (Consistent Aesthetic)
139
+ # ─────────────────────────────────────────────────────────────────────────────
140
+
141
+
142
+ def render_responsive_table(
143
+ title: str,
144
+ columns: list[tuple[str, str]], # (header, style)
145
+ rows: list[list[str]],
146
+ wide_columns: list[tuple[str, str]] | None = None, # Extra columns for wide mode
147
+ ) -> None:
148
+ """Render a table that adapts to terminal width.
149
+
150
+ Display base columns on narrow terminals, adding extra columns when
151
+ terminal width exceeds WIDE_MODE_THRESHOLD.
152
+
153
+ Args:
154
+ title: Table title displayed above the table.
155
+ columns: Base columns as list of (header, style) tuples.
156
+ rows: Data rows where each row contains values for all columns
157
+ (base + wide). Extra values are ignored on narrow terminals.
158
+ wide_columns: Additional columns shown only on wide terminals.
159
+ """
160
+ width = console.width
161
+ wide_mode = width >= WIDE_MODE_THRESHOLD
162
+
163
+ table = Table(
164
+ title=f"[bold cyan]{title}[/bold cyan]",
165
+ box=box.ROUNDED,
166
+ header_style="bold cyan",
167
+ expand=True,
168
+ show_lines=False,
169
+ )
170
+
171
+ # Add base columns
172
+ for header, style in columns:
173
+ table.add_column(header, style=style)
174
+
175
+ # Add extra columns in wide mode
176
+ if wide_mode and wide_columns:
177
+ for header, style in wide_columns:
178
+ table.add_column(header, style=style)
179
+
180
+ # Add rows
181
+ for row in rows:
182
+ if wide_mode and wide_columns:
183
+ table.add_row(*row)
184
+ else:
185
+ # Truncate to base columns only
186
+ table.add_row(*row[: len(columns)])
187
+
188
+ console.print()
189
+ console.print(table)
190
+ console.print()