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,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worktree package - commands for git worktrees, sessions, and containers.
|
|
3
|
+
|
|
4
|
+
This package contains the decomposed worktree functionality:
|
|
5
|
+
- app.py: Typer app definitions and command wiring
|
|
6
|
+
- worktree_commands.py: Git worktree management commands
|
|
7
|
+
- container_commands.py: Docker container management commands
|
|
8
|
+
- session_commands.py: Session management commands
|
|
9
|
+
- context_commands.py: Work context management commands
|
|
10
|
+
- _helpers.py: Pure helper functions
|
|
11
|
+
|
|
12
|
+
Public API re-exports for backward compatibility.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Re-export pure helpers for testing
|
|
16
|
+
from ._helpers import build_worktree_list_data, is_container_stopped
|
|
17
|
+
from .app import (
|
|
18
|
+
container_app,
|
|
19
|
+
context_app,
|
|
20
|
+
session_app,
|
|
21
|
+
worktree_app,
|
|
22
|
+
)
|
|
23
|
+
from .container_commands import (
|
|
24
|
+
container_list_cmd,
|
|
25
|
+
list_cmd,
|
|
26
|
+
prune_cmd,
|
|
27
|
+
stop_cmd,
|
|
28
|
+
)
|
|
29
|
+
from .context_commands import context_clear_cmd
|
|
30
|
+
from .session_commands import session_list_cmd, sessions_cmd
|
|
31
|
+
from .worktree_commands import (
|
|
32
|
+
worktree_create_cmd,
|
|
33
|
+
worktree_enter_cmd,
|
|
34
|
+
worktree_list_cmd,
|
|
35
|
+
worktree_prune_cmd,
|
|
36
|
+
worktree_remove_cmd,
|
|
37
|
+
worktree_select_cmd,
|
|
38
|
+
worktree_switch_cmd,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Backward compatibility alias (original name had underscore prefix)
|
|
42
|
+
_is_container_stopped = is_container_stopped
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Typer apps
|
|
46
|
+
"worktree_app",
|
|
47
|
+
"session_app",
|
|
48
|
+
"container_app",
|
|
49
|
+
"context_app",
|
|
50
|
+
# Worktree commands
|
|
51
|
+
"worktree_create_cmd",
|
|
52
|
+
"worktree_list_cmd",
|
|
53
|
+
"worktree_switch_cmd",
|
|
54
|
+
"worktree_select_cmd",
|
|
55
|
+
"worktree_enter_cmd",
|
|
56
|
+
"worktree_remove_cmd",
|
|
57
|
+
"worktree_prune_cmd",
|
|
58
|
+
# Container commands
|
|
59
|
+
"list_cmd",
|
|
60
|
+
"stop_cmd",
|
|
61
|
+
"prune_cmd",
|
|
62
|
+
"container_list_cmd",
|
|
63
|
+
# Session commands
|
|
64
|
+
"sessions_cmd",
|
|
65
|
+
"session_list_cmd",
|
|
66
|
+
# Context commands
|
|
67
|
+
"context_clear_cmd",
|
|
68
|
+
# Pure helpers
|
|
69
|
+
"build_worktree_list_data",
|
|
70
|
+
"is_container_stopped",
|
|
71
|
+
"_is_container_stopped",
|
|
72
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Pure helper functions for worktree commands.
|
|
2
|
+
|
|
3
|
+
This module contains pure, side-effect-free functions extracted from the
|
|
4
|
+
worktree command module. These functions are ideal for unit testing without
|
|
5
|
+
mocks and serve as building blocks for the higher-level command logic.
|
|
6
|
+
|
|
7
|
+
Functions:
|
|
8
|
+
build_worktree_list_data: Build worktree list data for JSON output.
|
|
9
|
+
is_container_stopped: Check if a Docker container status indicates stopped.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_worktree_list_data(
|
|
18
|
+
worktrees: list[dict[str, Any]],
|
|
19
|
+
workspace: str,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Build worktree list data for JSON output.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
worktrees: List of worktree dictionaries from git.list_worktrees()
|
|
25
|
+
workspace: Path to the workspace
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary with worktrees, count, and workspace
|
|
29
|
+
"""
|
|
30
|
+
return {
|
|
31
|
+
"worktrees": worktrees,
|
|
32
|
+
"count": len(worktrees),
|
|
33
|
+
"workspace": workspace,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_container_stopped(status: str) -> bool:
|
|
38
|
+
"""Check if a container status indicates it's stopped (not running).
|
|
39
|
+
|
|
40
|
+
Docker status strings:
|
|
41
|
+
- "Up 2 hours" / "Up 30 seconds" / "Up 2 hours (healthy)" = running
|
|
42
|
+
- "Exited (0) 2 hours ago" / "Exited (137) 5 seconds ago" = stopped
|
|
43
|
+
- "Created" = created but never started (stopped)
|
|
44
|
+
- "Dead" = dead container (stopped)
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
status: The Docker container status string.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if the container is stopped, False if running.
|
|
51
|
+
"""
|
|
52
|
+
status_lower = status.lower()
|
|
53
|
+
# Running containers have status starting with "up"
|
|
54
|
+
if status_lower.startswith("up"):
|
|
55
|
+
return False
|
|
56
|
+
# Everything else is stopped: Exited, Created, Dead, etc.
|
|
57
|
+
return True
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worktree package - Typer app definitions and command wiring.
|
|
3
|
+
|
|
4
|
+
This module contains the Typer app definitions and wires commands from:
|
|
5
|
+
- worktree_commands.py: Git worktree management
|
|
6
|
+
- container_commands.py: Docker container management
|
|
7
|
+
- session_commands.py: Claude Code session management
|
|
8
|
+
- context_commands.py: Work context management
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from .container_commands import (
|
|
16
|
+
container_list_cmd,
|
|
17
|
+
list_cmd,
|
|
18
|
+
)
|
|
19
|
+
from .context_commands import context_clear_cmd
|
|
20
|
+
from .session_commands import session_list_cmd
|
|
21
|
+
from .worktree_commands import (
|
|
22
|
+
worktree_create_cmd,
|
|
23
|
+
worktree_enter_cmd,
|
|
24
|
+
worktree_list_cmd,
|
|
25
|
+
worktree_prune_cmd,
|
|
26
|
+
worktree_remove_cmd,
|
|
27
|
+
worktree_select_cmd,
|
|
28
|
+
worktree_switch_cmd,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Worktree App
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
worktree_app = typer.Typer(
|
|
36
|
+
name="worktree",
|
|
37
|
+
help="""Manage git worktrees for parallel development.
|
|
38
|
+
|
|
39
|
+
Shell Integration (add to ~/.bashrc or ~/.zshrc):
|
|
40
|
+
|
|
41
|
+
wt() { cd "$(scc worktree switch "$@")" || return 1; }
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
wt ^ # Switch to main branch
|
|
46
|
+
wt - # Switch to previous directory
|
|
47
|
+
wt feature-x # Fuzzy match worktree
|
|
48
|
+
""",
|
|
49
|
+
no_args_is_help=False,
|
|
50
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
51
|
+
invoke_without_command=True,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@worktree_app.callback(invoke_without_command=True)
|
|
56
|
+
def worktree_callback(
|
|
57
|
+
ctx: typer.Context,
|
|
58
|
+
workspace: str = typer.Argument(".", help="Path to the repository"),
|
|
59
|
+
interactive: bool = typer.Option(
|
|
60
|
+
False, "-i", "--interactive", help="Interactive mode: select a worktree"
|
|
61
|
+
),
|
|
62
|
+
verbose: bool = typer.Option(
|
|
63
|
+
False, "--verbose", "-v", help="Show git status (staged/modified/untracked)"
|
|
64
|
+
),
|
|
65
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
66
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON output"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""List worktrees by default.
|
|
69
|
+
|
|
70
|
+
This makes `scc worktree` behave like `scc worktree list` for convenience.
|
|
71
|
+
"""
|
|
72
|
+
if ctx.invoked_subcommand is None:
|
|
73
|
+
worktree_list_cmd(
|
|
74
|
+
workspace=workspace,
|
|
75
|
+
interactive=interactive,
|
|
76
|
+
verbose=verbose,
|
|
77
|
+
json_output=json_output,
|
|
78
|
+
pretty=pretty,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Wire worktree commands
|
|
83
|
+
worktree_app.command("create")(worktree_create_cmd)
|
|
84
|
+
worktree_app.command("list")(worktree_list_cmd)
|
|
85
|
+
worktree_app.command("switch")(worktree_switch_cmd)
|
|
86
|
+
worktree_app.command("select")(worktree_select_cmd)
|
|
87
|
+
worktree_app.command("enter")(worktree_enter_cmd)
|
|
88
|
+
worktree_app.command("remove")(worktree_remove_cmd)
|
|
89
|
+
worktree_app.command("prune")(worktree_prune_cmd)
|
|
90
|
+
|
|
91
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
92
|
+
# Session App (Symmetric Alias)
|
|
93
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
session_app = typer.Typer(
|
|
96
|
+
name="session",
|
|
97
|
+
help="Session management commands.",
|
|
98
|
+
no_args_is_help=False,
|
|
99
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
100
|
+
invoke_without_command=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Wire session commands
|
|
104
|
+
session_app.command("list")(session_list_cmd)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@session_app.callback(invoke_without_command=True)
|
|
108
|
+
def session_callback(
|
|
109
|
+
ctx: typer.Context,
|
|
110
|
+
limit: int = typer.Option(10, "-n", "--limit", help="Number of sessions to show"),
|
|
111
|
+
team: str | None = typer.Option(None, "-t", "--team", help="Filter by team"),
|
|
112
|
+
all_teams: bool = typer.Option(
|
|
113
|
+
False, "--all", help="Show sessions for all teams (ignore active team)"
|
|
114
|
+
),
|
|
115
|
+
select: bool = typer.Option(
|
|
116
|
+
False, "--select", "-s", help="Interactive picker to select a session"
|
|
117
|
+
),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""List recent sessions (default).
|
|
120
|
+
|
|
121
|
+
This makes `scc session` behave like `scc session list` for convenience.
|
|
122
|
+
"""
|
|
123
|
+
if ctx.invoked_subcommand is None:
|
|
124
|
+
session_list_cmd(limit=limit, team=team, all_teams=all_teams, select=select)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
# Container App (Symmetric Alias)
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
container_app = typer.Typer(
|
|
132
|
+
name="container",
|
|
133
|
+
help="Container management commands.",
|
|
134
|
+
no_args_is_help=False,
|
|
135
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
136
|
+
invoke_without_command=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Wire container commands
|
|
140
|
+
container_app.command("list")(container_list_cmd)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@container_app.callback(invoke_without_command=True)
|
|
144
|
+
def container_callback(
|
|
145
|
+
ctx: typer.Context,
|
|
146
|
+
interactive: bool = typer.Option(
|
|
147
|
+
False, "-i", "--interactive", help="Interactive mode: select container"
|
|
148
|
+
),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""List containers (default).
|
|
151
|
+
|
|
152
|
+
This makes `scc container` behave like `scc container list` for convenience.
|
|
153
|
+
"""
|
|
154
|
+
if ctx.invoked_subcommand is None:
|
|
155
|
+
list_cmd(interactive=interactive)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
159
|
+
# Context App (Work Context Management)
|
|
160
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
context_app = typer.Typer(
|
|
163
|
+
name="context",
|
|
164
|
+
help="Work context management commands.",
|
|
165
|
+
no_args_is_help=True,
|
|
166
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Wire context commands
|
|
170
|
+
context_app.command("clear")(context_clear_cmd)
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Container commands for Docker sandbox management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.status import Status
|
|
9
|
+
|
|
10
|
+
from ... import docker
|
|
11
|
+
from ...cli_common import console, handle_errors, render_responsive_table
|
|
12
|
+
from ...cli_helpers import ConfirmItems, confirm_action
|
|
13
|
+
from ...panels import create_info_panel, create_success_panel, create_warning_panel
|
|
14
|
+
from ...theme import Indicators, Spinners
|
|
15
|
+
from ...ui.gate import InteractivityContext
|
|
16
|
+
from ...ui.picker import TeamSwitchRequested, pick_containers
|
|
17
|
+
from ._helpers import is_container_stopped
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _list_interactive(containers: list[docker.ContainerInfo]) -> None:
|
|
21
|
+
"""Run interactive container list with action keys.
|
|
22
|
+
|
|
23
|
+
Allows user to navigate containers and press action keys:
|
|
24
|
+
- s: Stop the selected container
|
|
25
|
+
- r: Resume the selected container
|
|
26
|
+
- Enter: Show container details
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
containers: List of ContainerInfo objects.
|
|
30
|
+
"""
|
|
31
|
+
from ...ui.formatters import format_container
|
|
32
|
+
from ...ui.list_screen import ListMode, ListScreen
|
|
33
|
+
|
|
34
|
+
# Convert to list items
|
|
35
|
+
items = [format_container(c) for c in containers]
|
|
36
|
+
|
|
37
|
+
# Define action handlers
|
|
38
|
+
def stop_container_action(item: Any) -> None:
|
|
39
|
+
"""Stop the selected container."""
|
|
40
|
+
container = item.value
|
|
41
|
+
with Status(f"[cyan]Stopping {container.name}...[/cyan]", console=console):
|
|
42
|
+
success = docker.stop_container(container.id)
|
|
43
|
+
if success:
|
|
44
|
+
console.print(f"[green]{Indicators.get('PASS')} Stopped: {container.name}[/green]")
|
|
45
|
+
else:
|
|
46
|
+
console.print(f"[red]{Indicators.get('FAIL')} Failed to stop: {container.name}[/red]")
|
|
47
|
+
|
|
48
|
+
def resume_container_action(item: Any) -> None:
|
|
49
|
+
"""Resume the selected container."""
|
|
50
|
+
container = item.value
|
|
51
|
+
with Status(f"[cyan]Resuming {container.name}...[/cyan]", console=console):
|
|
52
|
+
success = docker.resume_container(container.id)
|
|
53
|
+
if success:
|
|
54
|
+
console.print(f"[green]{Indicators.get('PASS')} Resumed: {container.name}[/green]")
|
|
55
|
+
else:
|
|
56
|
+
console.print(f"[red]{Indicators.get('FAIL')} Failed to resume: {container.name}[/red]")
|
|
57
|
+
|
|
58
|
+
# Create screen with action handlers
|
|
59
|
+
screen = ListScreen(
|
|
60
|
+
items,
|
|
61
|
+
title="Containers",
|
|
62
|
+
mode=ListMode.ACTIONABLE,
|
|
63
|
+
custom_actions={
|
|
64
|
+
"s": stop_container_action,
|
|
65
|
+
"r": resume_container_action,
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Run the screen (actions execute via callbacks, returns None)
|
|
70
|
+
screen.run()
|
|
71
|
+
|
|
72
|
+
console.print("[dim]Actions: s=stop, r=resume, q=quit[/dim]")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@handle_errors
|
|
76
|
+
def list_cmd(
|
|
77
|
+
interactive: bool = typer.Option(
|
|
78
|
+
False, "-i", "--interactive", help="Interactive mode: select container and take action"
|
|
79
|
+
),
|
|
80
|
+
) -> None:
|
|
81
|
+
"""List all SCC-managed Docker containers.
|
|
82
|
+
|
|
83
|
+
With -i/--interactive, enter actionable mode where you can select a container
|
|
84
|
+
and press action keys:
|
|
85
|
+
- s: Stop the container
|
|
86
|
+
- r: Resume the container
|
|
87
|
+
- Enter: Select and show details
|
|
88
|
+
"""
|
|
89
|
+
with Status("[cyan]Fetching containers...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
90
|
+
containers = docker.list_scc_containers()
|
|
91
|
+
|
|
92
|
+
if not containers:
|
|
93
|
+
console.print(
|
|
94
|
+
create_warning_panel(
|
|
95
|
+
"No Containers",
|
|
96
|
+
"No SCC-managed containers found.",
|
|
97
|
+
"Use: scc sessions (recent sessions) or scc start <workspace>",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Interactive mode: use ACTIONABLE list screen
|
|
103
|
+
if interactive:
|
|
104
|
+
_list_interactive(containers)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# Build rows for table display
|
|
108
|
+
rows = []
|
|
109
|
+
for c in containers:
|
|
110
|
+
# Color status based on state
|
|
111
|
+
status = c.status
|
|
112
|
+
if "Up" in status:
|
|
113
|
+
status = f"[green]{status}[/green]"
|
|
114
|
+
elif "Exited" in status:
|
|
115
|
+
status = f"[yellow]{status}[/yellow]"
|
|
116
|
+
|
|
117
|
+
ws = c.workspace or "-"
|
|
118
|
+
if ws != "-" and len(ws) > 35:
|
|
119
|
+
ws = "..." + ws[-32:]
|
|
120
|
+
|
|
121
|
+
rows.append([c.name, status, ws, c.profile or "-", c.branch or "-"])
|
|
122
|
+
|
|
123
|
+
render_responsive_table(
|
|
124
|
+
title="SCC Containers",
|
|
125
|
+
columns=[
|
|
126
|
+
("Container", "cyan"),
|
|
127
|
+
("Status", "white"),
|
|
128
|
+
],
|
|
129
|
+
rows=rows,
|
|
130
|
+
wide_columns=[
|
|
131
|
+
("Workspace", "dim"),
|
|
132
|
+
("Profile", "yellow"),
|
|
133
|
+
("Branch", "green"),
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
console.print("[dim]Resume with: docker start -ai <container_name>[/dim]")
|
|
138
|
+
console.print("[dim]Or use: scc list -i for interactive mode[/dim]")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@handle_errors
|
|
142
|
+
def stop_cmd(
|
|
143
|
+
container: str = typer.Argument(
|
|
144
|
+
None,
|
|
145
|
+
help="Container name or ID to stop (omit for interactive picker)",
|
|
146
|
+
),
|
|
147
|
+
all_containers: bool = typer.Option(
|
|
148
|
+
False, "--all", "-a", help="Stop all running Claude Code sandboxes"
|
|
149
|
+
),
|
|
150
|
+
interactive: bool = typer.Option(
|
|
151
|
+
False, "-i", "--interactive", help="Use multi-select picker to choose containers"
|
|
152
|
+
),
|
|
153
|
+
yes: bool = typer.Option(
|
|
154
|
+
False, "-y", "--yes", help="Skip confirmation prompt when stopping multiple containers"
|
|
155
|
+
),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Stop running Docker sandbox(es).
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
scc stop # Interactive picker if multiple running
|
|
161
|
+
scc stop -i # Force interactive multi-select picker
|
|
162
|
+
scc stop claude-sandbox-2025... # Stop specific container
|
|
163
|
+
scc stop --all # Stop all (explicit)
|
|
164
|
+
scc stop --yes # Stop all without confirmation
|
|
165
|
+
"""
|
|
166
|
+
with Status("[cyan]Fetching sandboxes...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
167
|
+
# List Docker Desktop sandbox containers (image: docker/sandbox-templates:claude-code)
|
|
168
|
+
running = docker.list_running_sandboxes()
|
|
169
|
+
|
|
170
|
+
if not running:
|
|
171
|
+
console.print(
|
|
172
|
+
create_info_panel(
|
|
173
|
+
"No Running Sandboxes",
|
|
174
|
+
"No Claude Code sandboxes are currently running.",
|
|
175
|
+
"Start one with: scc -w /path/to/project",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# If specific container requested
|
|
181
|
+
if container and not all_containers:
|
|
182
|
+
# Find matching container
|
|
183
|
+
match = None
|
|
184
|
+
for c in running:
|
|
185
|
+
if c.name == container or c.id.startswith(container):
|
|
186
|
+
match = c
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if not match:
|
|
190
|
+
console.print(
|
|
191
|
+
create_warning_panel(
|
|
192
|
+
"Container Not Found",
|
|
193
|
+
f"No running container matches: {container}",
|
|
194
|
+
"Run 'scc list' to see available containers",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
raise typer.Exit(1)
|
|
198
|
+
|
|
199
|
+
# Stop the specific container
|
|
200
|
+
with Status(f"[cyan]Stopping {match.name}...[/cyan]", console=console):
|
|
201
|
+
success = docker.stop_container(match.id)
|
|
202
|
+
|
|
203
|
+
if success:
|
|
204
|
+
console.print(create_success_panel("Container Stopped", {"Name": match.name}))
|
|
205
|
+
else:
|
|
206
|
+
console.print(
|
|
207
|
+
create_warning_panel(
|
|
208
|
+
"Stop Failed",
|
|
209
|
+
f"Could not stop container: {match.name}",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
raise typer.Exit(1)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# Determine which containers to stop
|
|
216
|
+
to_stop = running
|
|
217
|
+
|
|
218
|
+
# Interactive picker mode: when -i flag OR multiple containers without --all/--yes
|
|
219
|
+
ctx = InteractivityContext.create(json_mode=False, no_interactive=False)
|
|
220
|
+
use_picker = interactive or (len(running) > 1 and not all_containers and not yes)
|
|
221
|
+
|
|
222
|
+
if use_picker and ctx.allows_prompt():
|
|
223
|
+
# Use multi-select picker
|
|
224
|
+
try:
|
|
225
|
+
selected = pick_containers(
|
|
226
|
+
running,
|
|
227
|
+
title="Stop Containers",
|
|
228
|
+
subtitle=f"{len(running)} running",
|
|
229
|
+
)
|
|
230
|
+
if not selected:
|
|
231
|
+
console.print("[dim]No containers selected.[/dim]")
|
|
232
|
+
return
|
|
233
|
+
to_stop = selected
|
|
234
|
+
except TeamSwitchRequested:
|
|
235
|
+
console.print("[dim]Use 'scc team switch' to change teams[/dim]")
|
|
236
|
+
return
|
|
237
|
+
elif len(running) > 1 and not yes:
|
|
238
|
+
# Fallback to confirmation prompt (non-TTY or --all without --yes)
|
|
239
|
+
try:
|
|
240
|
+
confirm_action(
|
|
241
|
+
yes=yes,
|
|
242
|
+
prompt=f"Stop {len(running)} running container(s)?",
|
|
243
|
+
items=ConfirmItems(
|
|
244
|
+
title=f"Found {len(running)} running container(s):",
|
|
245
|
+
items=[c.name for c in running],
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
except typer.Abort:
|
|
249
|
+
console.print("[dim]Aborted.[/dim]")
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
console.print(f"[cyan]Stopping {len(to_stop)} container(s)...[/cyan]")
|
|
253
|
+
|
|
254
|
+
stopped = []
|
|
255
|
+
failed = []
|
|
256
|
+
for c in to_stop:
|
|
257
|
+
with Status(f"[cyan]Stopping {c.name}...[/cyan]", console=console):
|
|
258
|
+
if docker.stop_container(c.id):
|
|
259
|
+
stopped.append(c.name)
|
|
260
|
+
else:
|
|
261
|
+
failed.append(c.name)
|
|
262
|
+
|
|
263
|
+
if stopped:
|
|
264
|
+
console.print(
|
|
265
|
+
create_success_panel(
|
|
266
|
+
"Containers Stopped",
|
|
267
|
+
{"Stopped": str(len(stopped)), "Names": ", ".join(stopped)},
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if failed:
|
|
272
|
+
console.print(
|
|
273
|
+
create_warning_panel(
|
|
274
|
+
"Some Failed",
|
|
275
|
+
f"Could not stop: {', '.join(failed)}",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@handle_errors
|
|
281
|
+
def prune_cmd(
|
|
282
|
+
yes: bool = typer.Option(
|
|
283
|
+
False, "--yes", "-y", help="Skip confirmation prompt (for scripts/CI)"
|
|
284
|
+
),
|
|
285
|
+
dry_run: bool = typer.Option(
|
|
286
|
+
False, "--dry-run", help="Only show what would be removed, don't prompt"
|
|
287
|
+
),
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Remove stopped SCC containers.
|
|
290
|
+
|
|
291
|
+
Shows stopped containers and prompts for confirmation before removing.
|
|
292
|
+
Use --yes/-y to skip confirmation (for scripts/CI).
|
|
293
|
+
Use --dry-run to only preview without prompting.
|
|
294
|
+
|
|
295
|
+
Only removes STOPPED containers. Running containers are never affected.
|
|
296
|
+
|
|
297
|
+
Examples:
|
|
298
|
+
scc prune # Show containers, prompt to remove
|
|
299
|
+
scc prune --yes # Remove without prompting (CI/scripts)
|
|
300
|
+
scc prune --dry-run # Only show what would be removed
|
|
301
|
+
"""
|
|
302
|
+
with Status("[cyan]Fetching containers...[/cyan]", console=console, spinner=Spinners.DOCKER):
|
|
303
|
+
# Use _list_all_sandbox_containers to find ALL sandbox containers (by image)
|
|
304
|
+
# This matches how stop_cmd uses list_running_sandboxes (also by image)
|
|
305
|
+
# Containers created by Docker Desktop directly don't have SCC labels
|
|
306
|
+
all_containers = docker._list_all_sandbox_containers()
|
|
307
|
+
|
|
308
|
+
# Filter to only stopped containers
|
|
309
|
+
stopped = [c for c in all_containers if is_container_stopped(c.status)]
|
|
310
|
+
|
|
311
|
+
if not stopped:
|
|
312
|
+
console.print(
|
|
313
|
+
create_info_panel(
|
|
314
|
+
"Nothing to Prune",
|
|
315
|
+
"No stopped SCC containers found.",
|
|
316
|
+
"Run 'scc stop' first to stop running containers, then prune.",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# Handle dry-run mode separately - show what would be removed
|
|
322
|
+
if dry_run:
|
|
323
|
+
console.print(f"[bold]Would remove {len(stopped)} stopped container(s):[/bold]")
|
|
324
|
+
for c in stopped:
|
|
325
|
+
console.print(f" [dim]•[/dim] {c.name}")
|
|
326
|
+
console.print("[dim]Dry run complete. No containers removed.[/dim]")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Use centralized confirmation helper for actual removal
|
|
330
|
+
# This handles: --yes, JSON mode, non-interactive mode
|
|
331
|
+
try:
|
|
332
|
+
confirm_action(
|
|
333
|
+
yes=yes,
|
|
334
|
+
dry_run=False,
|
|
335
|
+
prompt=f"Remove {len(stopped)} stopped container(s)?",
|
|
336
|
+
items=ConfirmItems(
|
|
337
|
+
title=f"Found {len(stopped)} stopped container(s):",
|
|
338
|
+
items=[c.name for c in stopped],
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
except typer.Abort:
|
|
342
|
+
console.print("[dim]Aborted.[/dim]")
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Actually remove containers
|
|
346
|
+
console.print(f"[cyan]Removing {len(stopped)} stopped container(s)...[/cyan]")
|
|
347
|
+
|
|
348
|
+
removed = []
|
|
349
|
+
failed = []
|
|
350
|
+
for c in stopped:
|
|
351
|
+
with Status(f"[cyan]Removing {c.name}...[/cyan]", console=console):
|
|
352
|
+
if docker.remove_container(c.name):
|
|
353
|
+
removed.append(c.name)
|
|
354
|
+
else:
|
|
355
|
+
failed.append(c.name)
|
|
356
|
+
|
|
357
|
+
if removed:
|
|
358
|
+
console.print(
|
|
359
|
+
create_success_panel(
|
|
360
|
+
"Containers Removed",
|
|
361
|
+
{"Removed": str(len(removed)), "Names": ", ".join(removed)},
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if failed:
|
|
366
|
+
console.print(
|
|
367
|
+
create_warning_panel(
|
|
368
|
+
"Some Failed",
|
|
369
|
+
f"Could not remove: {', '.join(failed)}",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
raise typer.Exit(1)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@handle_errors
|
|
376
|
+
def container_list_cmd() -> None:
|
|
377
|
+
"""List all SCC-managed Docker containers.
|
|
378
|
+
|
|
379
|
+
Alias for 'scc list'. Provides symmetric command structure.
|
|
380
|
+
|
|
381
|
+
Examples:
|
|
382
|
+
scc container list
|
|
383
|
+
"""
|
|
384
|
+
# Delegate to list_cmd to avoid duplication and ensure consistent behavior
|
|
385
|
+
list_cmd(interactive=False)
|