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,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workspace validation and preparation functions.
|
|
3
|
+
|
|
4
|
+
This module handles workspace-related operations for the launch command:
|
|
5
|
+
- Path validation and resolution
|
|
6
|
+
- Worktree creation and mounting
|
|
7
|
+
- Dependency installation
|
|
8
|
+
- Team-workspace association resolution
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.status import Status
|
|
20
|
+
|
|
21
|
+
from ... import config, deps, git
|
|
22
|
+
from ... import platform as platform_module
|
|
23
|
+
from ...cli_common import console
|
|
24
|
+
from ...confirm import Confirm
|
|
25
|
+
from ...core.constants import WORKTREE_BRANCH_PREFIX
|
|
26
|
+
from ...core.errors import NotAGitRepoError, WorkspaceNotFoundError
|
|
27
|
+
from ...core.exit_codes import EXIT_CANCELLED
|
|
28
|
+
from ...core.workspace import ResolverResult
|
|
29
|
+
from ...output_mode import print_human
|
|
30
|
+
from ...panels import create_info_panel, create_success_panel, create_warning_panel
|
|
31
|
+
from ...theme import Indicators, Spinners
|
|
32
|
+
from ...ui.gate import is_interactive_allowed
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LaunchContext:
|
|
40
|
+
"""Display-focused launch context wrapping ResolverResult.
|
|
41
|
+
|
|
42
|
+
This dataclass is used for rendering the launch panel and JSON output.
|
|
43
|
+
It combines resolver results with session-specific information.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
resolver_result: ResolverResult
|
|
47
|
+
team: str | None
|
|
48
|
+
branch: str | None
|
|
49
|
+
session_name: str | None
|
|
50
|
+
mode: str # "new" or "resume"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def workspace_root(self) -> Path:
|
|
54
|
+
"""Workspace root (WR)."""
|
|
55
|
+
return self.resolver_result.workspace_root
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def entry_dir(self) -> Path:
|
|
59
|
+
"""Entry directory (ED)."""
|
|
60
|
+
return self.resolver_result.entry_dir
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def mount_root(self) -> Path:
|
|
64
|
+
"""Mount root (MR)."""
|
|
65
|
+
return self.resolver_result.mount_root
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def container_workdir(self) -> str:
|
|
69
|
+
"""Container working directory (CW)."""
|
|
70
|
+
return self.resolver_result.container_workdir
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def entry_dir_relative(self) -> str:
|
|
74
|
+
"""Entry dir path relative to workspace root."""
|
|
75
|
+
try:
|
|
76
|
+
return str(self.entry_dir.relative_to(self.workspace_root))
|
|
77
|
+
except ValueError:
|
|
78
|
+
return str(self.entry_dir)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_mount_expanded(self) -> bool:
|
|
82
|
+
"""Whether mount was expanded for worktree support."""
|
|
83
|
+
return self.resolver_result.is_mount_expanded
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict[str, Any]:
|
|
86
|
+
"""Convert to dictionary for JSON output."""
|
|
87
|
+
return {
|
|
88
|
+
"workspace_root": str(self.workspace_root),
|
|
89
|
+
"entry_dir": str(self.entry_dir),
|
|
90
|
+
"entry_dir_relative": self.entry_dir_relative,
|
|
91
|
+
"mount_root": str(self.mount_root),
|
|
92
|
+
"container_workdir": self.container_workdir,
|
|
93
|
+
"is_mount_expanded": self.is_mount_expanded,
|
|
94
|
+
"team": self.team,
|
|
95
|
+
"branch": self.branch,
|
|
96
|
+
"session_name": self.session_name,
|
|
97
|
+
"mode": self.mode,
|
|
98
|
+
"reason": self.resolver_result.reason,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_and_resolve_workspace(
|
|
103
|
+
workspace: str | None,
|
|
104
|
+
*,
|
|
105
|
+
no_interactive: bool = False,
|
|
106
|
+
allow_suspicious: bool = False,
|
|
107
|
+
json_mode: bool = False,
|
|
108
|
+
) -> Path | None:
|
|
109
|
+
"""
|
|
110
|
+
Validate workspace path and handle platform-specific warnings.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
workspace: Workspace path string.
|
|
114
|
+
no_interactive: If True, fail fast instead of prompting.
|
|
115
|
+
allow_suspicious: If True, allow suspicious workspaces in non-interactive mode.
|
|
116
|
+
json_mode: If True, output is JSON (suppress Rich panels).
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
WorkspaceNotFoundError: If workspace path doesn't exist.
|
|
120
|
+
UsageError: If workspace is suspicious in non-interactive mode without --allow-suspicious-workspace.
|
|
121
|
+
typer.Exit: If user declines to continue after warnings.
|
|
122
|
+
"""
|
|
123
|
+
from ...core.errors import UsageError
|
|
124
|
+
from ...services.workspace.suspicious import get_suspicious_reason, is_suspicious_directory
|
|
125
|
+
|
|
126
|
+
if workspace is None:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
workspace_path = Path(workspace).expanduser().resolve()
|
|
130
|
+
|
|
131
|
+
if not workspace_path.exists():
|
|
132
|
+
raise WorkspaceNotFoundError(path=str(workspace_path))
|
|
133
|
+
|
|
134
|
+
# Check for suspicious workspace (home, /tmp, system directories)
|
|
135
|
+
if is_suspicious_directory(workspace_path):
|
|
136
|
+
reason = get_suspicious_reason(workspace_path) or "Suspicious directory"
|
|
137
|
+
|
|
138
|
+
# If --allow-suspicious-workspace is set, skip confirmation entirely
|
|
139
|
+
if allow_suspicious:
|
|
140
|
+
print_human(
|
|
141
|
+
f"[yellow]Warning:[/yellow] {reason}",
|
|
142
|
+
file=sys.stderr,
|
|
143
|
+
highlight=False,
|
|
144
|
+
)
|
|
145
|
+
elif is_interactive_allowed(json_mode=json_mode, no_interactive_flag=no_interactive):
|
|
146
|
+
# Interactive mode: warn but allow user to continue
|
|
147
|
+
console.print()
|
|
148
|
+
console.print(
|
|
149
|
+
create_warning_panel(
|
|
150
|
+
"Suspicious Workspace",
|
|
151
|
+
reason,
|
|
152
|
+
"Consider using a project-specific directory instead.",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
console.print()
|
|
156
|
+
if not Confirm.ask("[cyan]Continue anyway?[/cyan]", default=True):
|
|
157
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
158
|
+
raise typer.Exit(EXIT_CANCELLED)
|
|
159
|
+
else:
|
|
160
|
+
# Non-interactive mode without flag: block
|
|
161
|
+
raise UsageError(
|
|
162
|
+
user_message=f"Refusing to start in suspicious directory: {workspace_path}\n → {reason}",
|
|
163
|
+
suggested_action=(
|
|
164
|
+
"Either:\n"
|
|
165
|
+
f" • Run: scc start --allow-suspicious-workspace {workspace_path}\n"
|
|
166
|
+
" • Run: scc start --interactive (to choose a different workspace)\n"
|
|
167
|
+
" • Run from a project directory inside a git repository"
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# WSL2 performance warning
|
|
172
|
+
if platform_module.is_wsl2():
|
|
173
|
+
is_optimal, warning = platform_module.check_path_performance(workspace_path)
|
|
174
|
+
if not is_optimal and warning:
|
|
175
|
+
print_human(
|
|
176
|
+
"[yellow]Warning:[/yellow] Workspace is on the Windows filesystem."
|
|
177
|
+
" Performance may be slow.",
|
|
178
|
+
file=sys.stderr,
|
|
179
|
+
highlight=False,
|
|
180
|
+
)
|
|
181
|
+
if is_interactive_allowed(no_interactive_flag=no_interactive):
|
|
182
|
+
console.print()
|
|
183
|
+
console.print(
|
|
184
|
+
create_warning_panel(
|
|
185
|
+
"Performance Warning",
|
|
186
|
+
"Your workspace is on the Windows filesystem.",
|
|
187
|
+
"For better performance, move to ~/projects inside WSL.",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
console.print()
|
|
191
|
+
if not Confirm.ask("[cyan]Continue anyway?[/cyan]", default=True):
|
|
192
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
193
|
+
raise typer.Exit(EXIT_CANCELLED)
|
|
194
|
+
|
|
195
|
+
return workspace_path
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def prepare_workspace(
|
|
199
|
+
workspace_path: Path | None,
|
|
200
|
+
worktree_name: str | None,
|
|
201
|
+
install_deps: bool,
|
|
202
|
+
) -> Path | None:
|
|
203
|
+
"""
|
|
204
|
+
Prepare workspace: create worktree, install deps, check git safety.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The (possibly updated) workspace path after worktree creation.
|
|
208
|
+
"""
|
|
209
|
+
if workspace_path is None:
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# Handle worktree creation
|
|
213
|
+
if worktree_name:
|
|
214
|
+
workspace_path = git.create_worktree(workspace_path, worktree_name)
|
|
215
|
+
console.print(
|
|
216
|
+
create_success_panel(
|
|
217
|
+
"Worktree Created",
|
|
218
|
+
{
|
|
219
|
+
"Path": str(workspace_path),
|
|
220
|
+
"Branch": f"{WORKTREE_BRANCH_PREFIX}{worktree_name}",
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Install dependencies if requested
|
|
226
|
+
if install_deps:
|
|
227
|
+
with Status(
|
|
228
|
+
"[cyan]Installing dependencies...[/cyan]", console=console, spinner=Spinners.SETUP
|
|
229
|
+
):
|
|
230
|
+
success = deps.auto_install_dependencies(workspace_path)
|
|
231
|
+
if success:
|
|
232
|
+
console.print(f"[green]{Indicators.get('PASS')} Dependencies installed[/green]")
|
|
233
|
+
else:
|
|
234
|
+
console.print("[yellow]⚠ Could not detect package manager or install failed[/yellow]")
|
|
235
|
+
|
|
236
|
+
# Check git safety (handles protected branch warnings)
|
|
237
|
+
if workspace_path.exists():
|
|
238
|
+
if not git.check_branch_safety(workspace_path, console):
|
|
239
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
240
|
+
raise typer.Exit(EXIT_CANCELLED)
|
|
241
|
+
|
|
242
|
+
return workspace_path
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def resolve_workspace_team(
|
|
246
|
+
workspace_path: Path | None,
|
|
247
|
+
team: str | None,
|
|
248
|
+
cfg: dict[str, Any],
|
|
249
|
+
*,
|
|
250
|
+
json_mode: bool = False,
|
|
251
|
+
standalone: bool = False,
|
|
252
|
+
no_interactive: bool = False,
|
|
253
|
+
) -> str | None:
|
|
254
|
+
"""Resolve team selection with proper priority.
|
|
255
|
+
|
|
256
|
+
Resolution priority:
|
|
257
|
+
1. Explicit --team flag (if provided)
|
|
258
|
+
2. selected_profile (explicit user choice via `scc team switch`)
|
|
259
|
+
3. Workspace-pinned team (auto-saved from previous session)
|
|
260
|
+
|
|
261
|
+
In interactive mode, prompts user when pinned team differs from selected profile.
|
|
262
|
+
In non-interactive mode, prefers selected_profile (explicit user action).
|
|
263
|
+
"""
|
|
264
|
+
if standalone or workspace_path is None:
|
|
265
|
+
return team
|
|
266
|
+
|
|
267
|
+
if team:
|
|
268
|
+
return team
|
|
269
|
+
|
|
270
|
+
pinned_team = config.get_workspace_team_from_config(cfg, workspace_path)
|
|
271
|
+
selected_profile: str | None = cfg.get("selected_profile")
|
|
272
|
+
|
|
273
|
+
if pinned_team and selected_profile and pinned_team != selected_profile:
|
|
274
|
+
if is_interactive_allowed(json_mode=json_mode, no_interactive_flag=no_interactive):
|
|
275
|
+
# Default to selected_profile (explicit user choice) for consistency
|
|
276
|
+
# with non-interactive mode behavior
|
|
277
|
+
message = (
|
|
278
|
+
f"[yellow]Note:[/yellow] This workspace was last used with team "
|
|
279
|
+
f"'[cyan]{pinned_team}[/cyan]', but your current profile is "
|
|
280
|
+
f"'[cyan]{selected_profile}[/cyan]'.\n"
|
|
281
|
+
f"Use workspace's previous team '{pinned_team}' instead?"
|
|
282
|
+
)
|
|
283
|
+
if Confirm.ask(message, default=False):
|
|
284
|
+
return pinned_team
|
|
285
|
+
return selected_profile
|
|
286
|
+
|
|
287
|
+
# Non-interactive: prefer selected_profile (explicit user choice via `scc team switch`)
|
|
288
|
+
# over workspace pinning (auto-saved from previous session)
|
|
289
|
+
if not json_mode:
|
|
290
|
+
print_human(
|
|
291
|
+
"[yellow]Notice:[/yellow] "
|
|
292
|
+
f"Workspace was last used with team '{pinned_team}', "
|
|
293
|
+
f"but current profile is '{selected_profile}'. Using '{selected_profile}'.",
|
|
294
|
+
file=sys.stderr,
|
|
295
|
+
highlight=False,
|
|
296
|
+
)
|
|
297
|
+
return selected_profile
|
|
298
|
+
|
|
299
|
+
if pinned_team:
|
|
300
|
+
return pinned_team
|
|
301
|
+
|
|
302
|
+
return selected_profile
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def resolve_mount_and_branch(
|
|
306
|
+
workspace_path: Path | None,
|
|
307
|
+
*,
|
|
308
|
+
json_mode: bool = False,
|
|
309
|
+
) -> tuple[Path | None, str | None]:
|
|
310
|
+
"""
|
|
311
|
+
Resolve mount path for worktrees and get current branch.
|
|
312
|
+
|
|
313
|
+
For worktrees, expands mount scope to include main repo.
|
|
314
|
+
Returns (mount_path, current_branch).
|
|
315
|
+
"""
|
|
316
|
+
if workspace_path is None:
|
|
317
|
+
return None, None
|
|
318
|
+
|
|
319
|
+
# Get current branch
|
|
320
|
+
current_branch = None
|
|
321
|
+
try:
|
|
322
|
+
current_branch = git.get_current_branch(workspace_path)
|
|
323
|
+
except (NotAGitRepoError, OSError):
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
# Handle worktree mounting
|
|
327
|
+
mount_path, is_expanded = git.get_workspace_mount_path(workspace_path)
|
|
328
|
+
if is_expanded and not json_mode:
|
|
329
|
+
console.print()
|
|
330
|
+
console.print(
|
|
331
|
+
create_info_panel(
|
|
332
|
+
"Worktree Detected",
|
|
333
|
+
f"Mounting parent directory for worktree support:\n{mount_path}",
|
|
334
|
+
"Both worktree and main repo will be accessible",
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
console.print()
|
|
338
|
+
|
|
339
|
+
return mount_path, current_branch
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Org package - organization configuration management commands.
|
|
3
|
+
|
|
4
|
+
This package contains the decomposed org functionality:
|
|
5
|
+
- app.py: Typer app definitions and command wiring
|
|
6
|
+
- validate_cmd.py: Schema and semantic validation
|
|
7
|
+
- update_cmd.py: Organization and team config refresh
|
|
8
|
+
- schema_cmd.py: Print bundled schema
|
|
9
|
+
- status_cmd.py: Current organization status
|
|
10
|
+
- import_cmd.py: Import organization config from URL
|
|
11
|
+
- init_cmd.py: Generate config skeleton from templates
|
|
12
|
+
- _builders.py: Pure builder functions
|
|
13
|
+
|
|
14
|
+
Public API re-exports for backward compatibility.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Re-export pure builders for testing
|
|
18
|
+
from ._builders import (
|
|
19
|
+
build_import_preview_data,
|
|
20
|
+
build_status_data,
|
|
21
|
+
build_update_data,
|
|
22
|
+
build_validation_data,
|
|
23
|
+
check_semantic_errors,
|
|
24
|
+
)
|
|
25
|
+
from .app import org_app
|
|
26
|
+
from .import_cmd import org_import_cmd
|
|
27
|
+
from .init_cmd import org_init_cmd
|
|
28
|
+
from .schema_cmd import org_schema_cmd
|
|
29
|
+
from .status_cmd import org_status_cmd
|
|
30
|
+
from .update_cmd import org_update_cmd
|
|
31
|
+
from .validate_cmd import org_validate_cmd
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Typer app
|
|
35
|
+
"org_app",
|
|
36
|
+
# Commands
|
|
37
|
+
"org_validate_cmd",
|
|
38
|
+
"org_update_cmd",
|
|
39
|
+
"org_schema_cmd",
|
|
40
|
+
"org_status_cmd",
|
|
41
|
+
"org_import_cmd",
|
|
42
|
+
"org_init_cmd",
|
|
43
|
+
# Pure builders
|
|
44
|
+
"build_validation_data",
|
|
45
|
+
"check_semantic_errors",
|
|
46
|
+
"build_import_preview_data",
|
|
47
|
+
"build_status_data",
|
|
48
|
+
"build_update_data",
|
|
49
|
+
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Pure builder functions for org commands.
|
|
2
|
+
|
|
3
|
+
These functions build data structures for JSON output and display.
|
|
4
|
+
They have no side effects and are ideal for unit testing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_validation_data(
|
|
16
|
+
source: str,
|
|
17
|
+
schema_errors: list[str],
|
|
18
|
+
semantic_errors: list[str],
|
|
19
|
+
schema_version: str,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Build validation result data for JSON output.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
source: Path or URL of validated config
|
|
25
|
+
schema_errors: List of JSON schema validation errors
|
|
26
|
+
semantic_errors: List of semantic validation errors
|
|
27
|
+
schema_version: Schema version used for validation
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary with validation results
|
|
31
|
+
"""
|
|
32
|
+
is_valid = len(schema_errors) == 0 and len(semantic_errors) == 0
|
|
33
|
+
return {
|
|
34
|
+
"source": source,
|
|
35
|
+
"schema_version": schema_version,
|
|
36
|
+
"valid": is_valid,
|
|
37
|
+
"schema_errors": schema_errors,
|
|
38
|
+
"semantic_errors": semantic_errors,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_semantic_errors(config: dict[str, Any]) -> list[str]:
|
|
43
|
+
"""Check for semantic errors beyond JSON schema validation.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: Parsed organization config
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of semantic error messages
|
|
50
|
+
"""
|
|
51
|
+
errors: list[str] = []
|
|
52
|
+
org = config.get("organization", {})
|
|
53
|
+
|
|
54
|
+
# Profiles are at TOP LEVEL of config as a DICT (not under "organization")
|
|
55
|
+
# Dict keys are unique, so no duplicate name checking needed
|
|
56
|
+
profiles = config.get("profiles", {})
|
|
57
|
+
profile_names: list[str] = list(profiles.keys())
|
|
58
|
+
|
|
59
|
+
# Check if default_profile references existing profile
|
|
60
|
+
default_profile = org.get("default_profile")
|
|
61
|
+
if default_profile and default_profile not in profile_names:
|
|
62
|
+
errors.append(f"default_profile '{default_profile}' references non-existent profile")
|
|
63
|
+
|
|
64
|
+
return errors
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_import_preview_data(
|
|
68
|
+
source: str,
|
|
69
|
+
resolved_url: str,
|
|
70
|
+
config: dict[str, Any],
|
|
71
|
+
validation_errors: list[str],
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""Build import preview data for display and JSON output.
|
|
74
|
+
|
|
75
|
+
Pure function that assembles preview information for an organization config
|
|
76
|
+
before it is imported.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
source: Original source string (URL or shorthand like github:org/repo)
|
|
80
|
+
resolved_url: Resolved URL after shorthand expansion
|
|
81
|
+
config: Parsed organization config dict
|
|
82
|
+
validation_errors: List of validation error messages
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with preview information including org details and validation status
|
|
86
|
+
"""
|
|
87
|
+
org_data = config.get("organization", {})
|
|
88
|
+
profiles_dict = config.get("profiles", {})
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"source": source,
|
|
92
|
+
"resolved_url": resolved_url,
|
|
93
|
+
"organization": {
|
|
94
|
+
"name": org_data.get("name", ""),
|
|
95
|
+
"id": org_data.get("id", ""),
|
|
96
|
+
"contact": org_data.get("contact", ""),
|
|
97
|
+
},
|
|
98
|
+
"valid": len(validation_errors) == 0,
|
|
99
|
+
"validation_errors": validation_errors,
|
|
100
|
+
"available_profiles": list(profiles_dict.keys()),
|
|
101
|
+
"schema_version": config.get("schema_version", ""),
|
|
102
|
+
"min_cli_version": config.get("min_cli_version", ""),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_status_data(
|
|
107
|
+
user_config: dict[str, Any],
|
|
108
|
+
org_config: dict[str, Any] | None,
|
|
109
|
+
cache_meta: dict[str, Any] | None,
|
|
110
|
+
) -> dict[str, Any]:
|
|
111
|
+
"""Build status data for JSON output and display.
|
|
112
|
+
|
|
113
|
+
Pure function that assembles status information from various sources.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
user_config: User configuration dict
|
|
117
|
+
org_config: Cached organization config (may be None)
|
|
118
|
+
cache_meta: Cache metadata dict (may be None)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary with complete status information
|
|
122
|
+
"""
|
|
123
|
+
# Import here to avoid circular imports at module level
|
|
124
|
+
from ...remote import is_cache_valid
|
|
125
|
+
from ...validate import check_version_compatibility
|
|
126
|
+
|
|
127
|
+
# Determine mode
|
|
128
|
+
is_standalone = user_config.get("standalone", False) or not user_config.get(
|
|
129
|
+
"organization_source"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if is_standalone:
|
|
133
|
+
return {
|
|
134
|
+
"mode": "standalone",
|
|
135
|
+
"organization": None,
|
|
136
|
+
"cache": None,
|
|
137
|
+
"version_compatibility": None,
|
|
138
|
+
"selected_profile": None,
|
|
139
|
+
"available_profiles": [],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Organization connected mode
|
|
143
|
+
org_source = user_config.get("organization_source", {})
|
|
144
|
+
source_url = org_source.get("url", "")
|
|
145
|
+
|
|
146
|
+
# Organization info
|
|
147
|
+
org_info: dict[str, Any] | None = None
|
|
148
|
+
available_profiles: list[str] = []
|
|
149
|
+
if org_config:
|
|
150
|
+
org_data = org_config.get("organization", {})
|
|
151
|
+
org_info = {
|
|
152
|
+
"name": org_data.get("name", "unknown"),
|
|
153
|
+
"id": org_data.get("id", ""),
|
|
154
|
+
"contact": org_data.get("contact", ""),
|
|
155
|
+
"source_url": source_url,
|
|
156
|
+
}
|
|
157
|
+
# Extract available profiles
|
|
158
|
+
profiles_dict = org_config.get("profiles", {})
|
|
159
|
+
available_profiles = list(profiles_dict.keys())
|
|
160
|
+
else:
|
|
161
|
+
org_info = {
|
|
162
|
+
"name": None,
|
|
163
|
+
"source_url": source_url,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Cache status
|
|
167
|
+
cache_info: dict[str, Any] | None = None
|
|
168
|
+
if cache_meta:
|
|
169
|
+
org_cache = cache_meta.get("org_config", {})
|
|
170
|
+
cache_info = {
|
|
171
|
+
"fetched_at": org_cache.get("fetched_at"),
|
|
172
|
+
"expires_at": org_cache.get("expires_at"),
|
|
173
|
+
"etag": org_cache.get("etag"),
|
|
174
|
+
"valid": is_cache_valid(cache_meta),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Version compatibility
|
|
178
|
+
version_compat: dict[str, Any] | None = None
|
|
179
|
+
if org_config:
|
|
180
|
+
compat = check_version_compatibility(org_config)
|
|
181
|
+
version_compat = {
|
|
182
|
+
"compatible": compat.compatible,
|
|
183
|
+
"blocking_error": compat.blocking_error,
|
|
184
|
+
"warnings": compat.warnings,
|
|
185
|
+
"schema_version": compat.schema_version,
|
|
186
|
+
"min_cli_version": compat.min_cli_version,
|
|
187
|
+
"current_cli_version": compat.current_cli_version,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"mode": "organization",
|
|
192
|
+
"organization": org_info,
|
|
193
|
+
"cache": cache_info,
|
|
194
|
+
"version_compatibility": version_compat,
|
|
195
|
+
"selected_profile": user_config.get("selected_profile"),
|
|
196
|
+
"available_profiles": available_profiles,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_update_data(
|
|
201
|
+
org_config: dict[str, Any] | None,
|
|
202
|
+
team_results: list[dict[str, Any]] | None = None,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Build update result data for JSON output.
|
|
205
|
+
|
|
206
|
+
Pure function that assembles update result information.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
org_config: Updated organization config (may be None on failure)
|
|
210
|
+
team_results: List of team update results (optional)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dictionary with update results including org and team info
|
|
214
|
+
"""
|
|
215
|
+
result: dict[str, Any] = {
|
|
216
|
+
"org_updated": org_config is not None,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if org_config:
|
|
220
|
+
org_data = org_config.get("organization", {})
|
|
221
|
+
result["organization"] = {
|
|
222
|
+
"name": org_data.get("name", ""),
|
|
223
|
+
"id": org_data.get("id", ""),
|
|
224
|
+
}
|
|
225
|
+
result["schema_version"] = org_config.get("schema_version", "")
|
|
226
|
+
|
|
227
|
+
if team_results is not None:
|
|
228
|
+
result["teams_updated"] = team_results
|
|
229
|
+
result["teams_success_count"] = sum(1 for t in team_results if t.get("success"))
|
|
230
|
+
result["teams_failed_count"] = sum(1 for t in team_results if not t.get("success"))
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _parse_config_source(source_dict: dict[str, Any]) -> Any:
|
|
236
|
+
"""Parse a config_source dict into the appropriate ConfigSource type.
|
|
237
|
+
|
|
238
|
+
Handles discriminated union parsing for github, git, url sources.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
source_dict: Raw config_source dict from org config
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
ConfigSource object (ConfigSourceGitHub, ConfigSourceGit, or ConfigSourceURL)
|
|
245
|
+
"""
|
|
246
|
+
# Import here to avoid circular imports
|
|
247
|
+
from ...marketplace.schema import (
|
|
248
|
+
ConfigSourceGit,
|
|
249
|
+
ConfigSourceGitHub,
|
|
250
|
+
ConfigSourceURL,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if "github" in source_dict:
|
|
254
|
+
github_data = source_dict["github"]
|
|
255
|
+
# Add source discriminator for Pydantic model
|
|
256
|
+
return ConfigSourceGitHub(source="github", **github_data)
|
|
257
|
+
elif "git" in source_dict:
|
|
258
|
+
git_data = source_dict["git"]
|
|
259
|
+
return ConfigSourceGit(source="git", **git_data)
|
|
260
|
+
elif "url" in source_dict:
|
|
261
|
+
url_data = source_dict["url"]
|
|
262
|
+
return ConfigSourceURL(source="url", **url_data)
|
|
263
|
+
else:
|
|
264
|
+
raise ValueError(f"Unknown config_source type: {list(source_dict.keys())}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Org package - Typer app definitions and command wiring.
|
|
3
|
+
|
|
4
|
+
This module contains the Typer app definitions and wires commands from:
|
|
5
|
+
- validate_cmd.py: Schema and semantic validation
|
|
6
|
+
- update_cmd.py: Organization and team config refresh
|
|
7
|
+
- schema_cmd.py: Print bundled schema
|
|
8
|
+
- status_cmd.py: Current organization status
|
|
9
|
+
- import_cmd.py: Import organization config from URL
|
|
10
|
+
- init_cmd.py: Generate config skeleton from templates
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from .import_cmd import org_import_cmd
|
|
18
|
+
from .init_cmd import org_init_cmd
|
|
19
|
+
from .schema_cmd import org_schema_cmd
|
|
20
|
+
from .status_cmd import org_status_cmd
|
|
21
|
+
from .update_cmd import org_update_cmd
|
|
22
|
+
from .validate_cmd import org_validate_cmd
|
|
23
|
+
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
# Org App
|
|
26
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
org_app = typer.Typer(
|
|
29
|
+
name="org",
|
|
30
|
+
help="Organization configuration management and validation.",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Wire org commands
|
|
36
|
+
org_app.command("validate")(org_validate_cmd)
|
|
37
|
+
org_app.command("update")(org_update_cmd)
|
|
38
|
+
org_app.command("schema")(org_schema_cmd)
|
|
39
|
+
org_app.command("status")(org_status_cmd)
|
|
40
|
+
org_app.command("import")(org_import_cmd)
|
|
41
|
+
org_app.command("init")(org_init_cmd)
|