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
@@ -0,0 +1,390 @@
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
+ StatuslineInstallRequested,
27
+ TeamSwitchRequested,
28
+ )
29
+ from ..list_screen import ListState
30
+ from ._dashboard import Dashboard
31
+ from .loaders import _load_all_tab_data
32
+ from .models import DashboardState, DashboardTab
33
+
34
+
35
+ def run_dashboard() -> None:
36
+ """Run the main SCC dashboard.
37
+
38
+ This is the entry point for `scc` with no arguments in a TTY.
39
+ It loads current resource data and displays the interactive dashboard.
40
+
41
+ Handles intent exceptions by executing the requested flow outside the
42
+ Rich Live context (critical to avoid nested Live conflicts), then
43
+ reloading the dashboard with restored tab state.
44
+
45
+ Intent Exceptions:
46
+ - TeamSwitchRequested: Show team picker, reload with new team
47
+ - StartRequested: Run start wizard, return to source tab with fresh data
48
+ - RefreshRequested: Reload tab data, return to source tab
49
+ """
50
+ # Track which tab to restore after flow (uses .name for stability)
51
+ restore_tab: str | None = None
52
+ # Toast message to show on next dashboard iteration (e.g., "Start cancelled")
53
+ toast_message: str | None = None
54
+
55
+ while True:
56
+ # Load real data for all tabs
57
+ tabs = _load_all_tab_data()
58
+
59
+ # Determine initial tab (restore previous or default to STATUS)
60
+ initial_tab = DashboardTab.STATUS
61
+ if restore_tab:
62
+ # Find tab by name (stable identifier)
63
+ for tab in DashboardTab:
64
+ if tab.name == restore_tab:
65
+ initial_tab = tab
66
+ break
67
+ restore_tab = None # Clear after use
68
+
69
+ state = DashboardState(
70
+ active_tab=initial_tab,
71
+ tabs=tabs,
72
+ list_state=ListState(items=tabs[initial_tab].items),
73
+ status_message=toast_message, # Show any pending toast
74
+ )
75
+ toast_message = None # Clear after use
76
+
77
+ dashboard = Dashboard(state)
78
+ try:
79
+ dashboard.run()
80
+ break # Normal exit (q or Esc)
81
+ except TeamSwitchRequested:
82
+ # User pressed 't' - show team picker then reload dashboard
83
+ _handle_team_switch()
84
+ # Loop continues to reload dashboard with new team
85
+
86
+ except StartRequested as start_req:
87
+ # User pressed Enter on startable placeholder
88
+ # Execute start flow OUTSIDE Rich Live (critical: avoids nested Live)
89
+ restore_tab = start_req.return_to
90
+ result = _handle_start_flow(start_req.reason)
91
+
92
+ if result is None:
93
+ # User pressed q: quit app entirely
94
+ break
95
+
96
+ if result is False:
97
+ # User pressed Esc: go back to dashboard, show toast
98
+ toast_message = "Start cancelled"
99
+ # Loop continues to reload dashboard with fresh data
100
+
101
+ except RefreshRequested as refresh_req:
102
+ # User pressed 'r' - just reload data
103
+ restore_tab = refresh_req.return_to
104
+ # Loop continues with fresh data (no additional action needed)
105
+
106
+ except SessionResumeRequested as resume_req:
107
+ # User pressed Enter on a session item → resume it
108
+ restore_tab = resume_req.return_to
109
+ success = _handle_session_resume(resume_req.session)
110
+
111
+ if not success:
112
+ # Resume failed (e.g., missing workspace) - show toast
113
+ toast_message = "Session resume failed"
114
+ else:
115
+ # Successfully launched - exit dashboard
116
+ # (container is running, user is now in Claude)
117
+ break
118
+
119
+ except StatuslineInstallRequested as statusline_req:
120
+ # User pressed 'y' on statusline row - install statusline
121
+ restore_tab = statusline_req.return_to
122
+ success = _handle_statusline_install()
123
+
124
+ if success:
125
+ toast_message = "Statusline installed successfully"
126
+ else:
127
+ toast_message = "Statusline installation failed"
128
+ # Loop continues to reload dashboard with fresh data
129
+
130
+
131
+ def _prepare_for_nested_ui(console: Console) -> None:
132
+ """Prepare terminal state for launching nested UI components.
133
+
134
+ Restores cursor visibility, ensures clean newline, and flushes
135
+ any buffered input to prevent ghost keypresses from Rich Live context.
136
+
137
+ This should be called before launching any interactive picker or wizard
138
+ from the dashboard to ensure clean terminal state.
139
+
140
+ Args:
141
+ console: Rich Console instance for terminal operations.
142
+ """
143
+ import io
144
+ import sys
145
+
146
+ # Restore cursor (Rich Live may hide it)
147
+ console.show_cursor(True)
148
+ console.print() # Ensure clean newline
149
+
150
+ # Flush buffered input (best-effort, Unix only)
151
+ try:
152
+ import termios
153
+
154
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
155
+ except (
156
+ ModuleNotFoundError, # Windows - no termios module
157
+ OSError, # Redirected stdin, no TTY
158
+ ValueError, # Invalid file descriptor
159
+ TypeError, # Mock stdin without fileno
160
+ io.UnsupportedOperation, # Stdin without fileno support
161
+ ):
162
+ pass # Non-Unix or non-TTY environment - safe to ignore
163
+
164
+
165
+ def _handle_team_switch() -> None:
166
+ """Handle team switch request from dashboard.
167
+
168
+ Shows the team picker and switches team if user selects one.
169
+ """
170
+ from ... import config, teams
171
+ from ..picker import pick_team
172
+
173
+ console = get_err_console()
174
+ _prepare_for_nested_ui(console)
175
+
176
+ try:
177
+ # Load config and org config for team list
178
+ cfg = config.load_user_config()
179
+ org_config = config.load_cached_org_config()
180
+
181
+ available_teams = teams.list_teams(cfg, org_config=org_config)
182
+ if not available_teams:
183
+ console.print("[yellow]No teams available[/yellow]")
184
+ return
185
+
186
+ # Get current team for marking
187
+ current_team = cfg.get("selected_profile")
188
+
189
+ selected = pick_team(
190
+ available_teams,
191
+ current_team=str(current_team) if current_team else None,
192
+ title="Switch Team",
193
+ )
194
+
195
+ if selected:
196
+ # Update team selection
197
+ team_name = selected.get("name", "")
198
+ cfg["selected_profile"] = team_name
199
+ config.save_user_config(cfg)
200
+ console.print(f"[green]Switched to team: {team_name}[/green]")
201
+ # If cancelled, just return to dashboard
202
+
203
+ except TeamSwitchRequested:
204
+ # Nested team switch (shouldn't happen, but handle gracefully)
205
+ pass
206
+ except Exception as e:
207
+ console.print(f"[red]Error switching team: {e}[/red]")
208
+
209
+
210
+ def _handle_start_flow(reason: str) -> bool | None:
211
+ """Handle start flow request from dashboard.
212
+
213
+ Runs the interactive start wizard and launches a sandbox if user completes it.
214
+ Executes OUTSIDE Rich Live context (the dashboard has already exited
215
+ via the exception unwind before this is called).
216
+
217
+ Three-state return contract:
218
+ - True: Sandbox launched successfully
219
+ - False: User pressed Esc (back to dashboard)
220
+ - None: User pressed q (quit app entirely)
221
+
222
+ Args:
223
+ reason: Why the start flow was triggered (e.g., "no_containers", "no_sessions").
224
+ Used for logging/analytics and to determine skip_quick_resume.
225
+
226
+ Returns:
227
+ True if wizard completed successfully, False if user wants to go back,
228
+ None if user wants to quit entirely.
229
+ """
230
+ from ...cli_launch import run_start_wizard_flow
231
+
232
+ console = get_err_console()
233
+ _prepare_for_nested_ui(console)
234
+
235
+ # For empty-state starts, skip Quick Resume (user intent is "create new")
236
+ skip_quick_resume = reason in ("no_containers", "no_sessions")
237
+
238
+ # Show contextual message based on reason
239
+ if reason == "no_containers":
240
+ console.print("[dim]Starting a new session...[/dim]")
241
+ elif reason == "no_sessions":
242
+ console.print("[dim]Starting your first session...[/dim]")
243
+ console.print()
244
+
245
+ # Run the wizard with allow_back=True for dashboard context
246
+ # Returns: True (success), False (Esc/back), None (q/quit)
247
+ return run_start_wizard_flow(skip_quick_resume=skip_quick_resume, allow_back=True)
248
+
249
+
250
+ def _handle_session_resume(session: dict[str, Any]) -> bool:
251
+ """Handle session resume request from dashboard.
252
+
253
+ Resumes an existing session by launching the Docker container with
254
+ the stored workspace, team, and branch configuration.
255
+
256
+ This function executes OUTSIDE Rich Live context (the dashboard has
257
+ already exited via the exception unwind before this is called).
258
+
259
+ Args:
260
+ session: Session dict containing workspace, team, branch, container_name, etc.
261
+
262
+ Returns:
263
+ True if session was resumed successfully, False if resume failed
264
+ (e.g., workspace no longer exists).
265
+ """
266
+ from pathlib import Path
267
+
268
+ from rich.status import Status
269
+
270
+ from ... import config, docker
271
+ from ...cli_launch import (
272
+ _configure_team_settings,
273
+ _launch_sandbox,
274
+ _resolve_mount_and_branch,
275
+ _sync_marketplace_settings,
276
+ _validate_and_resolve_workspace,
277
+ )
278
+ from ...theme import Spinners
279
+
280
+ console = get_err_console()
281
+ _prepare_for_nested_ui(console)
282
+
283
+ # Extract session info
284
+ workspace = session.get("workspace", "")
285
+ team = session.get("team") # May be None for standalone
286
+ session_name = session.get("name")
287
+ branch = session.get("branch")
288
+
289
+ if not workspace:
290
+ console.print("[red]Session has no workspace path[/red]")
291
+ return False
292
+
293
+ # Validate workspace still exists
294
+ workspace_path = Path(workspace)
295
+ if not workspace_path.exists():
296
+ console.print(f"[red]Workspace no longer exists: {workspace}[/red]")
297
+ console.print("[dim]The session may have been deleted or moved.[/dim]")
298
+ return False
299
+
300
+ try:
301
+ # Docker availability check
302
+ with Status("[cyan]Checking Docker...[/cyan]", console=console, spinner=Spinners.DOCKER):
303
+ docker.check_docker_available()
304
+
305
+ # Validate and resolve workspace (we know it exists from earlier check)
306
+ resolved_path = _validate_and_resolve_workspace(str(workspace_path))
307
+ if resolved_path is None:
308
+ console.print("[red]Workspace validation failed[/red]")
309
+ return False
310
+ workspace_path = resolved_path
311
+
312
+ # Configure team settings
313
+ cfg = config.load_config()
314
+ _configure_team_settings(team, cfg)
315
+
316
+ # Sync marketplace settings
317
+ _sync_marketplace_settings(workspace_path, team)
318
+
319
+ # Resolve mount path and branch
320
+ mount_path, current_branch = _resolve_mount_and_branch(workspace_path)
321
+
322
+ # Use session's stored branch if available (more accurate than detected)
323
+ if branch:
324
+ current_branch = branch
325
+
326
+ # Show resume info
327
+ workspace_name = workspace_path.name
328
+ console.print(f"[cyan]Resuming session:[/cyan] {workspace_name}")
329
+ if team:
330
+ console.print(f"[dim]Team: {team}[/dim]")
331
+ if current_branch:
332
+ console.print(f"[dim]Branch: {current_branch}[/dim]")
333
+ console.print()
334
+
335
+ # Launch sandbox with resume flag
336
+ _launch_sandbox(
337
+ workspace_path=workspace_path,
338
+ mount_path=mount_path,
339
+ team=team,
340
+ session_name=session_name,
341
+ current_branch=current_branch,
342
+ should_continue_session=True, # Resume existing container
343
+ fresh=False,
344
+ )
345
+ return True
346
+
347
+ except Exception as e:
348
+ console.print(f"[red]Error resuming session: {e}[/red]")
349
+ return False
350
+
351
+
352
+ def _handle_statusline_install() -> bool:
353
+ """Handle statusline installation request from dashboard.
354
+
355
+ Installs the Claude Code statusline enhancement using the same logic
356
+ as `scc statusline`. Works cross-platform (Windows, macOS, Linux).
357
+
358
+ Returns:
359
+ True if statusline was installed successfully, False otherwise.
360
+ """
361
+ from rich.status import Status
362
+
363
+ from ...cli_admin import install_statusline
364
+ from ...theme import Spinners
365
+
366
+ console = get_err_console()
367
+ _prepare_for_nested_ui(console)
368
+
369
+ console.print("[cyan]Installing statusline...[/cyan]")
370
+ console.print()
371
+
372
+ try:
373
+ with Status(
374
+ "[cyan]Configuring statusline...[/cyan]",
375
+ console=console,
376
+ spinner=Spinners.DOCKER,
377
+ ):
378
+ result = install_statusline()
379
+
380
+ if result:
381
+ console.print("[green]✓ Statusline installed successfully![/green]")
382
+ console.print("[dim]Press any key to continue...[/dim]")
383
+ else:
384
+ console.print("[yellow]Statusline installation completed with warnings[/yellow]")
385
+
386
+ return result
387
+
388
+ except Exception as e:
389
+ console.print(f"[red]Error installing statusline: {e}[/red]")
390
+ return False