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,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Launch render functions - pure output with no business logic.
|
|
3
|
+
|
|
4
|
+
This module contains display/rendering functions extracted from launch.py.
|
|
5
|
+
These are pure output functions that format and display information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ... import git
|
|
17
|
+
from ...cli_common import MAX_DISPLAY_PATH_LENGTH, PATH_TRUNCATE_LENGTH, console
|
|
18
|
+
from ...output_mode import print_human
|
|
19
|
+
from ...theme import Indicators
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .workspace import LaunchContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def warn_if_non_worktree(workspace_path: Path | None, *, json_mode: bool = False) -> None:
|
|
26
|
+
"""Warn when running from a main repo without a worktree.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
workspace_path: Path to the workspace directory, or None.
|
|
30
|
+
json_mode: If True, suppress the warning.
|
|
31
|
+
"""
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
if json_mode or workspace_path is None:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if not git.is_git_repo(workspace_path):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if git.is_worktree(workspace_path):
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
print_human(
|
|
44
|
+
"[yellow]Tip:[/yellow] You're working in the main repo. "
|
|
45
|
+
"For isolation, try: scc worktree create . <feature> or "
|
|
46
|
+
"scc start --worktree <feature>",
|
|
47
|
+
file=sys.stderr,
|
|
48
|
+
highlight=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_dry_run_data(
|
|
53
|
+
workspace_path: Path,
|
|
54
|
+
team: str | None,
|
|
55
|
+
org_config: dict[str, Any] | None,
|
|
56
|
+
project_config: dict[str, Any] | None,
|
|
57
|
+
*,
|
|
58
|
+
entry_dir: Path | None = None,
|
|
59
|
+
mount_root: Path | None = None,
|
|
60
|
+
container_workdir: str | None = None,
|
|
61
|
+
resolution_reason: str | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Build dry run data showing resolved configuration.
|
|
65
|
+
|
|
66
|
+
This pure function assembles configuration information for preview
|
|
67
|
+
without performing any side effects like Docker launch.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
workspace_path: Path to the workspace root (WR).
|
|
71
|
+
team: Selected team profile name (or None).
|
|
72
|
+
org_config: Organization configuration dict (or None).
|
|
73
|
+
project_config: Project-level .scc.yaml config (or None).
|
|
74
|
+
entry_dir: Entry directory (ED), defaults to workspace_path if not provided.
|
|
75
|
+
mount_root: Mount root (MR), defaults to workspace_path if not provided.
|
|
76
|
+
container_workdir: Container workdir (CW), defaults to entry_dir if not provided.
|
|
77
|
+
resolution_reason: Debug explanation for how workspace was resolved.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dictionary with resolved configuration data including path information.
|
|
81
|
+
"""
|
|
82
|
+
plugins: list[dict[str, Any]] = []
|
|
83
|
+
blocked_items: list[str] = []
|
|
84
|
+
|
|
85
|
+
if org_config and team:
|
|
86
|
+
from ... import profiles
|
|
87
|
+
|
|
88
|
+
workspace_for_project = None if project_config is not None else workspace_path
|
|
89
|
+
effective = profiles.compute_effective_config(
|
|
90
|
+
org_config,
|
|
91
|
+
team,
|
|
92
|
+
project_config=project_config,
|
|
93
|
+
workspace_path=workspace_for_project,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for plugin in sorted(effective.plugins):
|
|
97
|
+
plugins.append({"name": plugin, "source": "resolved"})
|
|
98
|
+
|
|
99
|
+
for blocked in effective.blocked_items:
|
|
100
|
+
if blocked.blocked_by:
|
|
101
|
+
blocked_items.append(f"{blocked.item} (blocked by '{blocked.blocked_by}')")
|
|
102
|
+
else:
|
|
103
|
+
blocked_items.append(blocked.item)
|
|
104
|
+
|
|
105
|
+
# Compute defaults for optional path fields
|
|
106
|
+
effective_entry = entry_dir if entry_dir is not None else workspace_path
|
|
107
|
+
effective_mount = mount_root if mount_root is not None else workspace_path
|
|
108
|
+
effective_cw = container_workdir if container_workdir is not None else str(effective_entry)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"workspace_root": str(workspace_path),
|
|
112
|
+
"entry_dir": str(effective_entry),
|
|
113
|
+
"mount_root": str(effective_mount),
|
|
114
|
+
"container_workdir": effective_cw,
|
|
115
|
+
"team": team,
|
|
116
|
+
"plugins": plugins,
|
|
117
|
+
"blocked_items": blocked_items,
|
|
118
|
+
"ready_to_start": len(blocked_items) == 0,
|
|
119
|
+
"resolution_reason": resolution_reason,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def show_launch_panel(
|
|
124
|
+
workspace: Path | None,
|
|
125
|
+
team: str | None,
|
|
126
|
+
session_name: str | None,
|
|
127
|
+
branch: str | None,
|
|
128
|
+
is_resume: bool,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Display launch info panel with session details.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
workspace: Path to the workspace directory, or None.
|
|
134
|
+
team: Team profile name, or None for base profile.
|
|
135
|
+
session_name: Optional session name for identification.
|
|
136
|
+
branch: Current git branch, or None if not in a git repo.
|
|
137
|
+
is_resume: True if resuming an existing container.
|
|
138
|
+
"""
|
|
139
|
+
grid = Table.grid(padding=(0, 2))
|
|
140
|
+
grid.add_column(style="dim", no_wrap=True)
|
|
141
|
+
grid.add_column(style="white")
|
|
142
|
+
|
|
143
|
+
if workspace:
|
|
144
|
+
# Shorten path for display
|
|
145
|
+
display_path = str(workspace)
|
|
146
|
+
if len(display_path) > MAX_DISPLAY_PATH_LENGTH:
|
|
147
|
+
display_path = "..." + display_path[-PATH_TRUNCATE_LENGTH:]
|
|
148
|
+
grid.add_row("Workspace:", display_path)
|
|
149
|
+
|
|
150
|
+
grid.add_row("Team:", team or "standalone")
|
|
151
|
+
|
|
152
|
+
if branch:
|
|
153
|
+
grid.add_row("Branch:", branch)
|
|
154
|
+
|
|
155
|
+
if session_name:
|
|
156
|
+
grid.add_row("Session:", session_name)
|
|
157
|
+
|
|
158
|
+
mode = "[green]Resume existing[/green]" if is_resume else "[cyan]New container[/cyan]"
|
|
159
|
+
grid.add_row("Mode:", mode)
|
|
160
|
+
|
|
161
|
+
panel = Panel(
|
|
162
|
+
grid,
|
|
163
|
+
title="[bold green]Launching Claude Code[/bold green]",
|
|
164
|
+
border_style="green",
|
|
165
|
+
padding=(0, 1),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
console.print()
|
|
169
|
+
console.print(panel)
|
|
170
|
+
console.print()
|
|
171
|
+
console.print("[dim]Starting Docker sandbox...[/dim]")
|
|
172
|
+
console.print()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def show_dry_run_panel(data: dict[str, Any]) -> None:
|
|
176
|
+
"""Display dry run configuration preview.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
data: Dictionary containing workspace paths, team, plugins, and ready_to_start status.
|
|
180
|
+
"""
|
|
181
|
+
grid = Table.grid(padding=(0, 2))
|
|
182
|
+
grid.add_column(style="dim", no_wrap=True)
|
|
183
|
+
grid.add_column(style="white")
|
|
184
|
+
|
|
185
|
+
# Workspace root (WR)
|
|
186
|
+
workspace_root = data.get("workspace_root", data.get("workspace", ""))
|
|
187
|
+
if len(workspace_root) > MAX_DISPLAY_PATH_LENGTH:
|
|
188
|
+
workspace_root = "..." + workspace_root[-PATH_TRUNCATE_LENGTH:]
|
|
189
|
+
grid.add_row("Workspace root:", workspace_root)
|
|
190
|
+
|
|
191
|
+
# Entry dir (ED) - only show if different from workspace_root
|
|
192
|
+
entry_dir = data.get("entry_dir", "")
|
|
193
|
+
if entry_dir and entry_dir != data.get("workspace_root"):
|
|
194
|
+
if len(entry_dir) > MAX_DISPLAY_PATH_LENGTH:
|
|
195
|
+
entry_dir = "..." + entry_dir[-PATH_TRUNCATE_LENGTH:]
|
|
196
|
+
grid.add_row("Entry dir:", entry_dir)
|
|
197
|
+
|
|
198
|
+
# Mount root (MR) - only show if different (worktree expansion)
|
|
199
|
+
mount_root = data.get("mount_root", "")
|
|
200
|
+
if mount_root and mount_root != data.get("workspace_root"):
|
|
201
|
+
if len(mount_root) > MAX_DISPLAY_PATH_LENGTH:
|
|
202
|
+
mount_root = "..." + mount_root[-PATH_TRUNCATE_LENGTH:]
|
|
203
|
+
grid.add_row("Mount root:", f"{mount_root} [dim](worktree)[/dim]")
|
|
204
|
+
|
|
205
|
+
# Container workdir (CW)
|
|
206
|
+
container_workdir = data.get("container_workdir", "")
|
|
207
|
+
if container_workdir:
|
|
208
|
+
if len(container_workdir) > MAX_DISPLAY_PATH_LENGTH:
|
|
209
|
+
container_workdir = "..." + container_workdir[-PATH_TRUNCATE_LENGTH:]
|
|
210
|
+
grid.add_row("Container cwd:", container_workdir)
|
|
211
|
+
|
|
212
|
+
# Team
|
|
213
|
+
grid.add_row("Team:", data.get("team") or "standalone")
|
|
214
|
+
|
|
215
|
+
# Plugins
|
|
216
|
+
plugins = data.get("plugins", [])
|
|
217
|
+
if plugins:
|
|
218
|
+
plugin_list = ", ".join(p.get("name", "unknown") for p in plugins)
|
|
219
|
+
grid.add_row("Plugins:", plugin_list)
|
|
220
|
+
else:
|
|
221
|
+
grid.add_row("Plugins:", "[dim]none[/dim]")
|
|
222
|
+
|
|
223
|
+
# Ready status
|
|
224
|
+
ready = data.get("ready_to_start", True)
|
|
225
|
+
status = (
|
|
226
|
+
f"[green]{Indicators.get('PASS')} Ready to start[/green]"
|
|
227
|
+
if ready
|
|
228
|
+
else f"[red]{Indicators.get('FAIL')} Blocked[/red]"
|
|
229
|
+
)
|
|
230
|
+
grid.add_row("Status:", status)
|
|
231
|
+
|
|
232
|
+
# Blocked items
|
|
233
|
+
blocked = data.get("blocked_items", [])
|
|
234
|
+
if blocked:
|
|
235
|
+
for item in blocked:
|
|
236
|
+
grid.add_row("[red]Blocked:[/red]", item)
|
|
237
|
+
|
|
238
|
+
panel = Panel(
|
|
239
|
+
grid,
|
|
240
|
+
title="[bold cyan]Dry Run Preview[/bold cyan]",
|
|
241
|
+
border_style="cyan",
|
|
242
|
+
padding=(0, 1),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
console.print()
|
|
246
|
+
console.print(panel)
|
|
247
|
+
console.print()
|
|
248
|
+
if ready:
|
|
249
|
+
console.print("[dim]Remove --dry-run to launch[/dim]")
|
|
250
|
+
console.print()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def show_launch_context_panel(ctx: LaunchContext) -> None:
|
|
254
|
+
"""Display enhanced launch context panel with path information.
|
|
255
|
+
|
|
256
|
+
Shows:
|
|
257
|
+
- Workspace root (WR)
|
|
258
|
+
- Entry dir (ED) with relative path if different from WR
|
|
259
|
+
- Mount root (MR) only if different from WR (worktree expansion)
|
|
260
|
+
- Container workdir (CW)
|
|
261
|
+
- Team / branch / session / mode
|
|
262
|
+
"""
|
|
263
|
+
grid = Table.grid(padding=(0, 2))
|
|
264
|
+
grid.add_column(style="dim", no_wrap=True)
|
|
265
|
+
grid.add_column(style="white")
|
|
266
|
+
|
|
267
|
+
# Workspace root (WR)
|
|
268
|
+
grid.add_row("Workspace:", str(ctx.workspace_root))
|
|
269
|
+
|
|
270
|
+
# Entry dir (ED) - show relative if different from WR
|
|
271
|
+
if ctx.entry_dir != ctx.workspace_root:
|
|
272
|
+
rel = ctx.entry_dir_relative
|
|
273
|
+
if rel != ".":
|
|
274
|
+
grid.add_row("Entry dir:", f"{rel} [dim](relative)[/dim]")
|
|
275
|
+
|
|
276
|
+
# Mount root (MR) - only show if different (worktree expansion)
|
|
277
|
+
if ctx.mount_root != ctx.workspace_root:
|
|
278
|
+
grid.add_row("Mount root:", f"{ctx.mount_root} [dim](expanded for worktree)[/dim]")
|
|
279
|
+
|
|
280
|
+
# Container workdir (CW)
|
|
281
|
+
grid.add_row("Container cwd:", ctx.container_workdir)
|
|
282
|
+
|
|
283
|
+
# Team
|
|
284
|
+
grid.add_row("Team:", ctx.team or "standalone")
|
|
285
|
+
|
|
286
|
+
# Branch
|
|
287
|
+
if ctx.branch:
|
|
288
|
+
grid.add_row("Branch:", ctx.branch)
|
|
289
|
+
|
|
290
|
+
# Session
|
|
291
|
+
if ctx.session_name:
|
|
292
|
+
grid.add_row("Session:", ctx.session_name)
|
|
293
|
+
|
|
294
|
+
# Mode
|
|
295
|
+
mode_display = (
|
|
296
|
+
"[green]Resume existing[/green]" if ctx.mode == "resume" else "[cyan]New container[/cyan]"
|
|
297
|
+
)
|
|
298
|
+
grid.add_row("Mode:", mode_display)
|
|
299
|
+
|
|
300
|
+
panel = Panel(
|
|
301
|
+
grid,
|
|
302
|
+
title="[bold green]Launching Claude Code[/bold green]",
|
|
303
|
+
border_style="green",
|
|
304
|
+
padding=(0, 1),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
console.print()
|
|
308
|
+
console.print(panel)
|
|
309
|
+
console.print()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker sandbox launching functions.
|
|
3
|
+
|
|
4
|
+
This module handles Docker container creation and execution for launch command:
|
|
5
|
+
- Container creation or resume
|
|
6
|
+
- Session recording
|
|
7
|
+
- Context recording for Quick Resume
|
|
8
|
+
- Docker process handoff
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from ... import config, docker, git, sessions
|
|
17
|
+
from ...contexts import WorkContext, record_context
|
|
18
|
+
from ...output_mode import print_human
|
|
19
|
+
from .render import show_launch_panel
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def launch_sandbox(
|
|
26
|
+
workspace_path: Path | None,
|
|
27
|
+
mount_path: Path | None,
|
|
28
|
+
team: str | None,
|
|
29
|
+
session_name: str | None,
|
|
30
|
+
current_branch: str | None,
|
|
31
|
+
should_continue_session: bool,
|
|
32
|
+
fresh: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Execute the Docker sandbox with all configurations applied.
|
|
36
|
+
|
|
37
|
+
Handles container creation, session recording, and process handoff.
|
|
38
|
+
Safety-net policy from org config is extracted and mounted read-only.
|
|
39
|
+
"""
|
|
40
|
+
# Load org config for safety-net policy injection
|
|
41
|
+
# This is already cached by _configure_team_settings(), so it's a fast read
|
|
42
|
+
org_config = config.load_cached_org_config()
|
|
43
|
+
|
|
44
|
+
# Prepare sandbox volume for credential persistence
|
|
45
|
+
docker.prepare_sandbox_volume_for_credentials()
|
|
46
|
+
|
|
47
|
+
# Get or create container
|
|
48
|
+
docker_cmd, is_resume = docker.get_or_create_container(
|
|
49
|
+
workspace=mount_path,
|
|
50
|
+
branch=current_branch,
|
|
51
|
+
profile=team,
|
|
52
|
+
force_new=fresh,
|
|
53
|
+
continue_session=should_continue_session,
|
|
54
|
+
env_vars=None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Extract container name for session tracking
|
|
58
|
+
container_name = extract_container_name(docker_cmd, is_resume)
|
|
59
|
+
|
|
60
|
+
# Record session and context
|
|
61
|
+
if workspace_path:
|
|
62
|
+
sessions.record_session(
|
|
63
|
+
workspace=str(workspace_path),
|
|
64
|
+
team=team,
|
|
65
|
+
session_name=session_name,
|
|
66
|
+
container_name=container_name,
|
|
67
|
+
branch=current_branch,
|
|
68
|
+
)
|
|
69
|
+
# Record context for quick resume feature
|
|
70
|
+
# Determine repo root (may be same as workspace for non-worktrees)
|
|
71
|
+
repo_root = git.get_worktree_main_repo(workspace_path) or workspace_path
|
|
72
|
+
worktree_name = workspace_path.name
|
|
73
|
+
context = WorkContext(
|
|
74
|
+
team=team, # Keep None for standalone mode (don't use "base")
|
|
75
|
+
repo_root=repo_root,
|
|
76
|
+
worktree_path=workspace_path,
|
|
77
|
+
worktree_name=worktree_name,
|
|
78
|
+
branch=current_branch, # For Quick Resume branch highlighting
|
|
79
|
+
last_session_id=session_name,
|
|
80
|
+
)
|
|
81
|
+
# Context recording is best-effort - failure should never block sandbox launch
|
|
82
|
+
# (Quick Resume is a convenience feature, not critical path)
|
|
83
|
+
try:
|
|
84
|
+
record_context(context)
|
|
85
|
+
except (OSError, ValueError) as e:
|
|
86
|
+
import logging
|
|
87
|
+
|
|
88
|
+
print_human(
|
|
89
|
+
"[yellow]Warning:[/yellow] Could not save Quick Resume context.",
|
|
90
|
+
highlight=False,
|
|
91
|
+
)
|
|
92
|
+
print_human(f"[dim]{e}[/dim]", highlight=False)
|
|
93
|
+
logging.debug(f"Failed to record context for Quick Resume: {e}")
|
|
94
|
+
|
|
95
|
+
if team:
|
|
96
|
+
try:
|
|
97
|
+
config.set_workspace_team(str(workspace_path), team)
|
|
98
|
+
except (OSError, ValueError) as e:
|
|
99
|
+
import logging
|
|
100
|
+
|
|
101
|
+
print_human(
|
|
102
|
+
"[yellow]Warning:[/yellow] Could not save workspace team preference.",
|
|
103
|
+
highlight=False,
|
|
104
|
+
)
|
|
105
|
+
print_human(f"[dim]{e}[/dim]", highlight=False)
|
|
106
|
+
logging.debug(f"Failed to store workspace team mapping: {e}")
|
|
107
|
+
|
|
108
|
+
# Show launch info and execute
|
|
109
|
+
show_launch_panel(
|
|
110
|
+
workspace=workspace_path,
|
|
111
|
+
team=team,
|
|
112
|
+
session_name=session_name,
|
|
113
|
+
branch=current_branch,
|
|
114
|
+
is_resume=is_resume,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Pass org_config for safety-net policy injection (mounted read-only)
|
|
118
|
+
# Pass workspace_path as container_workdir so Claude's CWD is the actual workspace
|
|
119
|
+
# (mount_path may be a parent directory for worktree support)
|
|
120
|
+
docker.run(docker_cmd, org_config=org_config, container_workdir=workspace_path)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def extract_container_name(docker_cmd: list[str], is_resume: bool) -> str | None:
|
|
124
|
+
"""Extract container name from docker command for session tracking."""
|
|
125
|
+
for idx, arg in enumerate(docker_cmd):
|
|
126
|
+
if arg == "--name" and idx + 1 < len(docker_cmd):
|
|
127
|
+
return docker_cmd[idx + 1]
|
|
128
|
+
if arg.startswith("--name="):
|
|
129
|
+
return arg.split("=", 1)[1]
|
|
130
|
+
|
|
131
|
+
if is_resume and docker_cmd:
|
|
132
|
+
# For resume, container name is the last arg
|
|
133
|
+
if docker_cmd[-1].startswith("scc-"):
|
|
134
|
+
return docker_cmd[-1]
|
|
135
|
+
return None
|