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,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings rendering for Claude Code integration.
|
|
3
|
+
|
|
4
|
+
This module provides the bridge between SCC's marketplace/plugin management
|
|
5
|
+
and Claude Code's settings.local.json format. Key responsibilities:
|
|
6
|
+
|
|
7
|
+
1. render_settings() - Convert effective plugins to Claude settings format
|
|
8
|
+
2. merge_settings() - Non-destructive merge preserving user customizations
|
|
9
|
+
3. check_conflicts() - Detect conflicts between user and team settings
|
|
10
|
+
|
|
11
|
+
Per RQ-11: All paths must be relative for Docker sandbox compatibility.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from scc_cli.marketplace.constants import MANAGED_STATE_FILE
|
|
21
|
+
|
|
22
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
# Render Settings
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_settings(
|
|
28
|
+
effective_plugins: dict[str, Any],
|
|
29
|
+
materialized_marketplaces: dict[str, Any],
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Render effective plugins and marketplaces to Claude settings format.
|
|
32
|
+
|
|
33
|
+
Creates a settings.local.json compatible structure with:
|
|
34
|
+
- extraKnownMarketplaces: Object mapping marketplace names to source configs
|
|
35
|
+
- enabledPlugins: Object mapping plugin references to boolean enable state
|
|
36
|
+
|
|
37
|
+
CRITICAL: Uses canonical_name (from marketplace.json) NOT alias name (from org config).
|
|
38
|
+
Claude Code looks up marketplaces by the 'name' field in marketplace.json,
|
|
39
|
+
not by the key used in the SCC org config.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
effective_plugins: Result from compute_effective_plugins()
|
|
43
|
+
- enabled: Set of enabled plugin references (using alias names)
|
|
44
|
+
- extra_marketplaces: List of marketplace IDs to enable
|
|
45
|
+
materialized_marketplaces: Dict mapping alias name to MaterializedMarketplace-like dicts
|
|
46
|
+
- relative_path: Path relative to project root
|
|
47
|
+
- source_type: Type of source (github, git, directory, url)
|
|
48
|
+
- canonical_name: The actual name from marketplace.json (what Claude Code sees)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with Claude Code settings structure:
|
|
52
|
+
{
|
|
53
|
+
"extraKnownMarketplaces": {
|
|
54
|
+
"canonical-marketplace-name": {
|
|
55
|
+
"source": {"source": "directory", "path": "..."}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"enabledPlugins": {"plugin@canonical-marketplace-name": true, ...}
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
settings: dict[str, Any] = {}
|
|
62
|
+
|
|
63
|
+
# Build alias -> canonical name mapping
|
|
64
|
+
alias_to_canonical: dict[str, str] = {}
|
|
65
|
+
for alias_name, marketplace_data in materialized_marketplaces.items():
|
|
66
|
+
canonical_name = marketplace_data.get("canonical_name", alias_name)
|
|
67
|
+
alias_to_canonical[alias_name] = canonical_name
|
|
68
|
+
|
|
69
|
+
# Build extraKnownMarketplaces as OBJECT with CANONICAL marketplace names as keys
|
|
70
|
+
# Claude Code expects: {"canonical-name": {"source": {"source": "directory", "path": "..."}}}
|
|
71
|
+
extra_marketplaces: dict[str, dict[str, Any]] = {}
|
|
72
|
+
for alias_name, marketplace_data in materialized_marketplaces.items():
|
|
73
|
+
# Get the relative path from the materialized data
|
|
74
|
+
relative_path = marketplace_data.get("relative_path", "")
|
|
75
|
+
# Use canonical name as the key - this is what Claude Code expects
|
|
76
|
+
canonical_name = marketplace_data.get("canonical_name", alias_name)
|
|
77
|
+
|
|
78
|
+
# All local marketplaces use source.source: directory
|
|
79
|
+
# This is because they've been cloned/downloaded to a local path
|
|
80
|
+
extra_marketplaces[canonical_name] = {
|
|
81
|
+
"source": {
|
|
82
|
+
"source": "directory",
|
|
83
|
+
"path": relative_path,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
settings["extraKnownMarketplaces"] = extra_marketplaces
|
|
88
|
+
|
|
89
|
+
# Build enabledPlugins as OBJECT with plugin references as keys
|
|
90
|
+
# Claude Code expects: {"plugin@canonical-marketplace-name": true, ...}
|
|
91
|
+
# We need to translate alias marketplace names to canonical names
|
|
92
|
+
enabled = effective_plugins.get("enabled", set())
|
|
93
|
+
enabled_plugins: dict[str, bool] = {}
|
|
94
|
+
for plugin_ref in enabled:
|
|
95
|
+
plugin_str = str(plugin_ref)
|
|
96
|
+
# Translate marketplace alias to canonical name in plugin reference
|
|
97
|
+
if "@" in plugin_str:
|
|
98
|
+
plugin_name, alias_name = plugin_str.rsplit("@", 1)
|
|
99
|
+
canonical_name = alias_to_canonical.get(alias_name, alias_name)
|
|
100
|
+
translated_ref = f"{plugin_name}@{canonical_name}"
|
|
101
|
+
enabled_plugins[translated_ref] = True
|
|
102
|
+
else:
|
|
103
|
+
# No marketplace specified, keep as-is
|
|
104
|
+
enabled_plugins[plugin_str] = True
|
|
105
|
+
|
|
106
|
+
settings["enabledPlugins"] = enabled_plugins
|
|
107
|
+
|
|
108
|
+
return settings
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
# Merge Settings (Non-Destructive)
|
|
113
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _load_settings(project_dir: Path) -> dict[str, Any]:
|
|
117
|
+
"""Load existing settings.local.json if it exists."""
|
|
118
|
+
settings_path = project_dir / ".claude" / "settings.local.json"
|
|
119
|
+
if settings_path.exists():
|
|
120
|
+
try:
|
|
121
|
+
result: dict[str, Any] = json.loads(settings_path.read_text())
|
|
122
|
+
return result
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
return {}
|
|
125
|
+
return {}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _load_managed_state(project_dir: Path) -> dict[str, Any]:
|
|
129
|
+
"""Load the SCC managed state tracking file."""
|
|
130
|
+
managed_path = project_dir / ".claude" / MANAGED_STATE_FILE
|
|
131
|
+
if managed_path.exists():
|
|
132
|
+
try:
|
|
133
|
+
result: dict[str, Any] = json.loads(managed_path.read_text())
|
|
134
|
+
return result
|
|
135
|
+
except json.JSONDecodeError:
|
|
136
|
+
return {}
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def merge_settings(
|
|
141
|
+
project_dir: Path,
|
|
142
|
+
new_settings: dict[str, Any],
|
|
143
|
+
) -> dict[str, Any]:
|
|
144
|
+
"""Non-destructively merge new settings with existing user settings.
|
|
145
|
+
|
|
146
|
+
This function implements RQ-7 from the research document:
|
|
147
|
+
- Preserves user-added plugins and marketplaces
|
|
148
|
+
- Removes old SCC-managed entries before adding new ones
|
|
149
|
+
- Uses .scc-managed.json to track what SCC has added
|
|
150
|
+
|
|
151
|
+
Algorithm:
|
|
152
|
+
1. Load existing settings.local.json
|
|
153
|
+
2. Load .scc-managed.json to know what was previously SCC-managed
|
|
154
|
+
3. Remove previously managed plugins and marketplaces
|
|
155
|
+
4. Add all new plugins and marketplaces from new_settings
|
|
156
|
+
5. Return merged result (caller responsible for writing)
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
project_dir: Project root directory
|
|
160
|
+
new_settings: New settings from render_settings()
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Merged settings dict ready to write to settings.local.json
|
|
164
|
+
"""
|
|
165
|
+
existing = _load_settings(project_dir)
|
|
166
|
+
managed = _load_managed_state(project_dir)
|
|
167
|
+
|
|
168
|
+
# Get what was previously managed by SCC
|
|
169
|
+
managed_plugins = set(managed.get("managed_plugins", []))
|
|
170
|
+
managed_marketplaces = set(managed.get("managed_marketplaces", []))
|
|
171
|
+
|
|
172
|
+
# Start with existing settings
|
|
173
|
+
merged = dict(existing)
|
|
174
|
+
|
|
175
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
176
|
+
# Process enabledPlugins (object format: {"plugin@market": true, ...})
|
|
177
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
existing_plugins_raw = existing.get("enabledPlugins", {})
|
|
180
|
+
|
|
181
|
+
# Handle legacy array format by converting to object format
|
|
182
|
+
if isinstance(existing_plugins_raw, list):
|
|
183
|
+
# Legacy array format - convert to object with all true
|
|
184
|
+
existing_plugins_obj: dict[str, bool] = {p: True for p in existing_plugins_raw}
|
|
185
|
+
else:
|
|
186
|
+
existing_plugins_obj = dict(existing_plugins_raw)
|
|
187
|
+
|
|
188
|
+
# Remove old SCC-managed plugins
|
|
189
|
+
remaining_user_plugins: dict[str, bool] = {}
|
|
190
|
+
for plugin, enabled in existing_plugins_obj.items():
|
|
191
|
+
if plugin not in managed_plugins:
|
|
192
|
+
remaining_user_plugins[plugin] = enabled
|
|
193
|
+
|
|
194
|
+
# Add new plugins from this render (always enabled=True for SCC-managed)
|
|
195
|
+
new_plugins_obj = new_settings.get("enabledPlugins", {})
|
|
196
|
+
if isinstance(new_plugins_obj, list):
|
|
197
|
+
# Handle if someone passes array format
|
|
198
|
+
new_plugins_obj = {p: True for p in new_plugins_obj}
|
|
199
|
+
|
|
200
|
+
# Merge: user plugins take precedence for existing keys, then add new ones
|
|
201
|
+
merged_plugins: dict[str, bool] = dict(remaining_user_plugins)
|
|
202
|
+
for plugin, enabled in new_plugins_obj.items():
|
|
203
|
+
if plugin not in merged_plugins:
|
|
204
|
+
merged_plugins[plugin] = enabled
|
|
205
|
+
|
|
206
|
+
merged["enabledPlugins"] = merged_plugins
|
|
207
|
+
|
|
208
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
209
|
+
# Process extraKnownMarketplaces (object format)
|
|
210
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
existing_marketplaces = existing.get("extraKnownMarketplaces", {})
|
|
213
|
+
|
|
214
|
+
# Handle legacy array format by converting to object format
|
|
215
|
+
if isinstance(existing_marketplaces, list):
|
|
216
|
+
# Legacy array format - skip it (will be replaced)
|
|
217
|
+
existing_marketplaces = {}
|
|
218
|
+
|
|
219
|
+
# Filter out old SCC-managed marketplaces by checking path in source
|
|
220
|
+
remaining_user_marketplaces: dict[str, Any] = {}
|
|
221
|
+
for name, config in existing_marketplaces.items():
|
|
222
|
+
source = config.get("source", {})
|
|
223
|
+
path = source.get("path", "")
|
|
224
|
+
if path not in managed_marketplaces:
|
|
225
|
+
remaining_user_marketplaces[name] = config
|
|
226
|
+
|
|
227
|
+
# Add new marketplaces from this render
|
|
228
|
+
new_marketplaces = new_settings.get("extraKnownMarketplaces", {})
|
|
229
|
+
|
|
230
|
+
# Merge: user marketplaces take precedence, then add new ones
|
|
231
|
+
merged_marketplaces: dict[str, Any] = dict(remaining_user_marketplaces)
|
|
232
|
+
for name, config in new_marketplaces.items():
|
|
233
|
+
if name not in merged_marketplaces:
|
|
234
|
+
merged_marketplaces[name] = config
|
|
235
|
+
|
|
236
|
+
merged["extraKnownMarketplaces"] = merged_marketplaces
|
|
237
|
+
|
|
238
|
+
return merged
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
# Conflict Detection
|
|
243
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def check_conflicts(
|
|
247
|
+
existing_plugins: list[str],
|
|
248
|
+
blocked_plugins: list[dict[str, Any]],
|
|
249
|
+
) -> list[str]:
|
|
250
|
+
"""Check for conflicts between user plugins and team security policy.
|
|
251
|
+
|
|
252
|
+
Generates human-readable warnings when a user has installed plugins
|
|
253
|
+
that would be blocked by the team's security policy.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
existing_plugins: List of plugin references from user's current settings
|
|
257
|
+
blocked_plugins: List of blocked plugin dicts from EffectivePlugins.blocked
|
|
258
|
+
Each dict has: plugin_id, reason, pattern
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of warning strings for display to user
|
|
262
|
+
"""
|
|
263
|
+
warnings: list[str] = []
|
|
264
|
+
|
|
265
|
+
# Build a set of blocked plugin IDs for fast lookup
|
|
266
|
+
blocked_ids = {b.get("plugin_id", "") for b in blocked_plugins}
|
|
267
|
+
|
|
268
|
+
for plugin in existing_plugins:
|
|
269
|
+
if plugin in blocked_ids:
|
|
270
|
+
# Find the block details
|
|
271
|
+
for blocked in blocked_plugins:
|
|
272
|
+
if blocked.get("plugin_id") == plugin:
|
|
273
|
+
reason = blocked.get("reason", "Blocked by policy")
|
|
274
|
+
pattern = blocked.get("pattern", "")
|
|
275
|
+
warnings.append(
|
|
276
|
+
f"⚠️ Plugin '{plugin}' is blocked by team policy: {reason} "
|
|
277
|
+
f"(matched pattern: {pattern})"
|
|
278
|
+
)
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
return warnings
|