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.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +383 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- 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()
|