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
@@ -0,0 +1,735 @@
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
+ CreateWorktreeRequested,
24
+ GitInitRequested,
25
+ RecentWorkspacesRequested,
26
+ RefreshRequested,
27
+ SessionResumeRequested,
28
+ StartRequested,
29
+ StatuslineInstallRequested,
30
+ TeamSwitchRequested,
31
+ VerboseToggleRequested,
32
+ )
33
+ from ..list_screen import ListState
34
+ from ._dashboard import Dashboard
35
+ from .loaders import _load_all_tab_data
36
+ from .models import DashboardState, DashboardTab
37
+
38
+
39
+ def run_dashboard() -> None:
40
+ """Run the main SCC dashboard.
41
+
42
+ This is the entry point for `scc` with no arguments in a TTY.
43
+ It loads current resource data and displays the interactive dashboard.
44
+
45
+ Handles intent exceptions by executing the requested flow outside the
46
+ Rich Live context (critical to avoid nested Live conflicts), then
47
+ reloading the dashboard with restored tab state.
48
+
49
+ Intent Exceptions:
50
+ - TeamSwitchRequested: Show team picker, reload with new team
51
+ - StartRequested: Run start wizard, return to source tab with fresh data
52
+ - RefreshRequested: Reload tab data, return to source tab
53
+ - VerboseToggleRequested: Toggle verbose worktree status display
54
+ """
55
+ from ... import config as scc_config
56
+
57
+ # Show one-time onboarding banner for new users
58
+ if not scc_config.has_seen_onboarding():
59
+ _show_onboarding_banner()
60
+ scc_config.mark_onboarding_seen()
61
+
62
+ # Track which tab to restore after flow (uses .name for stability)
63
+ restore_tab: str | None = None
64
+ # Toast message to show on next dashboard iteration (e.g., "Start cancelled")
65
+ toast_message: str | None = None
66
+ # Track verbose worktree status display (persists across reloads)
67
+ verbose_worktrees: bool = False
68
+
69
+ while True:
70
+ # Load real data for all tabs (pass verbose flag for worktrees)
71
+ tabs = _load_all_tab_data(verbose_worktrees=verbose_worktrees)
72
+
73
+ # Determine initial tab (restore previous or default to STATUS)
74
+ initial_tab = DashboardTab.STATUS
75
+ if restore_tab:
76
+ # Find tab by name (stable identifier)
77
+ for tab in DashboardTab:
78
+ if tab.name == restore_tab:
79
+ initial_tab = tab
80
+ break
81
+ restore_tab = None # Clear after use
82
+
83
+ state = DashboardState(
84
+ active_tab=initial_tab,
85
+ tabs=tabs,
86
+ list_state=ListState(items=tabs[initial_tab].items),
87
+ status_message=toast_message, # Show any pending toast
88
+ verbose_worktrees=verbose_worktrees, # Preserve verbose state
89
+ )
90
+ toast_message = None # Clear after use
91
+
92
+ dashboard = Dashboard(state)
93
+ try:
94
+ dashboard.run()
95
+ break # Normal exit (q or Esc)
96
+ except TeamSwitchRequested:
97
+ # User pressed 't' - show team picker then reload dashboard
98
+ _handle_team_switch()
99
+ # Loop continues to reload dashboard with new team
100
+
101
+ except StartRequested as start_req:
102
+ # User pressed Enter on startable placeholder
103
+ # Execute start flow OUTSIDE Rich Live (critical: avoids nested Live)
104
+ restore_tab = start_req.return_to
105
+ result = _handle_start_flow(start_req.reason)
106
+
107
+ if result is None:
108
+ # User pressed q: quit app entirely
109
+ break
110
+
111
+ if result is False:
112
+ # User pressed Esc: go back to dashboard, show toast
113
+ toast_message = "Start cancelled"
114
+ # Loop continues to reload dashboard with fresh data
115
+
116
+ except RefreshRequested as refresh_req:
117
+ # User pressed 'r' - just reload data
118
+ restore_tab = refresh_req.return_to
119
+ # Loop continues with fresh data (no additional action needed)
120
+
121
+ except SessionResumeRequested as resume_req:
122
+ # User pressed Enter on a session item → resume it
123
+ restore_tab = resume_req.return_to
124
+ success = _handle_session_resume(resume_req.session)
125
+
126
+ if not success:
127
+ # Resume failed (e.g., missing workspace) - show toast
128
+ toast_message = "Session resume failed"
129
+ else:
130
+ # Successfully launched - exit dashboard
131
+ # (container is running, user is now in Claude)
132
+ break
133
+
134
+ except StatuslineInstallRequested as statusline_req:
135
+ # User pressed 'y' on statusline row - install statusline
136
+ restore_tab = statusline_req.return_to
137
+ success = _handle_statusline_install()
138
+
139
+ if success:
140
+ toast_message = "Statusline installed successfully"
141
+ else:
142
+ toast_message = "Statusline installation failed"
143
+ # Loop continues to reload dashboard with fresh data
144
+
145
+ except RecentWorkspacesRequested as recent_req:
146
+ # User pressed 'w' - show recent workspaces picker
147
+ restore_tab = recent_req.return_to
148
+ selected_workspace = _handle_recent_workspaces()
149
+
150
+ if selected_workspace is None:
151
+ # User cancelled or quit
152
+ toast_message = "Cancelled"
153
+ elif selected_workspace:
154
+ # User selected a workspace - start session in it
155
+ # For now, just show message; full integration comes later
156
+ toast_message = f"Selected: {selected_workspace}"
157
+ # Loop continues to reload dashboard
158
+
159
+ except GitInitRequested as init_req:
160
+ # User pressed 'i' - initialize git repo
161
+ restore_tab = init_req.return_to
162
+ success = _handle_git_init()
163
+
164
+ if success:
165
+ toast_message = "Git repository initialized"
166
+ else:
167
+ toast_message = "Git init cancelled or failed"
168
+ # Loop continues to reload dashboard
169
+
170
+ except CreateWorktreeRequested as create_req:
171
+ # User pressed 'c' - create worktree or clone
172
+ restore_tab = create_req.return_to
173
+
174
+ if create_req.is_git_repo:
175
+ success = _handle_create_worktree()
176
+ if success:
177
+ toast_message = "Worktree created"
178
+ else:
179
+ toast_message = "Worktree creation cancelled"
180
+ else:
181
+ success = _handle_clone()
182
+ if success:
183
+ toast_message = "Repository cloned"
184
+ else:
185
+ toast_message = "Clone cancelled"
186
+ # Loop continues to reload dashboard
187
+
188
+ except VerboseToggleRequested as verbose_req:
189
+ # User pressed 'v' - toggle verbose worktree status
190
+ restore_tab = verbose_req.return_to
191
+ verbose_worktrees = verbose_req.verbose
192
+ toast_message = "Status on" if verbose_worktrees else "Status off"
193
+ # Loop continues with new verbose setting
194
+
195
+
196
+ def _prepare_for_nested_ui(console: Console) -> None:
197
+ """Prepare terminal state for launching nested UI components.
198
+
199
+ Restores cursor visibility, ensures clean newline, and flushes
200
+ any buffered input to prevent ghost keypresses from Rich Live context.
201
+
202
+ This should be called before launching any interactive picker or wizard
203
+ from the dashboard to ensure clean terminal state.
204
+
205
+ Args:
206
+ console: Rich Console instance for terminal operations.
207
+ """
208
+ import io
209
+ import sys
210
+
211
+ # Restore cursor (Rich Live may hide it)
212
+ console.show_cursor(True)
213
+ console.print() # Ensure clean newline
214
+
215
+ # Flush buffered input (best-effort, Unix only)
216
+ try:
217
+ import termios
218
+
219
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
220
+ except (
221
+ ModuleNotFoundError, # Windows - no termios module
222
+ OSError, # Redirected stdin, no TTY
223
+ ValueError, # Invalid file descriptor
224
+ TypeError, # Mock stdin without fileno
225
+ io.UnsupportedOperation, # Stdin without fileno support
226
+ ):
227
+ pass # Non-Unix or non-TTY environment - safe to ignore
228
+
229
+
230
+ def _handle_team_switch() -> None:
231
+ """Handle team switch request from dashboard.
232
+
233
+ Shows the team picker and switches team if user selects one.
234
+ """
235
+ from ... import config, teams
236
+ from ..picker import pick_team
237
+
238
+ console = get_err_console()
239
+ _prepare_for_nested_ui(console)
240
+
241
+ try:
242
+ # Load config and org config for team list
243
+ cfg = config.load_user_config()
244
+ org_config = config.load_cached_org_config()
245
+
246
+ available_teams = teams.list_teams(cfg, org_config=org_config)
247
+ if not available_teams:
248
+ console.print("[yellow]No teams available[/yellow]")
249
+ return
250
+
251
+ # Get current team for marking
252
+ current_team = cfg.get("selected_profile")
253
+
254
+ selected = pick_team(
255
+ available_teams,
256
+ current_team=str(current_team) if current_team else None,
257
+ title="Switch Team",
258
+ )
259
+
260
+ if selected:
261
+ # Update team selection
262
+ team_name = selected.get("name", "")
263
+ cfg["selected_profile"] = team_name
264
+ config.save_user_config(cfg)
265
+ console.print(f"[green]Switched to team: {team_name}[/green]")
266
+ # If cancelled, just return to dashboard
267
+
268
+ except TeamSwitchRequested:
269
+ # Nested team switch (shouldn't happen, but handle gracefully)
270
+ pass
271
+ except Exception as e:
272
+ console.print(f"[red]Error switching team: {e}[/red]")
273
+
274
+
275
+ def _handle_start_flow(reason: str) -> bool | None:
276
+ """Handle start flow request from dashboard.
277
+
278
+ Runs the interactive start wizard and launches a sandbox if user completes it.
279
+ Executes OUTSIDE Rich Live context (the dashboard has already exited
280
+ via the exception unwind before this is called).
281
+
282
+ Three-state return contract:
283
+ - True: Sandbox launched successfully
284
+ - False: User pressed Esc (back to dashboard)
285
+ - None: User pressed q (quit app entirely)
286
+
287
+ Args:
288
+ reason: Why the start flow was triggered. Can be:
289
+ - "no_containers", "no_sessions": Empty state triggers (show wizard)
290
+ - "worktree:/path/to/worktree": Start session in specific worktree
291
+
292
+ Returns:
293
+ True if wizard completed successfully, False if user wants to go back,
294
+ None if user wants to quit entirely.
295
+ """
296
+ from ...commands.launch import run_start_wizard_flow
297
+
298
+ console = get_err_console()
299
+ _prepare_for_nested_ui(console)
300
+
301
+ # Handle worktree-specific start (Enter on worktree in details pane)
302
+ if reason.startswith("worktree:"):
303
+ worktree_path = reason[9:] # Remove "worktree:" prefix
304
+ return _handle_worktree_start(worktree_path)
305
+
306
+ # For empty-state starts, skip Quick Resume (user intent is "create new")
307
+ skip_quick_resume = reason in ("no_containers", "no_sessions")
308
+
309
+ # Show contextual message based on reason
310
+ if reason == "no_containers":
311
+ console.print("[dim]Starting a new session...[/dim]")
312
+ elif reason == "no_sessions":
313
+ console.print("[dim]Starting your first session...[/dim]")
314
+ console.print()
315
+
316
+ # Run the wizard with allow_back=True for dashboard context
317
+ # Returns: True (success), False (Esc/back), None (q/quit)
318
+ return run_start_wizard_flow(skip_quick_resume=skip_quick_resume, allow_back=True)
319
+
320
+
321
+ def _handle_worktree_start(worktree_path: str) -> bool | None:
322
+ """Handle starting a session in a specific worktree.
323
+
324
+ Launches a new session directly in the selected worktree, bypassing
325
+ the wizard workspace selection since the user already selected a worktree.
326
+
327
+ Args:
328
+ worktree_path: Absolute path to the worktree directory.
329
+
330
+ Returns:
331
+ True if session started successfully, False if cancelled,
332
+ None if user wants to quit entirely.
333
+ """
334
+ from pathlib import Path
335
+
336
+ from rich.status import Status
337
+
338
+ from ... import config, docker
339
+ from ...commands.launch import (
340
+ _configure_team_settings,
341
+ _launch_sandbox,
342
+ _resolve_mount_and_branch,
343
+ _sync_marketplace_settings,
344
+ _validate_and_resolve_workspace,
345
+ )
346
+ from ...theme import Spinners
347
+
348
+ console = get_err_console()
349
+
350
+ workspace_path = Path(worktree_path)
351
+ workspace_name = workspace_path.name
352
+
353
+ # Validate workspace exists
354
+ if not workspace_path.exists():
355
+ console.print(f"[red]Worktree no longer exists: {worktree_path}[/red]")
356
+ return False
357
+
358
+ console.print(f"[cyan]Starting session in:[/cyan] {workspace_name}")
359
+ console.print()
360
+
361
+ try:
362
+ # Docker availability check
363
+ with Status("[cyan]Checking Docker...[/cyan]", console=console, spinner=Spinners.DOCKER):
364
+ docker.check_docker_available()
365
+
366
+ # Validate and resolve workspace
367
+ resolved_path = _validate_and_resolve_workspace(str(workspace_path))
368
+ if resolved_path is None:
369
+ console.print("[red]Workspace validation failed[/red]")
370
+ return False
371
+ workspace_path = resolved_path
372
+
373
+ # Get current team from config
374
+ cfg = config.load_config()
375
+ team = cfg.get("selected_profile")
376
+ _configure_team_settings(team, cfg)
377
+
378
+ # Sync marketplace settings
379
+ _sync_marketplace_settings(workspace_path, team)
380
+
381
+ # Resolve mount path and branch
382
+ mount_path, current_branch = _resolve_mount_and_branch(workspace_path)
383
+
384
+ # Show session info
385
+ if team:
386
+ console.print(f"[dim]Team: {team}[/dim]")
387
+ if current_branch:
388
+ console.print(f"[dim]Branch: {current_branch}[/dim]")
389
+ console.print()
390
+
391
+ # Launch sandbox
392
+ _launch_sandbox(
393
+ workspace_path=workspace_path,
394
+ mount_path=mount_path,
395
+ team=team,
396
+ session_name=None, # No specific session name
397
+ current_branch=current_branch,
398
+ should_continue_session=False,
399
+ fresh=False,
400
+ )
401
+ return True
402
+
403
+ except KeyboardInterrupt:
404
+ console.print("\n[yellow]Cancelled[/yellow]")
405
+ return False
406
+ except Exception as e:
407
+ console.print(f"[red]Error starting session: {e}[/red]")
408
+ return False
409
+
410
+
411
+ def _handle_session_resume(session: dict[str, Any]) -> bool:
412
+ """Handle session resume request from dashboard.
413
+
414
+ Resumes an existing session by launching the Docker container with
415
+ the stored workspace, team, and branch configuration.
416
+
417
+ This function executes OUTSIDE Rich Live context (the dashboard has
418
+ already exited via the exception unwind before this is called).
419
+
420
+ Args:
421
+ session: Session dict containing workspace, team, branch, container_name, etc.
422
+
423
+ Returns:
424
+ True if session was resumed successfully, False if resume failed
425
+ (e.g., workspace no longer exists).
426
+ """
427
+ from pathlib import Path
428
+
429
+ from rich.status import Status
430
+
431
+ from ... import config, docker
432
+ from ...commands.launch import (
433
+ _configure_team_settings,
434
+ _launch_sandbox,
435
+ _resolve_mount_and_branch,
436
+ _sync_marketplace_settings,
437
+ _validate_and_resolve_workspace,
438
+ )
439
+ from ...theme import Spinners
440
+
441
+ console = get_err_console()
442
+ _prepare_for_nested_ui(console)
443
+
444
+ # Extract session info
445
+ workspace = session.get("workspace", "")
446
+ team = session.get("team") # May be None for standalone
447
+ session_name = session.get("name")
448
+ branch = session.get("branch")
449
+
450
+ if not workspace:
451
+ console.print("[red]Session has no workspace path[/red]")
452
+ return False
453
+
454
+ # Validate workspace still exists
455
+ workspace_path = Path(workspace)
456
+ if not workspace_path.exists():
457
+ console.print(f"[red]Workspace no longer exists: {workspace}[/red]")
458
+ console.print("[dim]The session may have been deleted or moved.[/dim]")
459
+ return False
460
+
461
+ try:
462
+ # Docker availability check
463
+ with Status("[cyan]Checking Docker...[/cyan]", console=console, spinner=Spinners.DOCKER):
464
+ docker.check_docker_available()
465
+
466
+ # Validate and resolve workspace (we know it exists from earlier check)
467
+ resolved_path = _validate_and_resolve_workspace(str(workspace_path))
468
+ if resolved_path is None:
469
+ console.print("[red]Workspace validation failed[/red]")
470
+ return False
471
+ workspace_path = resolved_path
472
+
473
+ # Configure team settings
474
+ cfg = config.load_config()
475
+ _configure_team_settings(team, cfg)
476
+
477
+ # Sync marketplace settings
478
+ _sync_marketplace_settings(workspace_path, team)
479
+
480
+ # Resolve mount path and branch
481
+ mount_path, current_branch = _resolve_mount_and_branch(workspace_path)
482
+
483
+ # Use session's stored branch if available (more accurate than detected)
484
+ if branch:
485
+ current_branch = branch
486
+
487
+ # Show resume info
488
+ workspace_name = workspace_path.name
489
+ console.print(f"[cyan]Resuming session:[/cyan] {workspace_name}")
490
+ if team:
491
+ console.print(f"[dim]Team: {team}[/dim]")
492
+ if current_branch:
493
+ console.print(f"[dim]Branch: {current_branch}[/dim]")
494
+ console.print()
495
+
496
+ # Launch sandbox with resume flag
497
+ _launch_sandbox(
498
+ workspace_path=workspace_path,
499
+ mount_path=mount_path,
500
+ team=team,
501
+ session_name=session_name,
502
+ current_branch=current_branch,
503
+ should_continue_session=True, # Resume existing container
504
+ fresh=False,
505
+ )
506
+ return True
507
+
508
+ except Exception as e:
509
+ console.print(f"[red]Error resuming session: {e}[/red]")
510
+ return False
511
+
512
+
513
+ def _handle_statusline_install() -> bool:
514
+ """Handle statusline installation request from dashboard.
515
+
516
+ Installs the Claude Code statusline enhancement using the same logic
517
+ as `scc statusline`. Works cross-platform (Windows, macOS, Linux).
518
+
519
+ Returns:
520
+ True if statusline was installed successfully, False otherwise.
521
+ """
522
+ from rich.status import Status
523
+
524
+ from ...commands.admin import install_statusline
525
+ from ...theme import Spinners
526
+
527
+ console = get_err_console()
528
+ _prepare_for_nested_ui(console)
529
+
530
+ console.print("[cyan]Installing statusline...[/cyan]")
531
+ console.print()
532
+
533
+ try:
534
+ with Status(
535
+ "[cyan]Configuring statusline...[/cyan]",
536
+ console=console,
537
+ spinner=Spinners.DOCKER,
538
+ ):
539
+ result = install_statusline()
540
+
541
+ if result:
542
+ console.print("[green]✓ Statusline installed successfully![/green]")
543
+ console.print("[dim]Press any key to continue...[/dim]")
544
+ else:
545
+ console.print("[yellow]Statusline installation completed with warnings[/yellow]")
546
+
547
+ return result
548
+
549
+ except Exception as e:
550
+ console.print(f"[red]Error installing statusline: {e}[/red]")
551
+ return False
552
+
553
+
554
+ def _handle_recent_workspaces() -> str | None:
555
+ """Handle recent workspaces picker from dashboard.
556
+
557
+ Shows a picker with recently used workspaces, allowing the user to
558
+ quickly navigate to a previous project.
559
+
560
+ Returns:
561
+ Path of selected workspace, or None if cancelled.
562
+ """
563
+ from ...contexts import load_recent_contexts
564
+ from ..picker import pick_context
565
+
566
+ console = get_err_console()
567
+ _prepare_for_nested_ui(console)
568
+
569
+ try:
570
+ recent = load_recent_contexts()
571
+ if not recent:
572
+ console.print("[yellow]No recent workspaces found[/yellow]")
573
+ console.print(
574
+ "[dim]Start a session with `scc start <path>` to populate this list.[/dim]"
575
+ )
576
+ return None
577
+
578
+ selected = pick_context(
579
+ recent,
580
+ title="Recent Workspaces",
581
+ subtitle="Select a workspace",
582
+ )
583
+
584
+ if selected:
585
+ return str(selected.worktree_path)
586
+ return None
587
+
588
+ except Exception as e:
589
+ console.print(f"[red]Error loading recent workspaces: {e}[/red]")
590
+ return None
591
+
592
+
593
+ def _handle_git_init() -> bool:
594
+ """Handle git init request from dashboard.
595
+
596
+ Initializes a new git repository in the current directory,
597
+ optionally creating an initial commit.
598
+
599
+ Returns:
600
+ True if git was initialized successfully, False otherwise.
601
+ """
602
+ import os
603
+ import subprocess
604
+
605
+ console = get_err_console()
606
+ _prepare_for_nested_ui(console)
607
+
608
+ cwd = os.getcwd()
609
+ console.print(f"[cyan]Initializing git repository in:[/cyan] {cwd}")
610
+ console.print()
611
+
612
+ try:
613
+ # Run git init
614
+ result = subprocess.run(
615
+ ["git", "init"],
616
+ cwd=cwd,
617
+ capture_output=True,
618
+ text=True,
619
+ check=True,
620
+ )
621
+ console.print(f"[green]✓ {result.stdout.strip()}[/green]")
622
+
623
+ # Optionally create initial commit
624
+ console.print()
625
+ console.print("[dim]Creating initial empty commit...[/dim]")
626
+
627
+ # Try to create an empty commit
628
+ try:
629
+ subprocess.run(
630
+ ["git", "commit", "--allow-empty", "-m", "Initial commit"],
631
+ cwd=cwd,
632
+ capture_output=True,
633
+ text=True,
634
+ check=True,
635
+ )
636
+ console.print("[green]✓ Initial commit created[/green]")
637
+ except subprocess.CalledProcessError as e:
638
+ # May fail if git identity not configured
639
+ if "user.email" in e.stderr or "user.name" in e.stderr:
640
+ console.print("[yellow]Tip: Configure git identity to enable commits:[/yellow]")
641
+ console.print(" git config user.name 'Your Name'")
642
+ console.print(" git config user.email 'your@email.com'")
643
+ else:
644
+ console.print(
645
+ f"[yellow]Could not create initial commit: {e.stderr.strip()}[/yellow]"
646
+ )
647
+
648
+ console.print()
649
+ console.print("[dim]Press any key to continue...[/dim]")
650
+ return True
651
+
652
+ except subprocess.CalledProcessError as e:
653
+ console.print(f"[red]Git init failed: {e.stderr.strip()}[/red]")
654
+ return False
655
+ except FileNotFoundError:
656
+ console.print("[red]Git is not installed or not in PATH[/red]")
657
+ return False
658
+
659
+
660
+ def _handle_create_worktree() -> bool:
661
+ """Handle create worktree request from dashboard.
662
+
663
+ Prompts for a worktree name and creates a new git worktree.
664
+
665
+ Returns:
666
+ True if worktree was created successfully, False otherwise.
667
+ """
668
+ console = get_err_console()
669
+ _prepare_for_nested_ui(console)
670
+
671
+ console.print("[cyan]Create new worktree[/cyan]")
672
+ console.print()
673
+ console.print("[dim]Use `scc worktree create <name>` from the terminal for full options.[/dim]")
674
+ console.print("[dim]Press any key to continue...[/dim]")
675
+
676
+ # For now, just inform user of CLI option
677
+ # Full interactive creation can be added in a future phase
678
+ return False
679
+
680
+
681
+ def _handle_clone() -> bool:
682
+ """Handle clone request from dashboard.
683
+
684
+ Informs user how to clone a repository.
685
+
686
+ Returns:
687
+ True if clone was successful, False otherwise.
688
+ """
689
+ console = get_err_console()
690
+ _prepare_for_nested_ui(console)
691
+
692
+ console.print("[cyan]Clone a repository[/cyan]")
693
+ console.print()
694
+ console.print("[dim]Use `git clone <url>` to clone a repository, then run `scc` in it.[/dim]")
695
+ console.print("[dim]Press any key to continue...[/dim]")
696
+
697
+ # For now, just inform user of git clone option
698
+ # Full interactive clone can be added in a future phase
699
+ return False
700
+
701
+
702
+ def _show_onboarding_banner() -> None:
703
+ """Show one-time onboarding banner for new users.
704
+
705
+ Displays a brief tip about `scc worktree enter` as the recommended
706
+ way to switch worktrees without shell configuration.
707
+
708
+ Waits for user to press any key before continuing.
709
+ """
710
+ import readchar
711
+ from rich.panel import Panel
712
+
713
+ console = get_err_console()
714
+
715
+ # Create a compact onboarding message
716
+ message = (
717
+ "[bold cyan]Welcome to SCC![/bold cyan]\n\n"
718
+ "[yellow]Tip:[/yellow] Use [bold]scc worktree enter[/bold] to switch worktrees.\n"
719
+ "No shell setup required — just type [dim]exit[/dim] to return.\n\n"
720
+ "[dim]Press [bold]?[/bold] anytime for help, or any key to continue...[/dim]"
721
+ )
722
+
723
+ console.print()
724
+ console.print(
725
+ Panel(
726
+ message,
727
+ title="[bold]Getting Started[/bold]",
728
+ border_style="cyan",
729
+ padding=(1, 2),
730
+ )
731
+ )
732
+ console.print()
733
+
734
+ # Wait for any key
735
+ readchar.readkey()