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
scc_cli/commands/team.py
ADDED
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Define team management commands for SCC CLI.
|
|
3
|
+
|
|
4
|
+
Provide structured team management:
|
|
5
|
+
- scc team list - List available teams
|
|
6
|
+
- scc team current - Show current team
|
|
7
|
+
- scc team switch - Switch to a different team (interactive picker)
|
|
8
|
+
- scc team info - Show detailed team information
|
|
9
|
+
- scc team validate - Validate team configuration (plugins, security, cache)
|
|
10
|
+
|
|
11
|
+
All commands support --json output with proper envelopes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from .. import config, teams
|
|
21
|
+
from ..cli_common import console, handle_errors, render_responsive_table
|
|
22
|
+
from ..json_command import json_command
|
|
23
|
+
from ..kinds import Kind
|
|
24
|
+
from ..marketplace.adapter import translate_org_config
|
|
25
|
+
from ..marketplace.compute import TeamNotFoundError
|
|
26
|
+
from ..marketplace.resolve import ConfigFetchError, EffectiveConfig, resolve_effective_config
|
|
27
|
+
from ..marketplace.schema import OrganizationConfig
|
|
28
|
+
from ..marketplace.team_fetch import TeamFetchResult, fetch_team_config
|
|
29
|
+
from ..marketplace.trust import TrustViolationError
|
|
30
|
+
from ..output_mode import is_json_mode, print_human
|
|
31
|
+
from ..panels import create_warning_panel
|
|
32
|
+
from ..ui.gate import InteractivityContext
|
|
33
|
+
from ..ui.picker import TeamSwitchRequested, pick_team
|
|
34
|
+
|
|
35
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
# Display Helpers
|
|
37
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _format_plugins_for_display(plugins: list[str], max_display: int = 2) -> str:
|
|
41
|
+
"""Format a list of plugins for table/summary display.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
plugins: List of plugin identifiers (e.g., ["plugin@marketplace", ...])
|
|
45
|
+
max_display: Maximum number of plugins to show before truncating
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Formatted string like "plugin1, plugin2 +3 more" or "-" if empty
|
|
49
|
+
"""
|
|
50
|
+
if not plugins:
|
|
51
|
+
return "-"
|
|
52
|
+
|
|
53
|
+
if len(plugins) <= max_display:
|
|
54
|
+
# Show all plugin names (without marketplace suffix for brevity)
|
|
55
|
+
names = [p.split("@")[0] for p in plugins]
|
|
56
|
+
return ", ".join(names)
|
|
57
|
+
else:
|
|
58
|
+
# Show first N and count of remaining
|
|
59
|
+
names = [p.split("@")[0] for p in plugins[:max_display]]
|
|
60
|
+
remaining = len(plugins) - max_display
|
|
61
|
+
return f"{', '.join(names)} +{remaining} more"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
# Federation Helpers
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_config_source_from_raw(
|
|
70
|
+
org_config: dict[str, Any] | None, team_name: str
|
|
71
|
+
) -> dict[str, Any] | None:
|
|
72
|
+
"""Extract config_source from raw org_config dict for a team.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
org_config: Raw org config dict (or None)
|
|
76
|
+
team_name: Team profile name
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Raw config_source dict if team is federated, None if inline or not found
|
|
80
|
+
"""
|
|
81
|
+
if org_config is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
profiles = org_config.get("profiles", {})
|
|
85
|
+
if not profiles or team_name not in profiles:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
profile = profiles[team_name]
|
|
89
|
+
if not isinstance(profile, dict):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return profile.get("config_source")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _parse_config_source(raw_source: dict[str, Any]) -> Any:
|
|
96
|
+
"""Parse raw config_source dict into ConfigSource model.
|
|
97
|
+
|
|
98
|
+
The org config uses a nested structure like:
|
|
99
|
+
{"github": {"owner": "...", "repo": "..."}}
|
|
100
|
+
|
|
101
|
+
The Pydantic models use a flat structure with a discriminator field:
|
|
102
|
+
{"source": "github", "owner": "...", "repo": "..."}
|
|
103
|
+
|
|
104
|
+
This function bridges the two formats.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
raw_source: Raw config_source dict from org config
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Parsed ConfigSource model (ConfigSourceGitHub, ConfigSourceGit, or ConfigSourceURL)
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If config_source format is invalid
|
|
114
|
+
"""
|
|
115
|
+
from ..marketplace.schema import (
|
|
116
|
+
ConfigSourceGit,
|
|
117
|
+
ConfigSourceGitHub,
|
|
118
|
+
ConfigSourceURL,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Config source is a dict with a single key indicating type
|
|
122
|
+
# Add the discriminator field when parsing
|
|
123
|
+
if "github" in raw_source:
|
|
124
|
+
config_data = {**raw_source["github"], "source": "github"}
|
|
125
|
+
return ConfigSourceGitHub.model_validate(config_data)
|
|
126
|
+
elif "git" in raw_source:
|
|
127
|
+
config_data = {**raw_source["git"], "source": "git"}
|
|
128
|
+
return ConfigSourceGit.model_validate(config_data)
|
|
129
|
+
elif "url" in raw_source:
|
|
130
|
+
config_data = {**raw_source["url"], "source": "url"}
|
|
131
|
+
return ConfigSourceURL.model_validate(config_data)
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError(f"Unknown config_source type: {list(raw_source.keys())}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _fetch_federated_team_config(
|
|
137
|
+
org_config: dict[str, Any] | None, team_name: str
|
|
138
|
+
) -> TeamFetchResult | None:
|
|
139
|
+
"""Fetch team config if team is federated, return None if inline.
|
|
140
|
+
|
|
141
|
+
This eagerly fetches the team config to prime the cache when
|
|
142
|
+
switching to a federated team.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
org_config: Raw org config dict
|
|
146
|
+
team_name: Team name to fetch config for
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
TeamFetchResult if federated team, None if inline
|
|
150
|
+
"""
|
|
151
|
+
raw_source = _get_config_source_from_raw(org_config, team_name)
|
|
152
|
+
if raw_source is None:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
config_source = _parse_config_source(raw_source)
|
|
157
|
+
return fetch_team_config(config_source, team_name)
|
|
158
|
+
except ValueError:
|
|
159
|
+
# Invalid config_source format - treat as inline
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
164
|
+
# Team App Definition
|
|
165
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
team_app = typer.Typer(
|
|
168
|
+
name="team",
|
|
169
|
+
help="Team profile management",
|
|
170
|
+
no_args_is_help=False,
|
|
171
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
172
|
+
invoke_without_command=True,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@team_app.callback(invoke_without_command=True)
|
|
177
|
+
def team_callback(
|
|
178
|
+
ctx: typer.Context,
|
|
179
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full descriptions"),
|
|
180
|
+
sync: bool = typer.Option(False, "--sync", "-s", help="Sync team configs from organization"),
|
|
181
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
182
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON"),
|
|
183
|
+
) -> None:
|
|
184
|
+
"""List teams by default.
|
|
185
|
+
|
|
186
|
+
This makes `scc team` behave like `scc team list` for convenience.
|
|
187
|
+
"""
|
|
188
|
+
if ctx.invoked_subcommand is None:
|
|
189
|
+
team_list(verbose=verbose, sync=sync, json_output=json_output, pretty=pretty)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
193
|
+
# Team List Command
|
|
194
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@team_app.command("list")
|
|
198
|
+
@json_command(Kind.TEAM_LIST)
|
|
199
|
+
@handle_errors
|
|
200
|
+
def team_list(
|
|
201
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full descriptions"),
|
|
202
|
+
sync: bool = typer.Option(False, "--sync", "-s", help="Sync team configs from organization"),
|
|
203
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
204
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
"""List available team profiles.
|
|
207
|
+
|
|
208
|
+
Returns a list of teams with their names, descriptions, and plugins.
|
|
209
|
+
Use --verbose to show full descriptions instead of truncated versions.
|
|
210
|
+
Use --sync to refresh the team list from the organization config.
|
|
211
|
+
"""
|
|
212
|
+
cfg = config.load_user_config()
|
|
213
|
+
org_config = config.load_cached_org_config()
|
|
214
|
+
|
|
215
|
+
# Sync if requested
|
|
216
|
+
if sync:
|
|
217
|
+
from ..remote import fetch_org_config
|
|
218
|
+
|
|
219
|
+
org_source = cfg.get("organization_source", {})
|
|
220
|
+
org_url = org_source.get("url")
|
|
221
|
+
org_auth = org_source.get("auth")
|
|
222
|
+
if org_url:
|
|
223
|
+
fetched_config, _etag, status_code = fetch_org_config(org_url, org_auth)
|
|
224
|
+
if fetched_config and status_code == 200:
|
|
225
|
+
org_config = fetched_config
|
|
226
|
+
# Save to cache
|
|
227
|
+
config.CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
import json
|
|
229
|
+
|
|
230
|
+
cache_file = config.CACHE_DIR / "org_config.json"
|
|
231
|
+
cache_file.write_text(json.dumps(org_config, indent=2))
|
|
232
|
+
print_human("[green]✓ Team list synced from organization[/green]")
|
|
233
|
+
|
|
234
|
+
# Get teams
|
|
235
|
+
available_teams = teams.list_teams(cfg, org_config=org_config)
|
|
236
|
+
|
|
237
|
+
# Get current team for marking
|
|
238
|
+
current = cfg.get("selected_profile")
|
|
239
|
+
|
|
240
|
+
# Build data structure for JSON output
|
|
241
|
+
team_data = []
|
|
242
|
+
for team in available_teams:
|
|
243
|
+
team_data.append(
|
|
244
|
+
{
|
|
245
|
+
"name": team["name"],
|
|
246
|
+
"description": team.get("description", ""),
|
|
247
|
+
"plugins": team.get("plugins", []),
|
|
248
|
+
"is_current": team["name"] == current,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Human-readable output
|
|
253
|
+
if not is_json_mode():
|
|
254
|
+
if not available_teams:
|
|
255
|
+
# Provide context-aware messaging based on mode
|
|
256
|
+
if config.is_standalone_mode():
|
|
257
|
+
console.print(
|
|
258
|
+
create_warning_panel(
|
|
259
|
+
"Standalone Mode",
|
|
260
|
+
"Teams are not available in standalone mode.",
|
|
261
|
+
"Run 'scc setup' with an organization URL to enable teams",
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
console.print(
|
|
266
|
+
create_warning_panel(
|
|
267
|
+
"No Teams",
|
|
268
|
+
"No team profiles defined in organization config.",
|
|
269
|
+
"Contact your organization admin to configure teams",
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
return {"teams": [], "current": current}
|
|
273
|
+
|
|
274
|
+
# Build rows for responsive table
|
|
275
|
+
rows = []
|
|
276
|
+
for team in available_teams:
|
|
277
|
+
name = team["name"]
|
|
278
|
+
if name == current:
|
|
279
|
+
name = f"[bold]{name}[/bold] ←"
|
|
280
|
+
|
|
281
|
+
desc = team.get("description", "")
|
|
282
|
+
if not verbose and len(desc) > 40:
|
|
283
|
+
desc = desc[:37] + "..."
|
|
284
|
+
|
|
285
|
+
plugins = team.get("plugins", [])
|
|
286
|
+
plugins_display = _format_plugins_for_display(plugins)
|
|
287
|
+
rows.append([name, desc, plugins_display])
|
|
288
|
+
|
|
289
|
+
render_responsive_table(
|
|
290
|
+
title="Available Team Profiles",
|
|
291
|
+
columns=[
|
|
292
|
+
("Team", "cyan"),
|
|
293
|
+
("Description", "white"),
|
|
294
|
+
],
|
|
295
|
+
rows=rows,
|
|
296
|
+
wide_columns=[
|
|
297
|
+
("Plugins", "yellow"),
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
console.print()
|
|
302
|
+
console.print(
|
|
303
|
+
"[dim]Use: scc team switch <name> to switch, scc team info <name> for details[/dim]"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return {"teams": team_data, "current": current}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
310
|
+
# Team Current Command
|
|
311
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@team_app.command("current")
|
|
315
|
+
@json_command(Kind.TEAM_CURRENT)
|
|
316
|
+
@handle_errors
|
|
317
|
+
def team_current(
|
|
318
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
319
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Show the currently selected team profile.
|
|
322
|
+
|
|
323
|
+
Displays the current team and basic information about it.
|
|
324
|
+
Returns null for team if no team is selected.
|
|
325
|
+
"""
|
|
326
|
+
cfg = config.load_user_config()
|
|
327
|
+
org_config = config.load_cached_org_config()
|
|
328
|
+
|
|
329
|
+
current = cfg.get("selected_profile")
|
|
330
|
+
|
|
331
|
+
if not current:
|
|
332
|
+
print_human(
|
|
333
|
+
"[yellow]No team currently selected.[/yellow]\n"
|
|
334
|
+
"[dim]Use 'scc team switch <name>' to select a team[/dim]"
|
|
335
|
+
)
|
|
336
|
+
return {"team": None, "profile": None}
|
|
337
|
+
|
|
338
|
+
# Get team details
|
|
339
|
+
details = teams.get_team_details(current, cfg, org_config=org_config)
|
|
340
|
+
|
|
341
|
+
if not details:
|
|
342
|
+
print_human(
|
|
343
|
+
f"[yellow]Current team '{current}' not found in configuration.[/yellow]\n"
|
|
344
|
+
"[dim]Run 'scc team list --sync' to refresh[/dim]"
|
|
345
|
+
)
|
|
346
|
+
return {"team": current, "profile": None, "error": "team_not_found"}
|
|
347
|
+
|
|
348
|
+
# Human output
|
|
349
|
+
print_human(f"[bold cyan]Current team:[/bold cyan] {current}")
|
|
350
|
+
if details.get("description"):
|
|
351
|
+
print_human(f"[dim]{details['description']}[/dim]")
|
|
352
|
+
plugins = details.get("plugins", [])
|
|
353
|
+
if plugins:
|
|
354
|
+
print_human(f"[dim]Plugins: {_format_plugins_for_display(plugins)}[/dim]")
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"team": current,
|
|
358
|
+
"profile": {
|
|
359
|
+
"name": details.get("name"),
|
|
360
|
+
"description": details.get("description"),
|
|
361
|
+
"plugins": plugins,
|
|
362
|
+
"marketplace": details.get("marketplace"),
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
368
|
+
# Team Switch Command
|
|
369
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@team_app.command("switch")
|
|
373
|
+
@json_command(Kind.TEAM_SWITCH)
|
|
374
|
+
@handle_errors
|
|
375
|
+
def team_switch(
|
|
376
|
+
team_name: str = typer.Argument(
|
|
377
|
+
None, help="Team name to switch to (interactive picker if not provided)"
|
|
378
|
+
),
|
|
379
|
+
non_interactive: bool = typer.Option(
|
|
380
|
+
False, "--non-interactive", help="Fail if team name not provided"
|
|
381
|
+
),
|
|
382
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
383
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
"""Switch to a different team profile.
|
|
386
|
+
|
|
387
|
+
If team_name is not provided, shows an interactive picker (if TTY).
|
|
388
|
+
Use --non-interactive to fail instead of showing picker.
|
|
389
|
+
"""
|
|
390
|
+
cfg = config.load_user_config()
|
|
391
|
+
org_config = config.load_cached_org_config()
|
|
392
|
+
|
|
393
|
+
available_teams = teams.list_teams(cfg, org_config=org_config)
|
|
394
|
+
|
|
395
|
+
if not available_teams:
|
|
396
|
+
# Provide context-aware messaging based on mode
|
|
397
|
+
if config.is_standalone_mode():
|
|
398
|
+
print_human(
|
|
399
|
+
"[yellow]Teams are not available in standalone mode.[/yellow]\n"
|
|
400
|
+
"[dim]Run 'scc setup' with an organization URL to enable teams[/dim]"
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
print_human(
|
|
404
|
+
"[yellow]No teams available to switch to.[/yellow]\n"
|
|
405
|
+
"[dim]No team profiles defined in organization config[/dim]"
|
|
406
|
+
)
|
|
407
|
+
return {"success": False, "error": "no_teams_available", "previous": None, "current": None}
|
|
408
|
+
|
|
409
|
+
# Get current team for picker display
|
|
410
|
+
current = cfg.get("selected_profile")
|
|
411
|
+
|
|
412
|
+
# Resolve team name (explicit arg, picker, or error)
|
|
413
|
+
resolved_name: str | None = team_name
|
|
414
|
+
|
|
415
|
+
if resolved_name is None:
|
|
416
|
+
# Create interactivity context from flags
|
|
417
|
+
ctx = InteractivityContext.create(
|
|
418
|
+
json_mode=is_json_mode(),
|
|
419
|
+
no_interactive=non_interactive,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if ctx.allows_prompt():
|
|
423
|
+
# Show interactive picker
|
|
424
|
+
try:
|
|
425
|
+
selected_team = pick_team(available_teams, current_team=current)
|
|
426
|
+
if selected_team is None:
|
|
427
|
+
# User cancelled - exit cleanly
|
|
428
|
+
return {
|
|
429
|
+
"success": False,
|
|
430
|
+
"cancelled": True,
|
|
431
|
+
"previous": current,
|
|
432
|
+
"current": None,
|
|
433
|
+
}
|
|
434
|
+
resolved_name = selected_team["name"]
|
|
435
|
+
except TeamSwitchRequested:
|
|
436
|
+
# Already in team picker - treat as cancel
|
|
437
|
+
return {"success": False, "cancelled": True, "previous": current, "current": None}
|
|
438
|
+
else:
|
|
439
|
+
# Non-interactive mode with no team specified
|
|
440
|
+
raise typer.BadParameter(
|
|
441
|
+
"Team name required in non-interactive mode. "
|
|
442
|
+
f"Available: {', '.join(t['name'] for t in available_teams)}"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Validate team exists (when name provided directly as arg)
|
|
446
|
+
team_names = [t["name"] for t in available_teams]
|
|
447
|
+
if resolved_name not in team_names:
|
|
448
|
+
print_human(
|
|
449
|
+
f"[red]Team '{resolved_name}' not found.[/red]\n"
|
|
450
|
+
f"[dim]Available: {', '.join(team_names)}[/dim]"
|
|
451
|
+
)
|
|
452
|
+
return {"success": False, "error": "team_not_found", "team": resolved_name}
|
|
453
|
+
|
|
454
|
+
# Get previous team
|
|
455
|
+
previous = cfg.get("selected_profile")
|
|
456
|
+
|
|
457
|
+
# Switch team
|
|
458
|
+
cfg["selected_profile"] = resolved_name
|
|
459
|
+
config.save_user_config(cfg)
|
|
460
|
+
|
|
461
|
+
# Check if team is federated and fetch config to prime cache
|
|
462
|
+
fetch_result = _fetch_federated_team_config(org_config, resolved_name)
|
|
463
|
+
is_federated = fetch_result is not None
|
|
464
|
+
|
|
465
|
+
print_human(f"[green]✓ Switched to team: {resolved_name}[/green]")
|
|
466
|
+
if previous and previous != resolved_name:
|
|
467
|
+
print_human(f"[dim]Previous: {previous}[/dim]")
|
|
468
|
+
|
|
469
|
+
details = teams.get_team_details(resolved_name, cfg, org_config=org_config)
|
|
470
|
+
if details:
|
|
471
|
+
description = details.get("description")
|
|
472
|
+
plugins = details.get("plugins", [])
|
|
473
|
+
marketplace = details.get("marketplace") or "default"
|
|
474
|
+
if description:
|
|
475
|
+
print_human(f"[dim]Description:[/dim] {description}")
|
|
476
|
+
print_human(f"[dim]Plugins:[/dim] {_format_plugins_for_display(plugins)}")
|
|
477
|
+
print_human(f"[dim]Marketplace:[/dim] {marketplace}")
|
|
478
|
+
|
|
479
|
+
# Display federation status
|
|
480
|
+
if fetch_result is not None:
|
|
481
|
+
if fetch_result.success:
|
|
482
|
+
print_human(f"[dim]Federated config synced from {fetch_result.source_url}[/dim]")
|
|
483
|
+
else:
|
|
484
|
+
print_human(f"[yellow]⚠ Could not sync federated config: {fetch_result.error}[/yellow]")
|
|
485
|
+
|
|
486
|
+
# Build response with federation metadata
|
|
487
|
+
response: dict[str, Any] = {
|
|
488
|
+
"success": True,
|
|
489
|
+
"previous": previous,
|
|
490
|
+
"current": resolved_name,
|
|
491
|
+
"is_federated": is_federated,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if is_federated and fetch_result is not None:
|
|
495
|
+
response["source_type"] = fetch_result.source_type
|
|
496
|
+
response["source_url"] = fetch_result.source_url
|
|
497
|
+
if fetch_result.commit_sha:
|
|
498
|
+
response["commit_sha"] = fetch_result.commit_sha
|
|
499
|
+
if fetch_result.etag:
|
|
500
|
+
response["etag"] = fetch_result.etag
|
|
501
|
+
if not fetch_result.success:
|
|
502
|
+
response["fetch_error"] = fetch_result.error
|
|
503
|
+
|
|
504
|
+
return response
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
508
|
+
# Team Info Command
|
|
509
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@team_app.command("info")
|
|
513
|
+
@json_command(Kind.TEAM_INFO)
|
|
514
|
+
@handle_errors
|
|
515
|
+
def team_info(
|
|
516
|
+
team_name: str = typer.Argument(..., help="Team name to show details for"),
|
|
517
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
518
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
519
|
+
) -> dict[str, Any]:
|
|
520
|
+
"""Show detailed information for a specific team profile.
|
|
521
|
+
|
|
522
|
+
Displays team description, plugin configuration, marketplace info,
|
|
523
|
+
federation status (federated vs inline), config source, and trust grants.
|
|
524
|
+
"""
|
|
525
|
+
cfg = config.load_user_config()
|
|
526
|
+
org_config = config.load_cached_org_config()
|
|
527
|
+
|
|
528
|
+
details = teams.get_team_details(team_name, cfg, org_config=org_config)
|
|
529
|
+
|
|
530
|
+
# Detect if team is federated (has config_source)
|
|
531
|
+
raw_source = _get_config_source_from_raw(org_config, team_name)
|
|
532
|
+
is_federated = raw_source is not None
|
|
533
|
+
|
|
534
|
+
# Get config source description for federated teams
|
|
535
|
+
config_source_display: str | None = None
|
|
536
|
+
if is_federated and raw_source is not None:
|
|
537
|
+
if "github" in raw_source:
|
|
538
|
+
gh = raw_source["github"]
|
|
539
|
+
config_source_display = f"github.com/{gh.get('owner', '?')}/{gh.get('repo', '?')}"
|
|
540
|
+
elif "git" in raw_source:
|
|
541
|
+
git = raw_source["git"]
|
|
542
|
+
url = git.get("url", "")
|
|
543
|
+
# Normalize for display
|
|
544
|
+
if url.startswith("https://"):
|
|
545
|
+
url = url[8:]
|
|
546
|
+
elif url.startswith("git@"):
|
|
547
|
+
url = url[4:].replace(":", "/", 1)
|
|
548
|
+
if url.endswith(".git"):
|
|
549
|
+
url = url[:-4]
|
|
550
|
+
config_source_display = url
|
|
551
|
+
elif "url" in raw_source:
|
|
552
|
+
url = raw_source["url"].get("url", "")
|
|
553
|
+
if url.startswith("https://"):
|
|
554
|
+
url = url[8:]
|
|
555
|
+
config_source_display = url
|
|
556
|
+
|
|
557
|
+
# Get trust grants for federated teams
|
|
558
|
+
trust_grants: dict[str, Any] | None = None
|
|
559
|
+
if is_federated and org_config:
|
|
560
|
+
profiles = org_config.get("profiles", {})
|
|
561
|
+
profile = profiles.get(team_name, {})
|
|
562
|
+
if isinstance(profile, dict):
|
|
563
|
+
trust_grants = profile.get("trust")
|
|
564
|
+
|
|
565
|
+
if not details:
|
|
566
|
+
if not is_json_mode():
|
|
567
|
+
console.print(
|
|
568
|
+
create_warning_panel(
|
|
569
|
+
"Team Not Found",
|
|
570
|
+
f"No team profile named '{team_name}'.",
|
|
571
|
+
"Run 'scc team list' to see available profiles",
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
return {"team": team_name, "found": False, "profile": None}
|
|
575
|
+
|
|
576
|
+
# Get validation info
|
|
577
|
+
validation = teams.validate_team_profile(team_name, cfg, org_config=org_config)
|
|
578
|
+
|
|
579
|
+
# Human output
|
|
580
|
+
if not is_json_mode():
|
|
581
|
+
grid = Table.grid(padding=(0, 2))
|
|
582
|
+
grid.add_column(style="dim", no_wrap=True)
|
|
583
|
+
grid.add_column(style="white")
|
|
584
|
+
|
|
585
|
+
grid.add_row("Description:", details.get("description", "-"))
|
|
586
|
+
|
|
587
|
+
# Show federation mode
|
|
588
|
+
if is_federated:
|
|
589
|
+
grid.add_row("Mode:", "[cyan]federated[/cyan]")
|
|
590
|
+
if config_source_display:
|
|
591
|
+
grid.add_row("Config Source:", config_source_display)
|
|
592
|
+
else:
|
|
593
|
+
grid.add_row("Mode:", "[dim]inline[/dim]")
|
|
594
|
+
|
|
595
|
+
plugins = details.get("plugins", [])
|
|
596
|
+
if plugins:
|
|
597
|
+
# Show all plugins with full identifiers
|
|
598
|
+
plugins_display = ", ".join(plugins)
|
|
599
|
+
grid.add_row("Plugins:", plugins_display)
|
|
600
|
+
if details.get("marketplace_repo"):
|
|
601
|
+
grid.add_row("Marketplace:", details.get("marketplace_repo", "-"))
|
|
602
|
+
else:
|
|
603
|
+
grid.add_row("Plugins:", "[dim]None (base profile)[/dim]")
|
|
604
|
+
|
|
605
|
+
# Show trust grants for federated teams
|
|
606
|
+
if trust_grants:
|
|
607
|
+
grid.add_row("", "")
|
|
608
|
+
grid.add_row("[bold]Trust Grants:[/bold]", "")
|
|
609
|
+
inherit = trust_grants.get("inherit_org_marketplaces", True)
|
|
610
|
+
allow_add = trust_grants.get("allow_additional_marketplaces", False)
|
|
611
|
+
grid.add_row(
|
|
612
|
+
" Inherit Org Marketplaces:", "[green]yes[/green]" if inherit else "[red]no[/red]"
|
|
613
|
+
)
|
|
614
|
+
grid.add_row(
|
|
615
|
+
" Allow Additional Marketplaces:",
|
|
616
|
+
"[green]yes[/green]" if allow_add else "[red]no[/red]",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Show validation warnings
|
|
620
|
+
if validation.get("warnings"):
|
|
621
|
+
grid.add_row("", "")
|
|
622
|
+
for warning in validation["warnings"]:
|
|
623
|
+
grid.add_row("[yellow]Warning:[/yellow]", warning)
|
|
624
|
+
|
|
625
|
+
panel = Panel(
|
|
626
|
+
grid,
|
|
627
|
+
title=f"[bold cyan]Team: {team_name}[/bold cyan]",
|
|
628
|
+
border_style="cyan",
|
|
629
|
+
padding=(1, 2),
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
console.print()
|
|
633
|
+
console.print(panel)
|
|
634
|
+
console.print()
|
|
635
|
+
console.print(f"[dim]Use: scc start -t {team_name} to use this profile[/dim]")
|
|
636
|
+
|
|
637
|
+
# Build response with federation metadata
|
|
638
|
+
response: dict[str, Any] = {
|
|
639
|
+
"team": team_name,
|
|
640
|
+
"found": True,
|
|
641
|
+
"is_federated": is_federated,
|
|
642
|
+
"profile": {
|
|
643
|
+
"name": details.get("name"),
|
|
644
|
+
"description": details.get("description"),
|
|
645
|
+
"plugins": details.get("plugins", []),
|
|
646
|
+
"marketplace": details.get("marketplace"),
|
|
647
|
+
"marketplace_type": details.get("marketplace_type"),
|
|
648
|
+
"marketplace_repo": details.get("marketplace_repo"),
|
|
649
|
+
},
|
|
650
|
+
"validation": {
|
|
651
|
+
"valid": validation.get("valid", True),
|
|
652
|
+
"warnings": validation.get("warnings", []),
|
|
653
|
+
"errors": validation.get("errors", []),
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# Add federation details for federated teams
|
|
658
|
+
if is_federated:
|
|
659
|
+
response["config_source"] = config_source_display
|
|
660
|
+
if trust_grants:
|
|
661
|
+
response["trust"] = trust_grants
|
|
662
|
+
|
|
663
|
+
return response
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
@team_app.command("validate")
|
|
667
|
+
@json_command(Kind.TEAM_VALIDATE)
|
|
668
|
+
@handle_errors
|
|
669
|
+
def team_validate(
|
|
670
|
+
team_name: str = typer.Argument(..., help="Team name to validate"),
|
|
671
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
672
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
673
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
674
|
+
) -> dict[str, Any]:
|
|
675
|
+
"""Validate team configuration and show effective plugins.
|
|
676
|
+
|
|
677
|
+
Resolves the team configuration (inline or federated) and validates:
|
|
678
|
+
- Plugin security compliance (blocked_plugins patterns)
|
|
679
|
+
- Plugin allowlists (allowed_plugins patterns)
|
|
680
|
+
- Marketplace trust grants (for federated teams)
|
|
681
|
+
- Cache freshness status (for federated teams)
|
|
682
|
+
|
|
683
|
+
Use --verbose to see detailed validation information including
|
|
684
|
+
individual blocked/disabled plugins and their reasons.
|
|
685
|
+
"""
|
|
686
|
+
org_config_data = config.load_cached_org_config()
|
|
687
|
+
if not org_config_data:
|
|
688
|
+
if not is_json_mode():
|
|
689
|
+
console.print(
|
|
690
|
+
create_warning_panel(
|
|
691
|
+
"No Org Config",
|
|
692
|
+
"No organization configuration found.",
|
|
693
|
+
"Run 'scc setup' to configure your organization",
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
return {
|
|
697
|
+
"team": team_name,
|
|
698
|
+
"valid": False,
|
|
699
|
+
"error": "No organization configuration found",
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
# Parse org config (translate external format to internal Pydantic format)
|
|
703
|
+
try:
|
|
704
|
+
internal_data = translate_org_config(org_config_data)
|
|
705
|
+
org_config = OrganizationConfig.model_validate(internal_data)
|
|
706
|
+
except Exception as e:
|
|
707
|
+
if not is_json_mode():
|
|
708
|
+
console.print(
|
|
709
|
+
create_warning_panel(
|
|
710
|
+
"Invalid Org Config",
|
|
711
|
+
f"Organization configuration is invalid: {e}",
|
|
712
|
+
"Run 'scc org update' to refresh your configuration",
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
return {
|
|
716
|
+
"team": team_name,
|
|
717
|
+
"valid": False,
|
|
718
|
+
"error": f"Invalid org config: {e}",
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
# Resolve effective config (validates team exists, trust, security)
|
|
722
|
+
try:
|
|
723
|
+
effective = resolve_effective_config(org_config, team_name)
|
|
724
|
+
except TeamNotFoundError as e:
|
|
725
|
+
if not is_json_mode():
|
|
726
|
+
console.print(
|
|
727
|
+
create_warning_panel(
|
|
728
|
+
"Team Not Found",
|
|
729
|
+
f"Team '{team_name}' not found in org config.",
|
|
730
|
+
f"Available teams: {', '.join(e.available_teams[:5])}",
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
return {
|
|
734
|
+
"team": team_name,
|
|
735
|
+
"valid": False,
|
|
736
|
+
"error": f"Team not found: {team_name}",
|
|
737
|
+
"available_teams": e.available_teams,
|
|
738
|
+
}
|
|
739
|
+
except TrustViolationError as e:
|
|
740
|
+
if not is_json_mode():
|
|
741
|
+
console.print(
|
|
742
|
+
create_warning_panel(
|
|
743
|
+
"Trust Violation",
|
|
744
|
+
f"Team configuration violates trust policy: {e.violation}",
|
|
745
|
+
"Check team config_source and trust grants in org config",
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
return {
|
|
749
|
+
"team": team_name,
|
|
750
|
+
"valid": False,
|
|
751
|
+
"error": f"Trust violation: {e.violation}",
|
|
752
|
+
"team_name": e.team_name,
|
|
753
|
+
}
|
|
754
|
+
except ConfigFetchError as e:
|
|
755
|
+
if not is_json_mode():
|
|
756
|
+
console.print(
|
|
757
|
+
create_warning_panel(
|
|
758
|
+
"Config Fetch Failed",
|
|
759
|
+
f"Failed to fetch config for team '{e.team_id}' from {e.source_type}",
|
|
760
|
+
str(e), # Includes remediation hint
|
|
761
|
+
)
|
|
762
|
+
)
|
|
763
|
+
return {
|
|
764
|
+
"team": team_name,
|
|
765
|
+
"valid": False,
|
|
766
|
+
"error": str(e),
|
|
767
|
+
"source_type": e.source_type,
|
|
768
|
+
"source_url": e.source_url,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# Determine overall validity
|
|
772
|
+
is_valid = not effective.has_security_violations
|
|
773
|
+
|
|
774
|
+
# Human output
|
|
775
|
+
if not is_json_mode():
|
|
776
|
+
_render_validation_result(effective, verbose)
|
|
777
|
+
|
|
778
|
+
# Build JSON response
|
|
779
|
+
response: dict[str, Any] = {
|
|
780
|
+
"team": team_name,
|
|
781
|
+
"valid": is_valid,
|
|
782
|
+
"is_federated": effective.is_federated,
|
|
783
|
+
"enabled_plugins_count": effective.plugin_count,
|
|
784
|
+
"blocked_plugins_count": len(effective.blocked_plugins),
|
|
785
|
+
"disabled_plugins_count": len(effective.disabled_plugins),
|
|
786
|
+
"not_allowed_plugins_count": len(effective.not_allowed_plugins),
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
# Add federation metadata
|
|
790
|
+
if effective.is_federated:
|
|
791
|
+
response["config_source"] = effective.source_description
|
|
792
|
+
if effective.config_commit_sha:
|
|
793
|
+
response["config_commit_sha"] = effective.config_commit_sha
|
|
794
|
+
if effective.config_etag:
|
|
795
|
+
response["config_etag"] = effective.config_etag
|
|
796
|
+
|
|
797
|
+
# Add cache status
|
|
798
|
+
if effective.used_cached_config:
|
|
799
|
+
response["used_cached_config"] = True
|
|
800
|
+
response["cache_is_stale"] = effective.cache_is_stale
|
|
801
|
+
if effective.staleness_warning:
|
|
802
|
+
response["staleness_warning"] = effective.staleness_warning
|
|
803
|
+
|
|
804
|
+
# Add verbose details
|
|
805
|
+
if verbose or json_output or pretty:
|
|
806
|
+
response["enabled_plugins"] = sorted(effective.enabled_plugins)
|
|
807
|
+
response["blocked_plugins"] = [
|
|
808
|
+
{"plugin_id": bp.plugin_id, "reason": bp.reason, "pattern": bp.pattern}
|
|
809
|
+
for bp in effective.blocked_plugins
|
|
810
|
+
]
|
|
811
|
+
response["disabled_plugins"] = effective.disabled_plugins
|
|
812
|
+
response["not_allowed_plugins"] = effective.not_allowed_plugins
|
|
813
|
+
response["extra_marketplaces"] = effective.extra_marketplaces
|
|
814
|
+
|
|
815
|
+
return response
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _render_validation_result(effective: EffectiveConfig, verbose: bool) -> None:
|
|
819
|
+
"""Render validation result to terminal.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
effective: Resolved effective configuration
|
|
823
|
+
verbose: Whether to show detailed output
|
|
824
|
+
"""
|
|
825
|
+
console.print()
|
|
826
|
+
|
|
827
|
+
# Header with validation status
|
|
828
|
+
if effective.has_security_violations:
|
|
829
|
+
status = "[red]FAILED[/red]"
|
|
830
|
+
border_style = "red"
|
|
831
|
+
else:
|
|
832
|
+
status = "[green]PASSED[/green]"
|
|
833
|
+
border_style = "green"
|
|
834
|
+
|
|
835
|
+
grid = Table.grid(padding=(0, 2))
|
|
836
|
+
grid.add_column(style="dim", no_wrap=True)
|
|
837
|
+
grid.add_column()
|
|
838
|
+
|
|
839
|
+
# Basic info
|
|
840
|
+
grid.add_row("Status:", status)
|
|
841
|
+
grid.add_row(
|
|
842
|
+
"Mode:", "[cyan]federated[/cyan]" if effective.is_federated else "[dim]inline[/dim]"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
if effective.is_federated:
|
|
846
|
+
grid.add_row("Config Source:", effective.source_description)
|
|
847
|
+
if effective.config_commit_sha:
|
|
848
|
+
grid.add_row("Commit SHA:", effective.config_commit_sha[:8])
|
|
849
|
+
|
|
850
|
+
# Cache status
|
|
851
|
+
if effective.used_cached_config:
|
|
852
|
+
cache_status = (
|
|
853
|
+
"[yellow]stale[/yellow]" if effective.cache_is_stale else "[green]fresh[/green]"
|
|
854
|
+
)
|
|
855
|
+
grid.add_row("Cache:", cache_status)
|
|
856
|
+
if effective.staleness_warning:
|
|
857
|
+
grid.add_row("", f"[dim]{effective.staleness_warning}[/dim]")
|
|
858
|
+
|
|
859
|
+
grid.add_row("", "")
|
|
860
|
+
|
|
861
|
+
# Plugin summary
|
|
862
|
+
grid.add_row("Enabled Plugins:", f"[green]{effective.plugin_count}[/green]")
|
|
863
|
+
if effective.blocked_plugins:
|
|
864
|
+
grid.add_row("Blocked Plugins:", f"[red]{len(effective.blocked_plugins)}[/red]")
|
|
865
|
+
if effective.disabled_plugins:
|
|
866
|
+
grid.add_row("Disabled Plugins:", f"[yellow]{len(effective.disabled_plugins)}[/yellow]")
|
|
867
|
+
if effective.not_allowed_plugins:
|
|
868
|
+
grid.add_row("Not Allowed:", f"[yellow]{len(effective.not_allowed_plugins)}[/yellow]")
|
|
869
|
+
|
|
870
|
+
# Verbose details
|
|
871
|
+
if verbose:
|
|
872
|
+
grid.add_row("", "")
|
|
873
|
+
if effective.enabled_plugins:
|
|
874
|
+
grid.add_row("[bold]Enabled:[/bold]", "")
|
|
875
|
+
for plugin in sorted(effective.enabled_plugins):
|
|
876
|
+
grid.add_row("", f" [green]✓[/green] {plugin}")
|
|
877
|
+
|
|
878
|
+
if effective.blocked_plugins:
|
|
879
|
+
grid.add_row("[bold]Blocked:[/bold]", "")
|
|
880
|
+
for bp in effective.blocked_plugins:
|
|
881
|
+
grid.add_row("", f" [red]✗[/red] {bp.plugin_id}")
|
|
882
|
+
grid.add_row("", f" [dim]Reason: {bp.reason}[/dim]")
|
|
883
|
+
grid.add_row("", f" [dim]Pattern: {bp.pattern}[/dim]")
|
|
884
|
+
|
|
885
|
+
if effective.disabled_plugins:
|
|
886
|
+
grid.add_row("[bold]Disabled:[/bold]", "")
|
|
887
|
+
for plugin in effective.disabled_plugins:
|
|
888
|
+
grid.add_row("", f" [yellow]○[/yellow] {plugin}")
|
|
889
|
+
|
|
890
|
+
if effective.not_allowed_plugins:
|
|
891
|
+
grid.add_row("[bold]Not Allowed:[/bold]", "")
|
|
892
|
+
for plugin in effective.not_allowed_plugins:
|
|
893
|
+
grid.add_row("", f" [yellow]○[/yellow] {plugin}")
|
|
894
|
+
|
|
895
|
+
panel = Panel(
|
|
896
|
+
grid,
|
|
897
|
+
title=f"[bold cyan]Team Validation: {effective.team_id}[/bold cyan]",
|
|
898
|
+
border_style=border_style,
|
|
899
|
+
padding=(1, 2),
|
|
900
|
+
)
|
|
901
|
+
console.print(panel)
|
|
902
|
+
|
|
903
|
+
# Hint
|
|
904
|
+
if not verbose and (
|
|
905
|
+
effective.blocked_plugins or effective.disabled_plugins or effective.not_allowed_plugins
|
|
906
|
+
):
|
|
907
|
+
console.print()
|
|
908
|
+
console.print("[dim]Use --verbose for detailed plugin information[/dim]")
|
|
909
|
+
|
|
910
|
+
console.print()
|