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.
- 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 +259 -0
- scc_cli/cli_admin.py +706 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1454 -0
- scc_cli/cli_org.py +1428 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +892 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -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 +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +604 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1074 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -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/exit_codes.py +55 -0
- scc_cli/git.py +1521 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -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 +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +257 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +260 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -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/sessions.py +425 -0
- scc_cli/setup.py +588 -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 +382 -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 +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +677 -0
- scc_cli/ui/dashboard/loaders.py +395 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +390 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +538 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +675 -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 +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.1.dist-info/METADATA +369 -0
- scc_cli-1.4.1.dist-info/RECORD +113 -0
- scc_cli-1.4.1.dist-info/WHEEL +4 -0
- scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
scc_cli/cli_worktree.py
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Worktree and Session Commands.
|
|
3
|
+
|
|
4
|
+
Commands for managing git worktrees, sessions, and containers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import asdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.prompt import Confirm
|
|
13
|
+
from rich.status import Status
|
|
14
|
+
|
|
15
|
+
from . import config, contexts, deps, docker, git, sessions
|
|
16
|
+
from .cli_common import console, handle_errors, render_responsive_table
|
|
17
|
+
from .cli_helpers import ConfirmItems, confirm_action
|
|
18
|
+
from .constants import WORKTREE_BRANCH_PREFIX
|
|
19
|
+
from .errors import NotAGitRepoError, WorkspaceNotFoundError
|
|
20
|
+
from .json_command import json_command
|
|
21
|
+
from .kinds import Kind
|
|
22
|
+
from .output_mode import is_json_mode
|
|
23
|
+
from .panels import create_info_panel, create_success_panel, create_warning_panel
|
|
24
|
+
from .theme import Indicators, Spinners
|
|
25
|
+
from .ui.gate import InteractivityContext
|
|
26
|
+
from .ui.picker import TeamSwitchRequested, pick_containers, pick_session, pick_worktree
|
|
27
|
+
|
|
28
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
# Worktree App
|
|
30
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
worktree_app = typer.Typer(
|
|
33
|
+
name="worktree",
|
|
34
|
+
help="Manage git worktrees for parallel development.",
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
# Pure Functions
|
|
41
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_worktree_list_data(
|
|
45
|
+
worktrees: list[dict[str, Any]],
|
|
46
|
+
workspace: str,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Build worktree list data for JSON output.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
worktrees: List of worktree dictionaries from git.list_worktrees()
|
|
52
|
+
workspace: Path to the workspace
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary with worktrees, count, and workspace
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"worktrees": worktrees,
|
|
59
|
+
"count": len(worktrees),
|
|
60
|
+
"workspace": workspace,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
# Worktree Commands
|
|
66
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@worktree_app.command("create")
|
|
70
|
+
@handle_errors
|
|
71
|
+
def worktree_create_cmd(
|
|
72
|
+
workspace: str = typer.Argument(..., help="Path to the main repository"),
|
|
73
|
+
name: str = typer.Argument(..., help="Name for the worktree/feature"),
|
|
74
|
+
base_branch: str | None = typer.Option(
|
|
75
|
+
None, "-b", "--base", help="Base branch (default: current)"
|
|
76
|
+
),
|
|
77
|
+
start_claude: bool = typer.Option(
|
|
78
|
+
True, "--start/--no-start", help="Start Claude after creating"
|
|
79
|
+
),
|
|
80
|
+
install_deps: bool = typer.Option(
|
|
81
|
+
False, "--install-deps", help="Install dependencies after creating worktree"
|
|
82
|
+
),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Create a new worktree for parallel development."""
|
|
85
|
+
workspace_path = Path(workspace).expanduser().resolve()
|
|
86
|
+
|
|
87
|
+
if not workspace_path.exists():
|
|
88
|
+
raise WorkspaceNotFoundError(path=str(workspace_path))
|
|
89
|
+
|
|
90
|
+
if not git.is_git_repo(workspace_path):
|
|
91
|
+
raise NotAGitRepoError(path=str(workspace_path))
|
|
92
|
+
|
|
93
|
+
worktree_path = git.create_worktree(workspace_path, name, base_branch)
|
|
94
|
+
|
|
95
|
+
console.print(
|
|
96
|
+
create_success_panel(
|
|
97
|
+
"Worktree Created",
|
|
98
|
+
{
|
|
99
|
+
"Path": str(worktree_path),
|
|
100
|
+
"Branch": f"{WORKTREE_BRANCH_PREFIX}{name}",
|
|
101
|
+
"Base": base_branch or "current branch",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Install dependencies if requested
|
|
107
|
+
if install_deps:
|
|
108
|
+
with Status(
|
|
109
|
+
"[cyan]Installing dependencies...[/cyan]", console=console, spinner=Spinners.SETUP
|
|
110
|
+
):
|
|
111
|
+
success = deps.auto_install_dependencies(worktree_path)
|
|
112
|
+
if success:
|
|
113
|
+
console.print(f"[green]{Indicators.get('PASS')} Dependencies installed[/green]")
|
|
114
|
+
else:
|
|
115
|
+
console.print("[yellow]⚠ Could not detect package manager or install failed[/yellow]")
|
|
116
|
+
|
|
117
|
+
if start_claude:
|
|
118
|
+
console.print()
|
|
119
|
+
if Confirm.ask("[cyan]Start Claude Code in this worktree?[/cyan]", default=True):
|
|
120
|
+
docker.check_docker_available()
|
|
121
|
+
docker_cmd, _ = docker.get_or_create_container(
|
|
122
|
+
workspace=worktree_path,
|
|
123
|
+
branch=f"{WORKTREE_BRANCH_PREFIX}{name}",
|
|
124
|
+
)
|
|
125
|
+
# Load org config for safety-net policy injection
|
|
126
|
+
org_config = config.load_cached_org_config()
|
|
127
|
+
docker.run(docker_cmd, org_config=org_config)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@worktree_app.command("list")
|
|
131
|
+
@json_command(Kind.WORKTREE_LIST)
|
|
132
|
+
@handle_errors
|
|
133
|
+
def worktree_list_cmd(
|
|
134
|
+
workspace: str = typer.Argument(".", help="Path to the repository"),
|
|
135
|
+
interactive: bool = typer.Option(
|
|
136
|
+
False, "-i", "--interactive", help="Interactive mode: select a worktree to work with"
|
|
137
|
+
),
|
|
138
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
139
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""List all worktrees for a repository.
|
|
142
|
+
|
|
143
|
+
With -i/--interactive, select a worktree and print its path
|
|
144
|
+
(useful for piping: cd $(scc worktree list -i))
|
|
145
|
+
"""
|
|
146
|
+
workspace_path = Path(workspace).expanduser().resolve()
|
|
147
|
+
|
|
148
|
+
if not workspace_path.exists():
|
|
149
|
+
raise WorkspaceNotFoundError(path=str(workspace_path))
|
|
150
|
+
|
|
151
|
+
worktree_list = git.list_worktrees(workspace_path)
|
|
152
|
+
|
|
153
|
+
# Convert WorktreeInfo dataclasses to dicts for JSON serialization
|
|
154
|
+
worktree_dicts = [asdict(wt) for wt in worktree_list]
|
|
155
|
+
data = build_worktree_list_data(worktree_dicts, str(workspace_path))
|
|
156
|
+
|
|
157
|
+
if is_json_mode():
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
if not worktree_list:
|
|
161
|
+
console.print(
|
|
162
|
+
create_warning_panel(
|
|
163
|
+
"No Worktrees",
|
|
164
|
+
"No worktrees found for this repository.",
|
|
165
|
+
"Create one with: scc worktree create <repo> <name>",
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Interactive mode: use worktree picker
|
|
171
|
+
if interactive:
|
|
172
|
+
try:
|
|
173
|
+
selected = pick_worktree(
|
|
174
|
+
worktree_list,
|
|
175
|
+
title="Select Worktree",
|
|
176
|
+
subtitle=f"{len(worktree_list)} worktrees in {workspace_path.name}",
|
|
177
|
+
)
|
|
178
|
+
if selected:
|
|
179
|
+
# Print just the path for scripting: cd $(scc worktree list -i)
|
|
180
|
+
print(selected.path)
|
|
181
|
+
except TeamSwitchRequested:
|
|
182
|
+
console.print("[dim]Use 'scc team switch' to change teams[/dim]")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Use the beautiful worktree rendering from git.py
|
|
186
|
+
git.render_worktrees(worktree_list, console)
|
|
187
|
+
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@worktree_app.command("remove")
|
|
192
|
+
@handle_errors
|
|
193
|
+
def worktree_remove_cmd(
|
|
194
|
+
workspace: str = typer.Argument(..., help="Path to the main repository"),
|
|
195
|
+
name: str = typer.Argument(..., help="Name of the worktree to remove"),
|
|
196
|
+
force: bool = typer.Option(
|
|
197
|
+
False, "-f", "--force", help="Force removal even with uncommitted changes"
|
|
198
|
+
),
|
|
199
|
+
yes: bool = typer.Option(False, "-y", "--yes", help="Skip all confirmation prompts"),
|
|
200
|
+
dry_run: bool = typer.Option(
|
|
201
|
+
False, "--dry-run", help="Show what would be removed without removing"
|
|
202
|
+
),
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Remove a worktree.
|
|
205
|
+
|
|
206
|
+
By default, prompts for confirmation if there are uncommitted changes and
|
|
207
|
+
asks whether to delete the associated branch.
|
|
208
|
+
|
|
209
|
+
Use --yes to skip prompts (auto-confirms all actions).
|
|
210
|
+
Use --dry-run to preview what would be removed.
|
|
211
|
+
Use --force to remove even with uncommitted changes (still prompts unless --yes).
|
|
212
|
+
"""
|
|
213
|
+
workspace_path = Path(workspace).expanduser().resolve()
|
|
214
|
+
|
|
215
|
+
if not workspace_path.exists():
|
|
216
|
+
raise WorkspaceNotFoundError(path=str(workspace_path))
|
|
217
|
+
|
|
218
|
+
# cleanup_worktree handles all output including success panels
|
|
219
|
+
git.cleanup_worktree(workspace_path, name, force, console, skip_confirm=yes, dry_run=dry_run)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
# Session Commands
|
|
224
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@handle_errors
|
|
228
|
+
def sessions_cmd(
|
|
229
|
+
limit: int = typer.Option(10, "-n", "--limit", help="Number of sessions to show"),
|
|
230
|
+
select: bool = typer.Option(
|
|
231
|
+
False, "--select", "-s", help="Interactive picker to select a session"
|
|
232
|
+
),
|
|
233
|
+
) -> None:
|
|
234
|
+
"""List recent Claude Code sessions."""
|
|
235
|
+
recent = sessions.list_recent(limit)
|
|
236
|
+
|
|
237
|
+
# Interactive picker mode
|
|
238
|
+
if select and recent:
|
|
239
|
+
try:
|
|
240
|
+
selected = pick_session(
|
|
241
|
+
recent,
|
|
242
|
+
title="Select Session",
|
|
243
|
+
subtitle=f"{len(recent)} recent sessions",
|
|
244
|
+
)
|
|
245
|
+
if selected:
|
|
246
|
+
console.print(f"[green]Selected session:[/green] {selected.get('name', '-')}")
|
|
247
|
+
console.print(f"[dim]Workspace: {selected.get('workspace', '-')}[/dim]")
|
|
248
|
+
except TeamSwitchRequested:
|
|
249
|
+
console.print("[dim]Use 'scc team switch' to change teams[/dim]")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
if not recent:
|
|
253
|
+
console.print(
|
|
254
|
+
create_warning_panel(
|
|
255
|
+
"No Sessions",
|
|
256
|
+
"No recent sessions found.",
|
|
257
|
+
"Start a session with: scc start <workspace>",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Build rows for responsive table
|
|
263
|
+
rows = []
|
|
264
|
+
for s in recent:
|
|
265
|
+
# Shorten workspace path if needed
|
|
266
|
+
ws = s.get("workspace", "-")
|
|
267
|
+
if len(ws) > 40:
|
|
268
|
+
ws = "..." + ws[-37:]
|
|
269
|
+
rows.append([s.get("name", "-"), ws, s.get("last_used", "-"), s.get("team", "-")])
|
|
270
|
+
|
|
271
|
+
render_responsive_table(
|
|
272
|
+
title="Recent Sessions",
|
|
273
|
+
columns=[
|
|
274
|
+
("Session", "cyan"),
|
|
275
|
+
("Workspace", "white"),
|
|
276
|
+
],
|
|
277
|
+
rows=rows,
|
|
278
|
+
wide_columns=[
|
|
279
|
+
("Last Used", "yellow"),
|
|
280
|
+
("Team", "green"),
|
|
281
|
+
],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
286
|
+
# Container Commands
|
|
287
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _list_interactive(containers: list[docker.ContainerInfo]) -> None:
|
|
291
|
+
"""Run interactive container list with action keys.
|
|
292
|
+
|
|
293
|
+
Allows user to navigate containers and press action keys:
|
|
294
|
+
- s: Stop the selected container
|
|
295
|
+
- r: Resume the selected container
|
|
296
|
+
- Enter: Show container details
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
containers: List of ContainerInfo objects.
|
|
300
|
+
"""
|
|
301
|
+
from .ui.formatters import format_container
|
|
302
|
+
from .ui.list_screen import ListMode, ListScreen
|
|
303
|
+
|
|
304
|
+
# Convert to list items
|
|
305
|
+
items = [format_container(c) for c in containers]
|
|
306
|
+
|
|
307
|
+
# Define action handlers
|
|
308
|
+
def stop_container_action(item: Any) -> None:
|
|
309
|
+
"""Stop the selected container."""
|
|
310
|
+
container = item.value
|
|
311
|
+
with Status(f"[cyan]Stopping {container.name}...[/cyan]", console=console):
|
|
312
|
+
success = docker.stop_container(container.id)
|
|
313
|
+
if success:
|
|
314
|
+
console.print(f"[green]{Indicators.get('PASS')} Stopped: {container.name}[/green]")
|
|
315
|
+
else:
|
|
316
|
+
console.print(f"[red]{Indicators.get('FAIL')} Failed to stop: {container.name}[/red]")
|
|
317
|
+
|
|
318
|
+
def resume_container_action(item: Any) -> None:
|
|
319
|
+
"""Resume the selected container."""
|
|
320
|
+
container = item.value
|
|
321
|
+
with Status(f"[cyan]Resuming {container.name}...[/cyan]", console=console):
|
|
322
|
+
success = docker.resume_container(container.id)
|
|
323
|
+
if success:
|
|
324
|
+
console.print(f"[green]{Indicators.get('PASS')} Resumed: {container.name}[/green]")
|
|
325
|
+
else:
|
|
326
|
+
console.print(f"[red]{Indicators.get('FAIL')} Failed to resume: {container.name}[/red]")
|
|
327
|
+
|
|
328
|
+
# Create screen with action handlers
|
|
329
|
+
screen = ListScreen(
|
|
330
|
+
items,
|
|
331
|
+
title="Containers",
|
|
332
|
+
mode=ListMode.ACTIONABLE,
|
|
333
|
+
custom_actions={
|
|
334
|
+
"s": stop_container_action,
|
|
335
|
+
"r": resume_container_action,
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Run the screen (actions execute via callbacks, returns None)
|
|
340
|
+
screen.run()
|
|
341
|
+
|
|
342
|
+
console.print("[dim]Actions: s=stop, r=resume, q=quit[/dim]")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@handle_errors
|
|
346
|
+
def list_cmd(
|
|
347
|
+
interactive: bool = typer.Option(
|
|
348
|
+
False, "-i", "--interactive", help="Interactive mode: select container and take action"
|
|
349
|
+
),
|
|
350
|
+
) -> None:
|
|
351
|
+
"""List all SCC-managed Docker containers.
|
|
352
|
+
|
|
353
|
+
With -i/--interactive, enter actionable mode where you can select a container
|
|
354
|
+
and press action keys:
|
|
355
|
+
- s: Stop the container
|
|
356
|
+
- r: Resume the container
|
|
357
|
+
- Enter: Select and show details
|
|
358
|
+
"""
|
|
359
|
+
with Status("[cyan]Fetching containers...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
360
|
+
containers = docker.list_scc_containers()
|
|
361
|
+
|
|
362
|
+
if not containers:
|
|
363
|
+
console.print(
|
|
364
|
+
create_warning_panel(
|
|
365
|
+
"No Containers",
|
|
366
|
+
"No SCC-managed containers found.",
|
|
367
|
+
"Start a session with: scc start <workspace>",
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
# Interactive mode: use ACTIONABLE list screen
|
|
373
|
+
if interactive:
|
|
374
|
+
_list_interactive(containers)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
# Build rows for table display
|
|
378
|
+
rows = []
|
|
379
|
+
for c in containers:
|
|
380
|
+
# Color status based on state
|
|
381
|
+
status = c.status
|
|
382
|
+
if "Up" in status:
|
|
383
|
+
status = f"[green]{status}[/green]"
|
|
384
|
+
elif "Exited" in status:
|
|
385
|
+
status = f"[yellow]{status}[/yellow]"
|
|
386
|
+
|
|
387
|
+
ws = c.workspace or "-"
|
|
388
|
+
if ws != "-" and len(ws) > 35:
|
|
389
|
+
ws = "..." + ws[-32:]
|
|
390
|
+
|
|
391
|
+
rows.append([c.name, status, ws, c.profile or "-", c.branch or "-"])
|
|
392
|
+
|
|
393
|
+
render_responsive_table(
|
|
394
|
+
title="SCC Containers",
|
|
395
|
+
columns=[
|
|
396
|
+
("Container", "cyan"),
|
|
397
|
+
("Status", "white"),
|
|
398
|
+
],
|
|
399
|
+
rows=rows,
|
|
400
|
+
wide_columns=[
|
|
401
|
+
("Workspace", "dim"),
|
|
402
|
+
("Profile", "yellow"),
|
|
403
|
+
("Branch", "green"),
|
|
404
|
+
],
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
console.print("[dim]Resume with: docker start -ai <container_name>[/dim]")
|
|
408
|
+
console.print("[dim]Or use: scc list -i for interactive mode[/dim]")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@handle_errors
|
|
412
|
+
def stop_cmd(
|
|
413
|
+
container: str = typer.Argument(
|
|
414
|
+
None,
|
|
415
|
+
help="Container name or ID to stop (omit for interactive picker)",
|
|
416
|
+
),
|
|
417
|
+
all_containers: bool = typer.Option(
|
|
418
|
+
False, "--all", "-a", help="Stop all running Claude Code sandboxes"
|
|
419
|
+
),
|
|
420
|
+
interactive: bool = typer.Option(
|
|
421
|
+
False, "-i", "--interactive", help="Use multi-select picker to choose containers"
|
|
422
|
+
),
|
|
423
|
+
yes: bool = typer.Option(
|
|
424
|
+
False, "-y", "--yes", help="Skip confirmation prompt when stopping multiple containers"
|
|
425
|
+
),
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Stop running Docker sandbox(es).
|
|
428
|
+
|
|
429
|
+
Examples:
|
|
430
|
+
scc stop # Interactive picker if multiple running
|
|
431
|
+
scc stop -i # Force interactive multi-select picker
|
|
432
|
+
scc stop claude-sandbox-2025... # Stop specific container
|
|
433
|
+
scc stop --all # Stop all (explicit)
|
|
434
|
+
scc stop --yes # Stop all without confirmation
|
|
435
|
+
"""
|
|
436
|
+
with Status("[cyan]Fetching sandboxes...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
437
|
+
# List Docker Desktop sandbox containers (image: docker/sandbox-templates:claude-code)
|
|
438
|
+
running = docker.list_running_sandboxes()
|
|
439
|
+
|
|
440
|
+
if not running:
|
|
441
|
+
console.print(
|
|
442
|
+
create_info_panel(
|
|
443
|
+
"No Running Sandboxes",
|
|
444
|
+
"No Claude Code sandboxes are currently running.",
|
|
445
|
+
"Start one with: scc -w /path/to/project",
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
# If specific container requested
|
|
451
|
+
if container and not all_containers:
|
|
452
|
+
# Find matching container
|
|
453
|
+
match = None
|
|
454
|
+
for c in running:
|
|
455
|
+
if c.name == container or c.id.startswith(container):
|
|
456
|
+
match = c
|
|
457
|
+
break
|
|
458
|
+
|
|
459
|
+
if not match:
|
|
460
|
+
console.print(
|
|
461
|
+
create_warning_panel(
|
|
462
|
+
"Container Not Found",
|
|
463
|
+
f"No running container matches: {container}",
|
|
464
|
+
"Run 'scc list' to see available containers",
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
raise typer.Exit(1)
|
|
468
|
+
|
|
469
|
+
# Stop the specific container
|
|
470
|
+
with Status(f"[cyan]Stopping {match.name}...[/cyan]", console=console):
|
|
471
|
+
success = docker.stop_container(match.id)
|
|
472
|
+
|
|
473
|
+
if success:
|
|
474
|
+
console.print(create_success_panel("Container Stopped", {"Name": match.name}))
|
|
475
|
+
else:
|
|
476
|
+
console.print(
|
|
477
|
+
create_warning_panel(
|
|
478
|
+
"Stop Failed",
|
|
479
|
+
f"Could not stop container: {match.name}",
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
raise typer.Exit(1)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# Determine which containers to stop
|
|
486
|
+
to_stop = running
|
|
487
|
+
|
|
488
|
+
# Interactive picker mode: when -i flag OR multiple containers without --all/--yes
|
|
489
|
+
ctx = InteractivityContext.create(json_mode=False, no_interactive=False)
|
|
490
|
+
use_picker = interactive or (len(running) > 1 and not all_containers and not yes)
|
|
491
|
+
|
|
492
|
+
if use_picker and ctx.allows_prompt():
|
|
493
|
+
# Use multi-select picker
|
|
494
|
+
try:
|
|
495
|
+
selected = pick_containers(
|
|
496
|
+
running,
|
|
497
|
+
title="Stop Containers",
|
|
498
|
+
subtitle=f"{len(running)} running",
|
|
499
|
+
)
|
|
500
|
+
if not selected:
|
|
501
|
+
console.print("[dim]No containers selected.[/dim]")
|
|
502
|
+
return
|
|
503
|
+
to_stop = selected
|
|
504
|
+
except TeamSwitchRequested:
|
|
505
|
+
console.print("[dim]Use 'scc team switch' to change teams[/dim]")
|
|
506
|
+
return
|
|
507
|
+
elif len(running) > 1 and not yes:
|
|
508
|
+
# Fallback to confirmation prompt (non-TTY or --all without --yes)
|
|
509
|
+
try:
|
|
510
|
+
confirm_action(
|
|
511
|
+
yes=yes,
|
|
512
|
+
prompt=f"Stop {len(running)} running container(s)?",
|
|
513
|
+
items=ConfirmItems(
|
|
514
|
+
title=f"Found {len(running)} running container(s):",
|
|
515
|
+
items=[c.name for c in running],
|
|
516
|
+
),
|
|
517
|
+
)
|
|
518
|
+
except typer.Abort:
|
|
519
|
+
console.print("[dim]Aborted.[/dim]")
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
console.print(f"[cyan]Stopping {len(to_stop)} container(s)...[/cyan]")
|
|
523
|
+
|
|
524
|
+
stopped = []
|
|
525
|
+
failed = []
|
|
526
|
+
for c in to_stop:
|
|
527
|
+
with Status(f"[cyan]Stopping {c.name}...[/cyan]", console=console):
|
|
528
|
+
if docker.stop_container(c.id):
|
|
529
|
+
stopped.append(c.name)
|
|
530
|
+
else:
|
|
531
|
+
failed.append(c.name)
|
|
532
|
+
|
|
533
|
+
if stopped:
|
|
534
|
+
console.print(
|
|
535
|
+
create_success_panel(
|
|
536
|
+
"Containers Stopped",
|
|
537
|
+
{"Stopped": str(len(stopped)), "Names": ", ".join(stopped)},
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if failed:
|
|
542
|
+
console.print(
|
|
543
|
+
create_warning_panel(
|
|
544
|
+
"Some Failed",
|
|
545
|
+
f"Could not stop: {', '.join(failed)}",
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
551
|
+
# Prune Command
|
|
552
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _is_container_stopped(status: str) -> bool:
|
|
556
|
+
"""Check if a container status indicates it's stopped (not running).
|
|
557
|
+
|
|
558
|
+
Docker status strings:
|
|
559
|
+
- "Up 2 hours" / "Up 30 seconds" / "Up 2 hours (healthy)" = running
|
|
560
|
+
- "Exited (0) 2 hours ago" / "Exited (137) 5 seconds ago" = stopped
|
|
561
|
+
- "Created" = created but never started (stopped)
|
|
562
|
+
- "Dead" = dead container (stopped)
|
|
563
|
+
"""
|
|
564
|
+
status_lower = status.lower()
|
|
565
|
+
# Running containers have status starting with "up"
|
|
566
|
+
if status_lower.startswith("up"):
|
|
567
|
+
return False
|
|
568
|
+
# Everything else is stopped: Exited, Created, Dead, etc.
|
|
569
|
+
return True
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@handle_errors
|
|
573
|
+
def prune_cmd(
|
|
574
|
+
yes: bool = typer.Option(
|
|
575
|
+
False, "--yes", "-y", help="Skip confirmation prompt (for scripts/CI)"
|
|
576
|
+
),
|
|
577
|
+
dry_run: bool = typer.Option(
|
|
578
|
+
False, "--dry-run", help="Only show what would be removed, don't prompt"
|
|
579
|
+
),
|
|
580
|
+
) -> None:
|
|
581
|
+
"""Remove stopped SCC containers.
|
|
582
|
+
|
|
583
|
+
Shows stopped containers and prompts for confirmation before removing.
|
|
584
|
+
Use --yes/-y to skip confirmation (for scripts/CI).
|
|
585
|
+
Use --dry-run to only preview without prompting.
|
|
586
|
+
|
|
587
|
+
Only removes STOPPED containers. Running containers are never affected.
|
|
588
|
+
|
|
589
|
+
Examples:
|
|
590
|
+
scc prune # Show containers, prompt to remove
|
|
591
|
+
scc prune --yes # Remove without prompting (CI/scripts)
|
|
592
|
+
scc prune --dry-run # Only show what would be removed
|
|
593
|
+
"""
|
|
594
|
+
with Status("[cyan]Fetching containers...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
595
|
+
# Use _list_all_sandbox_containers to find ALL sandbox containers (by image)
|
|
596
|
+
# This matches how stop_cmd uses list_running_sandboxes (also by image)
|
|
597
|
+
# Containers created by Docker Desktop directly don't have SCC labels
|
|
598
|
+
all_containers = docker._list_all_sandbox_containers()
|
|
599
|
+
|
|
600
|
+
# Filter to only stopped containers
|
|
601
|
+
stopped = [c for c in all_containers if _is_container_stopped(c.status)]
|
|
602
|
+
|
|
603
|
+
if not stopped:
|
|
604
|
+
console.print(
|
|
605
|
+
create_info_panel(
|
|
606
|
+
"Nothing to Prune",
|
|
607
|
+
"No stopped SCC containers found.",
|
|
608
|
+
"Run 'scc stop' first to stop running containers, then prune.",
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
# Handle dry-run mode separately - show what would be removed
|
|
614
|
+
if dry_run:
|
|
615
|
+
console.print(f"[bold]Would remove {len(stopped)} stopped container(s):[/bold]")
|
|
616
|
+
for c in stopped:
|
|
617
|
+
console.print(f" [dim]•[/dim] {c.name}")
|
|
618
|
+
console.print("[dim]Dry run complete. No containers removed.[/dim]")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
# Use centralized confirmation helper for actual removal
|
|
622
|
+
# This handles: --yes, JSON mode, non-interactive mode
|
|
623
|
+
try:
|
|
624
|
+
confirm_action(
|
|
625
|
+
yes=yes,
|
|
626
|
+
dry_run=False,
|
|
627
|
+
prompt=f"Remove {len(stopped)} stopped container(s)?",
|
|
628
|
+
items=ConfirmItems(
|
|
629
|
+
title=f"Found {len(stopped)} stopped container(s):",
|
|
630
|
+
items=[c.name for c in stopped],
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
except typer.Abort:
|
|
634
|
+
console.print("[dim]Aborted.[/dim]")
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
# Actually remove containers
|
|
638
|
+
console.print(f"[cyan]Removing {len(stopped)} stopped container(s)...[/cyan]")
|
|
639
|
+
|
|
640
|
+
removed = []
|
|
641
|
+
failed = []
|
|
642
|
+
for c in stopped:
|
|
643
|
+
with Status(f"[cyan]Removing {c.name}...[/cyan]", console=console):
|
|
644
|
+
if docker.remove_container(c.name):
|
|
645
|
+
removed.append(c.name)
|
|
646
|
+
else:
|
|
647
|
+
failed.append(c.name)
|
|
648
|
+
|
|
649
|
+
if removed:
|
|
650
|
+
console.print(
|
|
651
|
+
create_success_panel(
|
|
652
|
+
"Containers Removed",
|
|
653
|
+
{"Removed": str(len(removed)), "Names": ", ".join(removed)},
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if failed:
|
|
658
|
+
console.print(
|
|
659
|
+
create_warning_panel(
|
|
660
|
+
"Some Failed",
|
|
661
|
+
f"Could not remove: {', '.join(failed)}",
|
|
662
|
+
)
|
|
663
|
+
)
|
|
664
|
+
raise typer.Exit(1)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
668
|
+
# Symmetric Alias Apps (Phase 8)
|
|
669
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
session_app = typer.Typer(
|
|
672
|
+
name="session",
|
|
673
|
+
help="Session management commands.",
|
|
674
|
+
no_args_is_help=True,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
container_app = typer.Typer(
|
|
678
|
+
name="container",
|
|
679
|
+
help="Container management commands.",
|
|
680
|
+
no_args_is_help=True,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@session_app.command("list")
|
|
685
|
+
@handle_errors
|
|
686
|
+
def session_list_cmd(
|
|
687
|
+
limit: int = typer.Option(10, "-n", "--limit", help="Number of sessions to show"),
|
|
688
|
+
select: bool = typer.Option(
|
|
689
|
+
False, "--select", "-s", help="Interactive picker to select a session"
|
|
690
|
+
),
|
|
691
|
+
) -> None:
|
|
692
|
+
"""List recent Claude Code sessions.
|
|
693
|
+
|
|
694
|
+
Alias for 'scc sessions'. Provides symmetric command structure.
|
|
695
|
+
|
|
696
|
+
Examples:
|
|
697
|
+
scc session list
|
|
698
|
+
scc session list -n 20
|
|
699
|
+
scc session list --select
|
|
700
|
+
"""
|
|
701
|
+
# Delegate to existing sessions logic
|
|
702
|
+
recent = sessions.list_recent(limit)
|
|
703
|
+
|
|
704
|
+
# Interactive picker mode
|
|
705
|
+
if select and recent:
|
|
706
|
+
try:
|
|
707
|
+
selected = pick_session(
|
|
708
|
+
recent,
|
|
709
|
+
title="Select Session",
|
|
710
|
+
subtitle=f"{len(recent)} recent sessions",
|
|
711
|
+
)
|
|
712
|
+
if selected:
|
|
713
|
+
console.print(f"[green]Selected session:[/green] {selected.get('name', '-')}")
|
|
714
|
+
console.print(f"[dim]Workspace: {selected.get('workspace', '-')}[/dim]")
|
|
715
|
+
except TeamSwitchRequested:
|
|
716
|
+
console.print("[dim]Use 'scc team switch' to change teams[/dim]")
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
if not recent:
|
|
720
|
+
console.print(
|
|
721
|
+
create_warning_panel(
|
|
722
|
+
"No Sessions",
|
|
723
|
+
"No recent sessions found.",
|
|
724
|
+
"Start a session with: scc start <workspace>",
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
# Build rows for responsive table
|
|
730
|
+
rows = []
|
|
731
|
+
for s in recent:
|
|
732
|
+
# Shorten workspace path if needed
|
|
733
|
+
ws = s.get("workspace", "-")
|
|
734
|
+
if len(ws) > 40:
|
|
735
|
+
ws = "..." + ws[-37:]
|
|
736
|
+
rows.append([s.get("name", "-"), ws, s.get("last_used", "-"), s.get("team", "-")])
|
|
737
|
+
|
|
738
|
+
render_responsive_table(
|
|
739
|
+
title="Recent Sessions",
|
|
740
|
+
columns=[
|
|
741
|
+
("Session", "cyan"),
|
|
742
|
+
("Workspace", "white"),
|
|
743
|
+
],
|
|
744
|
+
rows=rows,
|
|
745
|
+
wide_columns=[
|
|
746
|
+
("Last Used", "yellow"),
|
|
747
|
+
("Team", "green"),
|
|
748
|
+
],
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@container_app.command("list")
|
|
753
|
+
@handle_errors
|
|
754
|
+
def container_list_cmd() -> None:
|
|
755
|
+
"""List all SCC-managed Docker containers.
|
|
756
|
+
|
|
757
|
+
Alias for 'scc list'. Provides symmetric command structure.
|
|
758
|
+
|
|
759
|
+
Examples:
|
|
760
|
+
scc container list
|
|
761
|
+
"""
|
|
762
|
+
# Delegate to existing list logic
|
|
763
|
+
with Status("[cyan]Fetching containers...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
764
|
+
containers = docker.list_scc_containers()
|
|
765
|
+
|
|
766
|
+
if not containers:
|
|
767
|
+
console.print(
|
|
768
|
+
create_warning_panel(
|
|
769
|
+
"No Containers",
|
|
770
|
+
"No SCC-managed containers found.",
|
|
771
|
+
"Start a session with: scc start <workspace>",
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
return
|
|
775
|
+
|
|
776
|
+
# Build rows
|
|
777
|
+
rows = []
|
|
778
|
+
for c in containers:
|
|
779
|
+
# Color status based on state
|
|
780
|
+
status = c.status
|
|
781
|
+
if status == "running":
|
|
782
|
+
status = f"[green]{status}[/green]"
|
|
783
|
+
elif status == "exited":
|
|
784
|
+
status = f"[yellow]{status}[/yellow]"
|
|
785
|
+
|
|
786
|
+
rows.append([c.name, status, c.workspace or "-", c.profile or "-", c.branch or "-"])
|
|
787
|
+
|
|
788
|
+
render_responsive_table(
|
|
789
|
+
title="SCC Containers",
|
|
790
|
+
columns=[
|
|
791
|
+
("Name", "cyan"),
|
|
792
|
+
("Status", "white"),
|
|
793
|
+
],
|
|
794
|
+
rows=rows,
|
|
795
|
+
wide_columns=[
|
|
796
|
+
("Workspace", "dim"),
|
|
797
|
+
("Profile", "yellow"),
|
|
798
|
+
("Branch", "green"),
|
|
799
|
+
],
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
console.print("[dim]Resume with: docker start -ai <container_name>[/dim]")
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
806
|
+
# Context App (Work Context Management)
|
|
807
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
808
|
+
|
|
809
|
+
context_app = typer.Typer(
|
|
810
|
+
name="context",
|
|
811
|
+
help="Work context management commands.",
|
|
812
|
+
no_args_is_help=True,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@context_app.command("clear")
|
|
817
|
+
@handle_errors
|
|
818
|
+
def context_clear_cmd(
|
|
819
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
820
|
+
) -> None:
|
|
821
|
+
"""Clear all recent work contexts from cache.
|
|
822
|
+
|
|
823
|
+
Use this command when the Recent Contexts list shows stale or
|
|
824
|
+
incorrect entries that you want to reset.
|
|
825
|
+
|
|
826
|
+
Examples:
|
|
827
|
+
scc context clear # With confirmation prompt
|
|
828
|
+
scc context clear --yes # Skip confirmation
|
|
829
|
+
"""
|
|
830
|
+
cache_path = contexts._get_contexts_path()
|
|
831
|
+
|
|
832
|
+
# Show current count
|
|
833
|
+
current_count = len(contexts.load_recent_contexts())
|
|
834
|
+
if current_count == 0:
|
|
835
|
+
console.print(
|
|
836
|
+
create_info_panel(
|
|
837
|
+
"No Contexts",
|
|
838
|
+
"No work contexts to clear.",
|
|
839
|
+
"Contexts are created when you run: scc start <workspace>",
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
# Confirm unless --yes (improved what/why/next confirmation)
|
|
845
|
+
if not yes:
|
|
846
|
+
console.print(
|
|
847
|
+
f"[yellow]This will remove {current_count} context(s) from {cache_path}[/yellow]"
|
|
848
|
+
)
|
|
849
|
+
if not Confirm.ask("Continue?"):
|
|
850
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
851
|
+
return
|
|
852
|
+
|
|
853
|
+
# Clear and report
|
|
854
|
+
cleared = contexts.clear_contexts()
|
|
855
|
+
|
|
856
|
+
console.print(
|
|
857
|
+
create_success_panel(
|
|
858
|
+
"Contexts Cleared",
|
|
859
|
+
{
|
|
860
|
+
"Removed": f"{cleared} work context(s)",
|
|
861
|
+
"Cache file": str(cache_path),
|
|
862
|
+
},
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
console.print("[dim]Run 'scc start' to repopulate.[/dim]")
|