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,528 @@
|
|
|
1
|
+
"""Provide CLI commands for managing teams, configuration, and setup."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .. import config, profiles, setup
|
|
8
|
+
from ..cli_common import console, handle_errors
|
|
9
|
+
from ..core.exit_codes import EXIT_USAGE
|
|
10
|
+
from ..panels import create_error_panel, create_info_panel
|
|
11
|
+
from ..source_resolver import ResolveError, resolve_source
|
|
12
|
+
from ..stores.exception_store import RepoStore, UserStore
|
|
13
|
+
from ..utils.ttl import format_relative
|
|
14
|
+
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
# Config App
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
config_app = typer.Typer(
|
|
20
|
+
name="config",
|
|
21
|
+
help="Manage configuration and team profiles.",
|
|
22
|
+
no_args_is_help=False,
|
|
23
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
# Setup Command
|
|
29
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@handle_errors
|
|
33
|
+
def setup_cmd(
|
|
34
|
+
quick: bool = typer.Option(False, "--quick", "-q", help="Quick setup with defaults"),
|
|
35
|
+
reset: bool = typer.Option(False, "--reset", help="Reset configuration"),
|
|
36
|
+
org: str | None = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
"--org",
|
|
39
|
+
help="Organization source (URL or shorthand like github:org/repo)",
|
|
40
|
+
),
|
|
41
|
+
org_url: str | None = typer.Option(
|
|
42
|
+
None, "--org-url", help="Organization config URL (deprecated, use --org)"
|
|
43
|
+
),
|
|
44
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile/team to select"),
|
|
45
|
+
team: str | None = typer.Option(
|
|
46
|
+
None, "--team", "-t", help="Team profile to select (alias for --profile)"
|
|
47
|
+
),
|
|
48
|
+
auth: str | None = typer.Option(None, "--auth", help="Auth spec (env:VAR or command:CMD)"),
|
|
49
|
+
standalone: bool = typer.Option(
|
|
50
|
+
False, "--standalone", help="Standalone mode (no organization)"
|
|
51
|
+
),
|
|
52
|
+
non_interactive: bool = typer.Option(
|
|
53
|
+
False,
|
|
54
|
+
"--non-interactive",
|
|
55
|
+
"--no-interactive",
|
|
56
|
+
help="Fail fast instead of prompting for missing setup inputs",
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Run initial setup wizard.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
scc setup # Interactive wizard
|
|
63
|
+
scc setup --standalone # Standalone mode
|
|
64
|
+
scc setup --org github:acme/config --profile dev # Non-interactive with shorthand
|
|
65
|
+
scc setup --org-url <url> --team dev # Non-interactive (legacy)
|
|
66
|
+
"""
|
|
67
|
+
if reset:
|
|
68
|
+
setup.reset_setup(console)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Handle --profile/--team alias (prefer --profile)
|
|
72
|
+
selected_profile = profile or team
|
|
73
|
+
|
|
74
|
+
# Handle --org/--org-url (prefer --org)
|
|
75
|
+
resolved_url: str | None = None
|
|
76
|
+
if org:
|
|
77
|
+
# Resolve shorthand to URL
|
|
78
|
+
result = resolve_source(org)
|
|
79
|
+
if isinstance(result, ResolveError):
|
|
80
|
+
console.print(
|
|
81
|
+
create_error_panel(
|
|
82
|
+
"Invalid Source",
|
|
83
|
+
result.message,
|
|
84
|
+
hint=result.suggestion or "",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
resolved_url = result.resolved_url
|
|
89
|
+
elif org_url:
|
|
90
|
+
resolved_url = org_url
|
|
91
|
+
|
|
92
|
+
if non_interactive and not (resolved_url or standalone):
|
|
93
|
+
console.print(
|
|
94
|
+
create_error_panel(
|
|
95
|
+
"Missing Setup Inputs",
|
|
96
|
+
"Non-interactive setup requires --org or --standalone.",
|
|
97
|
+
hint="Provide --org <source> or use interactive setup without --non-interactive.",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
raise typer.Exit(EXIT_USAGE)
|
|
101
|
+
|
|
102
|
+
# Non-interactive mode if org source or standalone specified
|
|
103
|
+
if resolved_url or standalone:
|
|
104
|
+
success = setup.run_non_interactive_setup(
|
|
105
|
+
console,
|
|
106
|
+
org_url=resolved_url,
|
|
107
|
+
team=selected_profile,
|
|
108
|
+
auth=auth,
|
|
109
|
+
standalone=standalone,
|
|
110
|
+
)
|
|
111
|
+
if not success:
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Run the setup wizard (--quick flag is a no-op for now, wizard handles all cases)
|
|
116
|
+
setup.run_setup_wizard(console)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
# Config Command
|
|
121
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@handle_errors
|
|
125
|
+
def config_cmd(
|
|
126
|
+
action: str = typer.Argument(None, help="Action: set, get, show, edit, explain"),
|
|
127
|
+
key: str = typer.Argument(None, help="Config key (for set/get, e.g. hooks.enabled)"),
|
|
128
|
+
value: str = typer.Argument(None, help="Value (for set only)"),
|
|
129
|
+
show: bool = typer.Option(False, "--show", help="Show current config"),
|
|
130
|
+
edit: bool = typer.Option(False, "--edit", help="Open config in editor"),
|
|
131
|
+
field: str | None = typer.Option(
|
|
132
|
+
None, "--field", help="Filter explain output to specific field (plugins, session, etc.)"
|
|
133
|
+
),
|
|
134
|
+
workspace: str | None = typer.Option(
|
|
135
|
+
None, "--workspace", help="Workspace path for project config (default: current directory)"
|
|
136
|
+
),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""View or edit configuration.
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
scc config --show # Show all config
|
|
142
|
+
scc config get selected_profile # Get specific key
|
|
143
|
+
scc config set hooks.enabled true # Set a value
|
|
144
|
+
scc config --edit # Open in editor
|
|
145
|
+
scc config explain # Explain effective config
|
|
146
|
+
scc config explain --field plugins # Explain only plugins
|
|
147
|
+
"""
|
|
148
|
+
# Handle action-based commands
|
|
149
|
+
if action == "set":
|
|
150
|
+
if not key or value is None:
|
|
151
|
+
console.print("[red]Usage: scc config set <key> <value>[/red]")
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
_config_set(key, value)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if action == "get":
|
|
157
|
+
if not key:
|
|
158
|
+
console.print("[red]Usage: scc config get <key>[/red]")
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
_config_get(key)
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if action == "explain":
|
|
164
|
+
_config_explain(field_filter=field, workspace_path=workspace)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Handle --show and --edit flags
|
|
168
|
+
if show or action == "show":
|
|
169
|
+
cfg = config.load_user_config()
|
|
170
|
+
console.print(
|
|
171
|
+
create_info_panel(
|
|
172
|
+
"Configuration",
|
|
173
|
+
f"Current settings loaded from {config.CONFIG_FILE}",
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
console.print()
|
|
177
|
+
console.print_json(data=cfg)
|
|
178
|
+
elif edit or action == "edit":
|
|
179
|
+
config.open_in_editor()
|
|
180
|
+
else:
|
|
181
|
+
console.print(
|
|
182
|
+
create_info_panel(
|
|
183
|
+
"Configuration Help",
|
|
184
|
+
"Commands:\n scc config --show View current settings\n scc config --edit Edit in your editor\n scc config get <key> Get a specific value\n scc config set <key> <value> Set a value",
|
|
185
|
+
f"Config location: {config.CONFIG_FILE}",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _config_set(key: str, value: str) -> None:
|
|
191
|
+
"""Set a configuration value by dotted key path."""
|
|
192
|
+
cfg = config.load_user_config()
|
|
193
|
+
|
|
194
|
+
# Parse dotted key path (e.g., "hooks.enabled")
|
|
195
|
+
keys = key.split(".")
|
|
196
|
+
obj = cfg
|
|
197
|
+
for k in keys[:-1]:
|
|
198
|
+
if k not in obj:
|
|
199
|
+
obj[k] = {}
|
|
200
|
+
obj = obj[k]
|
|
201
|
+
|
|
202
|
+
# Parse value (handle booleans and numbers)
|
|
203
|
+
parsed_value: bool | int | str
|
|
204
|
+
if value.lower() == "true":
|
|
205
|
+
parsed_value = True
|
|
206
|
+
elif value.lower() == "false":
|
|
207
|
+
parsed_value = False
|
|
208
|
+
elif value.isdigit():
|
|
209
|
+
parsed_value = int(value)
|
|
210
|
+
else:
|
|
211
|
+
parsed_value = value
|
|
212
|
+
|
|
213
|
+
obj[keys[-1]] = parsed_value
|
|
214
|
+
config.save_user_config(cfg)
|
|
215
|
+
console.print(f"[green]✓ Set {key} = {parsed_value}[/green]")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _config_get(key: str) -> None:
|
|
219
|
+
"""Get a configuration value by dotted key path."""
|
|
220
|
+
cfg = config.load_user_config()
|
|
221
|
+
|
|
222
|
+
# Navigate dotted key path
|
|
223
|
+
keys = key.split(".")
|
|
224
|
+
obj = cfg
|
|
225
|
+
for k in keys:
|
|
226
|
+
if isinstance(obj, dict) and k in obj:
|
|
227
|
+
obj = obj[k]
|
|
228
|
+
else:
|
|
229
|
+
console.print(f"[yellow]Key '{key}' not found[/yellow]")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Display value
|
|
233
|
+
if isinstance(obj, dict):
|
|
234
|
+
console.print_json(data=obj)
|
|
235
|
+
else:
|
|
236
|
+
console.print(str(obj))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _config_explain(field_filter: str | None = None, workspace_path: str | None = None) -> None:
|
|
240
|
+
"""Explain the effective configuration with source attribution.
|
|
241
|
+
|
|
242
|
+
Shows:
|
|
243
|
+
- Effective config values and where they came from
|
|
244
|
+
- Blocked items and the patterns that blocked them
|
|
245
|
+
- Denied additions and why they were denied
|
|
246
|
+
"""
|
|
247
|
+
# Load org config
|
|
248
|
+
org_config = config.load_cached_org_config()
|
|
249
|
+
if not org_config:
|
|
250
|
+
console.print("[red]No organization config found. Run 'scc setup' first.[/red]")
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
# Get selected profile/team
|
|
254
|
+
team = config.get_selected_profile()
|
|
255
|
+
if not team:
|
|
256
|
+
console.print("[red]No team selected. Run 'scc team switch <name>' first.[/red]")
|
|
257
|
+
raise typer.Exit(1)
|
|
258
|
+
|
|
259
|
+
# Determine workspace path
|
|
260
|
+
ws_path = Path(workspace_path) if workspace_path else Path.cwd()
|
|
261
|
+
|
|
262
|
+
# Compute effective config
|
|
263
|
+
effective = profiles.compute_effective_config(
|
|
264
|
+
org_config=org_config,
|
|
265
|
+
team_name=team,
|
|
266
|
+
workspace_path=ws_path,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Build output
|
|
270
|
+
console.print(
|
|
271
|
+
create_info_panel(
|
|
272
|
+
"Effective Configuration",
|
|
273
|
+
f"Organization: {org_config.get('organization', {}).get('name', 'Unknown')}",
|
|
274
|
+
f"Team: {team}",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
console.print()
|
|
278
|
+
|
|
279
|
+
# Show decisions (config values with source attribution)
|
|
280
|
+
_render_config_decisions(effective, field_filter)
|
|
281
|
+
|
|
282
|
+
# Show blocked items
|
|
283
|
+
if effective.blocked_items and (not field_filter or field_filter == "blocked"):
|
|
284
|
+
_render_blocked_items(effective.blocked_items)
|
|
285
|
+
|
|
286
|
+
# Show denied additions
|
|
287
|
+
if effective.denied_additions and (not field_filter or field_filter == "denied"):
|
|
288
|
+
_render_denied_additions(effective.denied_additions)
|
|
289
|
+
|
|
290
|
+
# Show active exceptions
|
|
291
|
+
if not field_filter or field_filter == "exceptions":
|
|
292
|
+
expired_count = _render_active_exceptions()
|
|
293
|
+
if expired_count > 0:
|
|
294
|
+
console.print(
|
|
295
|
+
f"[dim]Note: {expired_count} expired local overrides "
|
|
296
|
+
f"(run `scc exceptions cleanup`)[/dim]"
|
|
297
|
+
)
|
|
298
|
+
console.print()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _render_config_decisions(effective: profiles.EffectiveConfig, field_filter: str | None) -> None:
|
|
302
|
+
"""Render config decisions grouped by field."""
|
|
303
|
+
# Group decisions by field
|
|
304
|
+
by_field: dict[str, list[profiles.ConfigDecision]] = {}
|
|
305
|
+
for decision in effective.decisions:
|
|
306
|
+
field = decision.field.split(".")[0] # Get top-level field
|
|
307
|
+
if field_filter and field != field_filter:
|
|
308
|
+
continue
|
|
309
|
+
if field not in by_field:
|
|
310
|
+
by_field[field] = []
|
|
311
|
+
by_field[field].append(decision)
|
|
312
|
+
|
|
313
|
+
# Also show effective values even if no explicit decisions
|
|
314
|
+
if not field_filter or field_filter == "plugins":
|
|
315
|
+
console.print("[bold cyan]Plugins[/bold cyan]")
|
|
316
|
+
if effective.plugins:
|
|
317
|
+
for plugin in sorted(effective.plugins):
|
|
318
|
+
# Find decision for this plugin
|
|
319
|
+
plugin_decision = next(
|
|
320
|
+
(d for d in effective.decisions if d.field == "plugins" and d.value == plugin),
|
|
321
|
+
None,
|
|
322
|
+
)
|
|
323
|
+
if plugin_decision:
|
|
324
|
+
console.print(
|
|
325
|
+
f" [green]✓[/green] {plugin} [dim](from {plugin_decision.source})[/dim]"
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
console.print(f" [green]✓[/green] {plugin}")
|
|
329
|
+
# Plugin trust model note
|
|
330
|
+
console.print()
|
|
331
|
+
console.print(
|
|
332
|
+
" [dim]Note: Plugins may bundle .mcp.json MCP servers. "
|
|
333
|
+
"SCC does not inspect plugin contents; to restrict, block the plugin.[/dim]"
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
console.print(" [dim]None configured[/dim]")
|
|
337
|
+
console.print()
|
|
338
|
+
|
|
339
|
+
if not field_filter or field_filter == "session":
|
|
340
|
+
console.print("[bold cyan]Session Config[/bold cyan]")
|
|
341
|
+
timeout = effective.session_config.timeout_hours or 8
|
|
342
|
+
auto_resume = effective.session_config.auto_resume
|
|
343
|
+
# Find decision for timeout
|
|
344
|
+
timeout_decision = next(
|
|
345
|
+
(d for d in effective.decisions if "timeout" in d.field.lower()),
|
|
346
|
+
None,
|
|
347
|
+
)
|
|
348
|
+
if timeout_decision:
|
|
349
|
+
console.print(f" timeout_hours: {timeout} [dim](from {timeout_decision.source})[/dim]")
|
|
350
|
+
else:
|
|
351
|
+
console.print(f" timeout_hours: {timeout} [dim](default)[/dim]")
|
|
352
|
+
console.print(f" auto_resume: {auto_resume}")
|
|
353
|
+
console.print()
|
|
354
|
+
|
|
355
|
+
if not field_filter or field_filter == "network":
|
|
356
|
+
console.print("[bold cyan]Network Policy[/bold cyan]")
|
|
357
|
+
policy = effective.network_policy or "default"
|
|
358
|
+
policy_decision = next(
|
|
359
|
+
(d for d in effective.decisions if d.field == "network_policy"),
|
|
360
|
+
None,
|
|
361
|
+
)
|
|
362
|
+
if policy_decision:
|
|
363
|
+
console.print(f" {policy} [dim](from {policy_decision.source})[/dim]")
|
|
364
|
+
else:
|
|
365
|
+
console.print(f" {policy}")
|
|
366
|
+
console.print()
|
|
367
|
+
|
|
368
|
+
if not field_filter or field_filter == "mcp_servers":
|
|
369
|
+
console.print("[bold cyan]MCP Servers[/bold cyan]")
|
|
370
|
+
if effective.mcp_servers:
|
|
371
|
+
for server in effective.mcp_servers:
|
|
372
|
+
# Find decision for this server
|
|
373
|
+
server_decision = next(
|
|
374
|
+
(
|
|
375
|
+
d
|
|
376
|
+
for d in effective.decisions
|
|
377
|
+
if d.field == "mcp_servers" and d.value == server.name
|
|
378
|
+
),
|
|
379
|
+
None,
|
|
380
|
+
)
|
|
381
|
+
server_info = f"{server.name} ({server.type})"
|
|
382
|
+
if server_decision:
|
|
383
|
+
console.print(
|
|
384
|
+
f" [green]✓[/green] {server_info} [dim](from {server_decision.source})[/dim]"
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
console.print(f" [green]✓[/green] {server_info}")
|
|
388
|
+
else:
|
|
389
|
+
console.print(" [dim]None configured[/dim]")
|
|
390
|
+
console.print()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _render_blocked_items(blocked_items: list[profiles.BlockedItem]) -> None:
|
|
394
|
+
"""Render blocked items with patterns and fix-it commands."""
|
|
395
|
+
from scc_cli.utils.fixit import generate_policy_exception_command
|
|
396
|
+
|
|
397
|
+
console.print("[bold red]Blocked Items[/bold red]")
|
|
398
|
+
for item in blocked_items:
|
|
399
|
+
console.print(
|
|
400
|
+
f" [red]✗[/red] [bold]{item.item}[/bold] [dim](blocked by pattern '{item.blocked_by}' from {item.source})[/dim]"
|
|
401
|
+
)
|
|
402
|
+
# Infer target type from source or pattern
|
|
403
|
+
target_type = _infer_target_type(item.item, item.source)
|
|
404
|
+
cmd = generate_policy_exception_command(item.item, target_type)
|
|
405
|
+
console.print(" [dim]To request exception (requires PR):[/dim]")
|
|
406
|
+
console.print(f" [cyan]{cmd}[/cyan]")
|
|
407
|
+
console.print()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _infer_target_type(item: str, source: str) -> str:
|
|
411
|
+
"""Infer target type from item name or source context.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
item: The item name (plugin, server, image)
|
|
415
|
+
source: The source context (e.g., "org.security")
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
One of "plugin", "mcp_server", or "base_image"
|
|
419
|
+
"""
|
|
420
|
+
# Check for common patterns
|
|
421
|
+
item_lower = item.lower()
|
|
422
|
+
|
|
423
|
+
# Image patterns (contains : or @ for tags/digests, or common registries)
|
|
424
|
+
if (
|
|
425
|
+
":" in item
|
|
426
|
+
or "@" in item
|
|
427
|
+
or any(reg in item_lower for reg in ["docker", "ghcr.io", "registry", ".io/", ".com/"])
|
|
428
|
+
):
|
|
429
|
+
return "base_image"
|
|
430
|
+
|
|
431
|
+
# MCP server patterns (often have -api, -server, -mcp suffix or look like URLs)
|
|
432
|
+
if any(pattern in item_lower for pattern in ["-api", "-server", "-mcp", "/"]):
|
|
433
|
+
return "mcp_server"
|
|
434
|
+
|
|
435
|
+
# Default to plugin (most common case for blocked items)
|
|
436
|
+
return "plugin"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _render_denied_additions(denied_additions: list[profiles.DelegationDenied]) -> None:
|
|
440
|
+
"""Render denied additions with reasons and fix-it commands."""
|
|
441
|
+
from scc_cli.utils.fixit import generate_unblock_command
|
|
442
|
+
|
|
443
|
+
console.print("[bold yellow]Denied Additions[/bold yellow]")
|
|
444
|
+
for denied in denied_additions:
|
|
445
|
+
console.print(
|
|
446
|
+
f" [yellow]⚠[/yellow] [bold]{denied.item}[/bold] [dim](requested by {denied.requested_by}: {denied.reason})[/dim]"
|
|
447
|
+
)
|
|
448
|
+
# Infer target type from item name
|
|
449
|
+
target_type = _infer_target_type(denied.item, denied.requested_by)
|
|
450
|
+
cmd = generate_unblock_command(denied.item, target_type)
|
|
451
|
+
console.print(" [dim]To unblock locally:[/dim]")
|
|
452
|
+
console.print(f" [cyan]{cmd}[/cyan]")
|
|
453
|
+
console.print()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _render_active_exceptions() -> int:
|
|
457
|
+
"""Render active exceptions from user and repo stores.
|
|
458
|
+
|
|
459
|
+
Returns the count of expired exceptions found (for user notification).
|
|
460
|
+
"""
|
|
461
|
+
from datetime import datetime, timezone
|
|
462
|
+
|
|
463
|
+
from ..models.exceptions import Exception as SccException
|
|
464
|
+
|
|
465
|
+
# Load exceptions from both stores
|
|
466
|
+
user_store = UserStore()
|
|
467
|
+
repo_store = RepoStore(Path.cwd())
|
|
468
|
+
|
|
469
|
+
user_file = user_store.read()
|
|
470
|
+
repo_file = repo_store.read()
|
|
471
|
+
|
|
472
|
+
# Filter active exceptions
|
|
473
|
+
now = datetime.now(timezone.utc)
|
|
474
|
+
active: list[tuple[str, SccException]] = [] # (source, exception)
|
|
475
|
+
expired_count = 0
|
|
476
|
+
|
|
477
|
+
for exc in user_file.exceptions:
|
|
478
|
+
try:
|
|
479
|
+
expires = datetime.fromisoformat(exc.expires_at.replace("Z", "+00:00"))
|
|
480
|
+
if expires > now:
|
|
481
|
+
active.append(("user", exc))
|
|
482
|
+
else:
|
|
483
|
+
expired_count += 1
|
|
484
|
+
except (ValueError, AttributeError):
|
|
485
|
+
expired_count += 1
|
|
486
|
+
|
|
487
|
+
for exc in repo_file.exceptions:
|
|
488
|
+
try:
|
|
489
|
+
expires = datetime.fromisoformat(exc.expires_at.replace("Z", "+00:00"))
|
|
490
|
+
if expires > now:
|
|
491
|
+
active.append(("repo", exc))
|
|
492
|
+
else:
|
|
493
|
+
expired_count += 1
|
|
494
|
+
except (ValueError, AttributeError):
|
|
495
|
+
expired_count += 1
|
|
496
|
+
|
|
497
|
+
if not active:
|
|
498
|
+
return expired_count
|
|
499
|
+
|
|
500
|
+
console.print("[bold cyan]Active Exceptions[/bold cyan]")
|
|
501
|
+
|
|
502
|
+
for source, exc in active:
|
|
503
|
+
# Format the exception target
|
|
504
|
+
targets: list[str] = []
|
|
505
|
+
if exc.allow.plugins:
|
|
506
|
+
targets.extend(f"plugin:{p}" for p in exc.allow.plugins)
|
|
507
|
+
if exc.allow.mcp_servers:
|
|
508
|
+
targets.extend(f"mcp:{s}" for s in exc.allow.mcp_servers)
|
|
509
|
+
if exc.allow.base_images:
|
|
510
|
+
targets.extend(f"image:{i}" for i in exc.allow.base_images)
|
|
511
|
+
|
|
512
|
+
target_str = ", ".join(targets) if targets else "none"
|
|
513
|
+
|
|
514
|
+
# Calculate expires_in
|
|
515
|
+
try:
|
|
516
|
+
expires = datetime.fromisoformat(exc.expires_at.replace("Z", "+00:00"))
|
|
517
|
+
expires_in = format_relative(expires)
|
|
518
|
+
except (ValueError, AttributeError):
|
|
519
|
+
expires_in = "unknown"
|
|
520
|
+
|
|
521
|
+
scope_badge = "[dim][local][/dim]" if exc.scope == "local" else "[cyan][policy][/cyan]"
|
|
522
|
+
console.print(
|
|
523
|
+
f" {scope_badge} {exc.id} {target_str} "
|
|
524
|
+
f"[dim]expires in {expires_in}[/dim] [dim](source: {source})[/dim]"
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
console.print()
|
|
528
|
+
return expired_count
|