scc-cli 1.4.0__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 (112) 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 +683 -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 +1400 -0
  16. scc_cli/cli_org.py +1433 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +858 -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 +603 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1082 -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 +1405 -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/compute.py +377 -0
  46. scc_cli/marketplace/constants.py +87 -0
  47. scc_cli/marketplace/managed.py +135 -0
  48. scc_cli/marketplace/materialize.py +723 -0
  49. scc_cli/marketplace/normalize.py +548 -0
  50. scc_cli/marketplace/render.py +238 -0
  51. scc_cli/marketplace/resolve.py +459 -0
  52. scc_cli/marketplace/schema.py +502 -0
  53. scc_cli/marketplace/sync.py +257 -0
  54. scc_cli/marketplace/team_cache.py +195 -0
  55. scc_cli/marketplace/team_fetch.py +688 -0
  56. scc_cli/marketplace/trust.py +244 -0
  57. scc_cli/models/__init__.py +41 -0
  58. scc_cli/models/exceptions.py +273 -0
  59. scc_cli/models/plugin_audit.py +434 -0
  60. scc_cli/org_templates.py +269 -0
  61. scc_cli/output_mode.py +167 -0
  62. scc_cli/panels.py +113 -0
  63. scc_cli/platform.py +350 -0
  64. scc_cli/profiles.py +1034 -0
  65. scc_cli/remote.py +443 -0
  66. scc_cli/schemas/__init__.py +1 -0
  67. scc_cli/schemas/org-v1.schema.json +456 -0
  68. scc_cli/schemas/team-config.v1.schema.json +163 -0
  69. scc_cli/sessions.py +425 -0
  70. scc_cli/setup.py +582 -0
  71. scc_cli/source_resolver.py +470 -0
  72. scc_cli/stats.py +378 -0
  73. scc_cli/stores/__init__.py +13 -0
  74. scc_cli/stores/exception_store.py +251 -0
  75. scc_cli/subprocess_utils.py +88 -0
  76. scc_cli/teams.py +339 -0
  77. scc_cli/templates/__init__.py +2 -0
  78. scc_cli/templates/org/__init__.py +0 -0
  79. scc_cli/templates/org/minimal.json +19 -0
  80. scc_cli/templates/org/reference.json +74 -0
  81. scc_cli/templates/org/strict.json +38 -0
  82. scc_cli/templates/org/teams.json +42 -0
  83. scc_cli/templates/statusline.sh +75 -0
  84. scc_cli/theme.py +348 -0
  85. scc_cli/ui/__init__.py +124 -0
  86. scc_cli/ui/branding.py +68 -0
  87. scc_cli/ui/chrome.py +395 -0
  88. scc_cli/ui/dashboard/__init__.py +62 -0
  89. scc_cli/ui/dashboard/_dashboard.py +669 -0
  90. scc_cli/ui/dashboard/loaders.py +369 -0
  91. scc_cli/ui/dashboard/models.py +184 -0
  92. scc_cli/ui/dashboard/orchestrator.py +337 -0
  93. scc_cli/ui/formatters.py +443 -0
  94. scc_cli/ui/gate.py +350 -0
  95. scc_cli/ui/help.py +157 -0
  96. scc_cli/ui/keys.py +521 -0
  97. scc_cli/ui/list_screen.py +431 -0
  98. scc_cli/ui/picker.py +700 -0
  99. scc_cli/ui/prompts.py +200 -0
  100. scc_cli/ui/wizard.py +490 -0
  101. scc_cli/update.py +680 -0
  102. scc_cli/utils/__init__.py +39 -0
  103. scc_cli/utils/fixit.py +264 -0
  104. scc_cli/utils/fuzzy.py +124 -0
  105. scc_cli/utils/locks.py +101 -0
  106. scc_cli/utils/ttl.py +376 -0
  107. scc_cli/validate.py +455 -0
  108. scc_cli-1.4.0.dist-info/METADATA +369 -0
  109. scc_cli-1.4.0.dist-info/RECORD +112 -0
  110. scc_cli-1.4.0.dist-info/WHEEL +4 -0
  111. scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
  112. scc_cli-1.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,337 @@
1
+ """Orchestration functions for the dashboard module.
2
+
3
+ This module contains the entry point and flow handlers:
4
+ - run_dashboard: Main entry point for `scc` with no arguments
5
+ - _handle_team_switch: Team picker integration
6
+ - _handle_start_flow: Start wizard integration
7
+ - _handle_session_resume: Session resume logic
8
+
9
+ The orchestrator manages the dashboard lifecycle including intent exceptions
10
+ that exit the Rich Live context before handling nested UI components.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from ...console import get_err_console
18
+
19
+ if TYPE_CHECKING:
20
+ from rich.console import Console
21
+
22
+ from ..keys import (
23
+ RefreshRequested,
24
+ SessionResumeRequested,
25
+ StartRequested,
26
+ TeamSwitchRequested,
27
+ )
28
+ from ..list_screen import ListState
29
+ from ._dashboard import Dashboard
30
+ from .loaders import _load_all_tab_data
31
+ from .models import DashboardState, DashboardTab
32
+
33
+
34
+ def run_dashboard() -> None:
35
+ """Run the main SCC dashboard.
36
+
37
+ This is the entry point for `scc` with no arguments in a TTY.
38
+ It loads current resource data and displays the interactive dashboard.
39
+
40
+ Handles intent exceptions by executing the requested flow outside the
41
+ Rich Live context (critical to avoid nested Live conflicts), then
42
+ reloading the dashboard with restored tab state.
43
+
44
+ Intent Exceptions:
45
+ - TeamSwitchRequested: Show team picker, reload with new team
46
+ - StartRequested: Run start wizard, return to source tab with fresh data
47
+ - RefreshRequested: Reload tab data, return to source tab
48
+ """
49
+ # Track which tab to restore after flow (uses .name for stability)
50
+ restore_tab: str | None = None
51
+ # Toast message to show on next dashboard iteration (e.g., "Start cancelled")
52
+ toast_message: str | None = None
53
+
54
+ while True:
55
+ # Load real data for all tabs
56
+ tabs = _load_all_tab_data()
57
+
58
+ # Determine initial tab (restore previous or default to STATUS)
59
+ initial_tab = DashboardTab.STATUS
60
+ if restore_tab:
61
+ # Find tab by name (stable identifier)
62
+ for tab in DashboardTab:
63
+ if tab.name == restore_tab:
64
+ initial_tab = tab
65
+ break
66
+ restore_tab = None # Clear after use
67
+
68
+ state = DashboardState(
69
+ active_tab=initial_tab,
70
+ tabs=tabs,
71
+ list_state=ListState(items=tabs[initial_tab].items),
72
+ status_message=toast_message, # Show any pending toast
73
+ )
74
+ toast_message = None # Clear after use
75
+
76
+ dashboard = Dashboard(state)
77
+ try:
78
+ dashboard.run()
79
+ break # Normal exit (q or Esc)
80
+ except TeamSwitchRequested:
81
+ # User pressed 't' - show team picker then reload dashboard
82
+ _handle_team_switch()
83
+ # Loop continues to reload dashboard with new team
84
+
85
+ except StartRequested as start_req:
86
+ # User pressed Enter on startable placeholder
87
+ # Execute start flow OUTSIDE Rich Live (critical: avoids nested Live)
88
+ restore_tab = start_req.return_to
89
+ result = _handle_start_flow(start_req.reason)
90
+
91
+ if result is None:
92
+ # User pressed q: quit app entirely
93
+ break
94
+
95
+ if result is False:
96
+ # User pressed Esc: go back to dashboard, show toast
97
+ toast_message = "Start cancelled"
98
+ # Loop continues to reload dashboard with fresh data
99
+
100
+ except RefreshRequested as refresh_req:
101
+ # User pressed 'r' - just reload data
102
+ restore_tab = refresh_req.return_to
103
+ # Loop continues with fresh data (no additional action needed)
104
+
105
+ except SessionResumeRequested as resume_req:
106
+ # User pressed Enter on a session item → resume it
107
+ restore_tab = resume_req.return_to
108
+ success = _handle_session_resume(resume_req.session)
109
+
110
+ if not success:
111
+ # Resume failed (e.g., missing workspace) - show toast
112
+ toast_message = "Session resume failed"
113
+ else:
114
+ # Successfully launched - exit dashboard
115
+ # (container is running, user is now in Claude)
116
+ break
117
+
118
+
119
+ def _prepare_for_nested_ui(console: Console) -> None:
120
+ """Prepare terminal state for launching nested UI components.
121
+
122
+ Restores cursor visibility, ensures clean newline, and flushes
123
+ any buffered input to prevent ghost keypresses from Rich Live context.
124
+
125
+ This should be called before launching any interactive picker or wizard
126
+ from the dashboard to ensure clean terminal state.
127
+
128
+ Args:
129
+ console: Rich Console instance for terminal operations.
130
+ """
131
+ import io
132
+ import sys
133
+
134
+ # Restore cursor (Rich Live may hide it)
135
+ console.show_cursor(True)
136
+ console.print() # Ensure clean newline
137
+
138
+ # Flush buffered input (best-effort, Unix only)
139
+ try:
140
+ import termios
141
+
142
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
143
+ except (
144
+ ModuleNotFoundError, # Windows - no termios module
145
+ OSError, # Redirected stdin, no TTY
146
+ ValueError, # Invalid file descriptor
147
+ TypeError, # Mock stdin without fileno
148
+ io.UnsupportedOperation, # Stdin without fileno support
149
+ ):
150
+ pass # Non-Unix or non-TTY environment - safe to ignore
151
+
152
+
153
+ def _handle_team_switch() -> None:
154
+ """Handle team switch request from dashboard.
155
+
156
+ Shows the team picker and switches team if user selects one.
157
+ """
158
+ from ... import config, teams
159
+ from ..picker import pick_team
160
+
161
+ console = get_err_console()
162
+ _prepare_for_nested_ui(console)
163
+
164
+ try:
165
+ # Load config and org config for team list
166
+ cfg = config.load_user_config()
167
+ org_config = config.load_cached_org_config()
168
+
169
+ available_teams = teams.list_teams(cfg, org_config=org_config)
170
+ if not available_teams:
171
+ console.print("[yellow]No teams available[/yellow]")
172
+ return
173
+
174
+ # Get current team for marking
175
+ current_team = cfg.get("selected_profile")
176
+
177
+ selected = pick_team(
178
+ available_teams,
179
+ current_team=str(current_team) if current_team else None,
180
+ title="Switch Team",
181
+ )
182
+
183
+ if selected:
184
+ # Update team selection
185
+ team_name = selected.get("name", "")
186
+ cfg["selected_profile"] = team_name
187
+ config.save_user_config(cfg)
188
+ console.print(f"[green]Switched to team: {team_name}[/green]")
189
+ # If cancelled, just return to dashboard
190
+
191
+ except TeamSwitchRequested:
192
+ # Nested team switch (shouldn't happen, but handle gracefully)
193
+ pass
194
+ except Exception as e:
195
+ console.print(f"[red]Error switching team: {e}[/red]")
196
+
197
+
198
+ def _handle_start_flow(reason: str) -> bool | None:
199
+ """Handle start flow request from dashboard.
200
+
201
+ Runs the interactive start wizard and launches a sandbox if user completes it.
202
+ Executes OUTSIDE Rich Live context (the dashboard has already exited
203
+ via the exception unwind before this is called).
204
+
205
+ Three-state return contract:
206
+ - True: Sandbox launched successfully
207
+ - False: User pressed Esc (back to dashboard)
208
+ - None: User pressed q (quit app entirely)
209
+
210
+ Args:
211
+ reason: Why the start flow was triggered (e.g., "no_containers", "no_sessions").
212
+ Used for logging/analytics and to determine skip_quick_resume.
213
+
214
+ Returns:
215
+ True if wizard completed successfully, False if user wants to go back,
216
+ None if user wants to quit entirely.
217
+ """
218
+ from ...cli_launch import run_start_wizard_flow
219
+
220
+ console = get_err_console()
221
+ _prepare_for_nested_ui(console)
222
+
223
+ # For empty-state starts, skip Quick Resume (user intent is "create new")
224
+ skip_quick_resume = reason in ("no_containers", "no_sessions")
225
+
226
+ # Show contextual message based on reason
227
+ if reason == "no_containers":
228
+ console.print("[dim]Starting a new session...[/dim]")
229
+ elif reason == "no_sessions":
230
+ console.print("[dim]Starting your first session...[/dim]")
231
+ console.print()
232
+
233
+ # Run the wizard with allow_back=True for dashboard context
234
+ # Returns: True (success), False (Esc/back), None (q/quit)
235
+ return run_start_wizard_flow(skip_quick_resume=skip_quick_resume, allow_back=True)
236
+
237
+
238
+ def _handle_session_resume(session: dict[str, Any]) -> bool:
239
+ """Handle session resume request from dashboard.
240
+
241
+ Resumes an existing session by launching the Docker container with
242
+ the stored workspace, team, and branch configuration.
243
+
244
+ This function executes OUTSIDE Rich Live context (the dashboard has
245
+ already exited via the exception unwind before this is called).
246
+
247
+ Args:
248
+ session: Session dict containing workspace, team, branch, container_name, etc.
249
+
250
+ Returns:
251
+ True if session was resumed successfully, False if resume failed
252
+ (e.g., workspace no longer exists).
253
+ """
254
+ from pathlib import Path
255
+
256
+ from rich.status import Status
257
+
258
+ from ... import config, docker
259
+ from ...cli_launch import (
260
+ _configure_team_settings,
261
+ _launch_sandbox,
262
+ _resolve_mount_and_branch,
263
+ _sync_marketplace_settings,
264
+ _validate_and_resolve_workspace,
265
+ )
266
+ from ...theme import Spinners
267
+
268
+ console = get_err_console()
269
+ _prepare_for_nested_ui(console)
270
+
271
+ # Extract session info
272
+ workspace = session.get("workspace", "")
273
+ team = session.get("team") # May be None for standalone
274
+ session_name = session.get("name")
275
+ branch = session.get("branch")
276
+
277
+ if not workspace:
278
+ console.print("[red]Session has no workspace path[/red]")
279
+ return False
280
+
281
+ # Validate workspace still exists
282
+ workspace_path = Path(workspace)
283
+ if not workspace_path.exists():
284
+ console.print(f"[red]Workspace no longer exists: {workspace}[/red]")
285
+ console.print("[dim]The session may have been deleted or moved.[/dim]")
286
+ return False
287
+
288
+ try:
289
+ # Docker availability check
290
+ with Status("[cyan]Checking Docker...[/cyan]", console=console, spinner=Spinners.DOCKER):
291
+ docker.check_docker_available()
292
+
293
+ # Validate and resolve workspace (we know it exists from earlier check)
294
+ resolved_path = _validate_and_resolve_workspace(str(workspace_path))
295
+ if resolved_path is None:
296
+ console.print("[red]Workspace validation failed[/red]")
297
+ return False
298
+ workspace_path = resolved_path
299
+
300
+ # Configure team settings
301
+ cfg = config.load_config()
302
+ _configure_team_settings(team, cfg)
303
+
304
+ # Sync marketplace settings
305
+ _sync_marketplace_settings(workspace_path, team)
306
+
307
+ # Resolve mount path and branch
308
+ mount_path, current_branch = _resolve_mount_and_branch(workspace_path)
309
+
310
+ # Use session's stored branch if available (more accurate than detected)
311
+ if branch:
312
+ current_branch = branch
313
+
314
+ # Show resume info
315
+ workspace_name = workspace_path.name
316
+ console.print(f"[cyan]Resuming session:[/cyan] {workspace_name}")
317
+ if team:
318
+ console.print(f"[dim]Team: {team}[/dim]")
319
+ if current_branch:
320
+ console.print(f"[dim]Branch: {current_branch}[/dim]")
321
+ console.print()
322
+
323
+ # Launch sandbox with resume flag
324
+ _launch_sandbox(
325
+ workspace_path=workspace_path,
326
+ mount_path=mount_path,
327
+ team=team,
328
+ session_name=session_name,
329
+ current_branch=current_branch,
330
+ should_continue_session=True, # Resume existing container
331
+ fresh=False,
332
+ )
333
+ return True
334
+
335
+ except Exception as e:
336
+ console.print(f"[red]Error resuming session: {e}[/red]")
337
+ return False