scc-cli 1.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

Files changed (153) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,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)