scc-cli 1.4.1__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.

Files changed (113) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +706 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1454 -0
  16. scc_cli/cli_org.py +1428 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +892 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +604 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1074 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1521 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/adapter.py +74 -0
  46. scc_cli/marketplace/compute.py +377 -0
  47. scc_cli/marketplace/constants.py +87 -0
  48. scc_cli/marketplace/managed.py +135 -0
  49. scc_cli/marketplace/materialize.py +723 -0
  50. scc_cli/marketplace/normalize.py +548 -0
  51. scc_cli/marketplace/render.py +257 -0
  52. scc_cli/marketplace/resolve.py +459 -0
  53. scc_cli/marketplace/schema.py +506 -0
  54. scc_cli/marketplace/sync.py +260 -0
  55. scc_cli/marketplace/team_cache.py +195 -0
  56. scc_cli/marketplace/team_fetch.py +688 -0
  57. scc_cli/marketplace/trust.py +244 -0
  58. scc_cli/models/__init__.py +41 -0
  59. scc_cli/models/exceptions.py +273 -0
  60. scc_cli/models/plugin_audit.py +434 -0
  61. scc_cli/org_templates.py +269 -0
  62. scc_cli/output_mode.py +167 -0
  63. scc_cli/panels.py +113 -0
  64. scc_cli/platform.py +350 -0
  65. scc_cli/profiles.py +960 -0
  66. scc_cli/remote.py +443 -0
  67. scc_cli/schemas/__init__.py +1 -0
  68. scc_cli/schemas/org-v1.schema.json +456 -0
  69. scc_cli/schemas/team-config.v1.schema.json +163 -0
  70. scc_cli/sessions.py +425 -0
  71. scc_cli/setup.py +588 -0
  72. scc_cli/source_resolver.py +470 -0
  73. scc_cli/stats.py +378 -0
  74. scc_cli/stores/__init__.py +13 -0
  75. scc_cli/stores/exception_store.py +251 -0
  76. scc_cli/subprocess_utils.py +88 -0
  77. scc_cli/teams.py +382 -0
  78. scc_cli/templates/__init__.py +2 -0
  79. scc_cli/templates/org/__init__.py +0 -0
  80. scc_cli/templates/org/minimal.json +19 -0
  81. scc_cli/templates/org/reference.json +74 -0
  82. scc_cli/templates/org/strict.json +38 -0
  83. scc_cli/templates/org/teams.json +42 -0
  84. scc_cli/templates/statusline.sh +75 -0
  85. scc_cli/theme.py +348 -0
  86. scc_cli/ui/__init__.py +124 -0
  87. scc_cli/ui/branding.py +68 -0
  88. scc_cli/ui/chrome.py +395 -0
  89. scc_cli/ui/dashboard/__init__.py +62 -0
  90. scc_cli/ui/dashboard/_dashboard.py +677 -0
  91. scc_cli/ui/dashboard/loaders.py +395 -0
  92. scc_cli/ui/dashboard/models.py +184 -0
  93. scc_cli/ui/dashboard/orchestrator.py +390 -0
  94. scc_cli/ui/formatters.py +443 -0
  95. scc_cli/ui/gate.py +350 -0
  96. scc_cli/ui/help.py +157 -0
  97. scc_cli/ui/keys.py +538 -0
  98. scc_cli/ui/list_screen.py +431 -0
  99. scc_cli/ui/picker.py +700 -0
  100. scc_cli/ui/prompts.py +200 -0
  101. scc_cli/ui/wizard.py +675 -0
  102. scc_cli/update.py +680 -0
  103. scc_cli/utils/__init__.py +39 -0
  104. scc_cli/utils/fixit.py +264 -0
  105. scc_cli/utils/fuzzy.py +124 -0
  106. scc_cli/utils/locks.py +101 -0
  107. scc_cli/utils/ttl.py +376 -0
  108. scc_cli/validate.py +455 -0
  109. scc_cli-1.4.1.dist-info/METADATA +369 -0
  110. scc_cli-1.4.1.dist-info/RECORD +113 -0
  111. scc_cli-1.4.1.dist-info/WHEEL +4 -0
  112. scc_cli-1.4.1.dist-info/entry_points.txt +2 -0
  113. scc_cli-1.4.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,257 @@
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
+ Args:
38
+ effective_plugins: Result from compute_effective_plugins()
39
+ - enabled: Set of enabled plugin references
40
+ - extra_marketplaces: List of marketplace IDs to enable
41
+ materialized_marketplaces: Dict mapping name to MaterializedMarketplace-like dicts
42
+ - relative_path: Path relative to project root
43
+ - source_type: Type of source (github, git, directory, url)
44
+
45
+ Returns:
46
+ Dict with Claude Code settings structure:
47
+ {
48
+ "extraKnownMarketplaces": {
49
+ "marketplace-name": {
50
+ "source": {"source": "directory", "path": "..."}
51
+ }
52
+ },
53
+ "enabledPlugins": {"plugin@marketplace": true, ...}
54
+ }
55
+ """
56
+ settings: dict[str, Any] = {}
57
+
58
+ # Build extraKnownMarketplaces as OBJECT with marketplace names as keys
59
+ # Claude Code expects: {"name": {"source": {"source": "directory", "path": "..."}}}
60
+ extra_marketplaces: dict[str, dict[str, Any]] = {}
61
+ for name, marketplace_data in materialized_marketplaces.items():
62
+ # Get the relative path from the materialized data
63
+ relative_path = marketplace_data.get("relative_path", "")
64
+
65
+ # All local marketplaces use source.source: directory
66
+ # This is because they've been cloned/downloaded to a local path
67
+ extra_marketplaces[name] = {
68
+ "source": {
69
+ "source": "directory",
70
+ "path": relative_path,
71
+ }
72
+ }
73
+
74
+ settings["extraKnownMarketplaces"] = extra_marketplaces
75
+
76
+ # Build enabledPlugins as OBJECT with plugin references as keys
77
+ # Claude Code expects: {"plugin@marketplace": true, ...}
78
+ enabled = effective_plugins.get("enabled", set())
79
+ enabled_plugins: dict[str, bool] = {}
80
+ for plugin in enabled:
81
+ enabled_plugins[str(plugin)] = True
82
+ settings["enabledPlugins"] = enabled_plugins
83
+
84
+ return settings
85
+
86
+
87
+ # ─────────────────────────────────────────────────────────────────────────────
88
+ # Merge Settings (Non-Destructive)
89
+ # ─────────────────────────────────────────────────────────────────────────────
90
+
91
+
92
+ def _load_settings(project_dir: Path) -> dict[str, Any]:
93
+ """Load existing settings.local.json if it exists."""
94
+ settings_path = project_dir / ".claude" / "settings.local.json"
95
+ if settings_path.exists():
96
+ try:
97
+ result: dict[str, Any] = json.loads(settings_path.read_text())
98
+ return result
99
+ except json.JSONDecodeError:
100
+ return {}
101
+ return {}
102
+
103
+
104
+ def _load_managed_state(project_dir: Path) -> dict[str, Any]:
105
+ """Load the SCC managed state tracking file."""
106
+ managed_path = project_dir / ".claude" / MANAGED_STATE_FILE
107
+ if managed_path.exists():
108
+ try:
109
+ result: dict[str, Any] = json.loads(managed_path.read_text())
110
+ return result
111
+ except json.JSONDecodeError:
112
+ return {}
113
+ return {}
114
+
115
+
116
+ def merge_settings(
117
+ project_dir: Path,
118
+ new_settings: dict[str, Any],
119
+ ) -> dict[str, Any]:
120
+ """Non-destructively merge new settings with existing user settings.
121
+
122
+ This function implements RQ-7 from the research document:
123
+ - Preserves user-added plugins and marketplaces
124
+ - Removes old SCC-managed entries before adding new ones
125
+ - Uses .scc-managed.json to track what SCC has added
126
+
127
+ Algorithm:
128
+ 1. Load existing settings.local.json
129
+ 2. Load .scc-managed.json to know what was previously SCC-managed
130
+ 3. Remove previously managed plugins and marketplaces
131
+ 4. Add all new plugins and marketplaces from new_settings
132
+ 5. Return merged result (caller responsible for writing)
133
+
134
+ Args:
135
+ project_dir: Project root directory
136
+ new_settings: New settings from render_settings()
137
+
138
+ Returns:
139
+ Merged settings dict ready to write to settings.local.json
140
+ """
141
+ existing = _load_settings(project_dir)
142
+ managed = _load_managed_state(project_dir)
143
+
144
+ # Get what was previously managed by SCC
145
+ managed_plugins = set(managed.get("managed_plugins", []))
146
+ managed_marketplaces = set(managed.get("managed_marketplaces", []))
147
+
148
+ # Start with existing settings
149
+ merged = dict(existing)
150
+
151
+ # ─────────────────────────────────────────────────────────────────────────
152
+ # Process enabledPlugins (object format: {"plugin@market": true, ...})
153
+ # ─────────────────────────────────────────────────────────────────────────
154
+
155
+ existing_plugins_raw = existing.get("enabledPlugins", {})
156
+
157
+ # Handle legacy array format by converting to object format
158
+ if isinstance(existing_plugins_raw, list):
159
+ # Legacy array format - convert to object with all true
160
+ existing_plugins_obj: dict[str, bool] = {p: True for p in existing_plugins_raw}
161
+ else:
162
+ existing_plugins_obj = dict(existing_plugins_raw)
163
+
164
+ # Remove old SCC-managed plugins
165
+ remaining_user_plugins: dict[str, bool] = {}
166
+ for plugin, enabled in existing_plugins_obj.items():
167
+ if plugin not in managed_plugins:
168
+ remaining_user_plugins[plugin] = enabled
169
+
170
+ # Add new plugins from this render (always enabled=True for SCC-managed)
171
+ new_plugins_obj = new_settings.get("enabledPlugins", {})
172
+ if isinstance(new_plugins_obj, list):
173
+ # Handle if someone passes array format
174
+ new_plugins_obj = {p: True for p in new_plugins_obj}
175
+
176
+ # Merge: user plugins take precedence for existing keys, then add new ones
177
+ merged_plugins: dict[str, bool] = dict(remaining_user_plugins)
178
+ for plugin, enabled in new_plugins_obj.items():
179
+ if plugin not in merged_plugins:
180
+ merged_plugins[plugin] = enabled
181
+
182
+ merged["enabledPlugins"] = merged_plugins
183
+
184
+ # ─────────────────────────────────────────────────────────────────────────
185
+ # Process extraKnownMarketplaces (object format)
186
+ # ─────────────────────────────────────────────────────────────────────────
187
+
188
+ existing_marketplaces = existing.get("extraKnownMarketplaces", {})
189
+
190
+ # Handle legacy array format by converting to object format
191
+ if isinstance(existing_marketplaces, list):
192
+ # Legacy array format - skip it (will be replaced)
193
+ existing_marketplaces = {}
194
+
195
+ # Filter out old SCC-managed marketplaces by checking path in source
196
+ remaining_user_marketplaces: dict[str, Any] = {}
197
+ for name, config in existing_marketplaces.items():
198
+ source = config.get("source", {})
199
+ path = source.get("path", "")
200
+ if path not in managed_marketplaces:
201
+ remaining_user_marketplaces[name] = config
202
+
203
+ # Add new marketplaces from this render
204
+ new_marketplaces = new_settings.get("extraKnownMarketplaces", {})
205
+
206
+ # Merge: user marketplaces take precedence, then add new ones
207
+ merged_marketplaces: dict[str, Any] = dict(remaining_user_marketplaces)
208
+ for name, config in new_marketplaces.items():
209
+ if name not in merged_marketplaces:
210
+ merged_marketplaces[name] = config
211
+
212
+ merged["extraKnownMarketplaces"] = merged_marketplaces
213
+
214
+ return merged
215
+
216
+
217
+ # ─────────────────────────────────────────────────────────────────────────────
218
+ # Conflict Detection
219
+ # ─────────────────────────────────────────────────────────────────────────────
220
+
221
+
222
+ def check_conflicts(
223
+ existing_plugins: list[str],
224
+ blocked_plugins: list[dict[str, Any]],
225
+ ) -> list[str]:
226
+ """Check for conflicts between user plugins and team security policy.
227
+
228
+ Generates human-readable warnings when a user has installed plugins
229
+ that would be blocked by the team's security policy.
230
+
231
+ Args:
232
+ existing_plugins: List of plugin references from user's current settings
233
+ blocked_plugins: List of blocked plugin dicts from EffectivePlugins.blocked
234
+ Each dict has: plugin_id, reason, pattern
235
+
236
+ Returns:
237
+ List of warning strings for display to user
238
+ """
239
+ warnings: list[str] = []
240
+
241
+ # Build a set of blocked plugin IDs for fast lookup
242
+ blocked_ids = {b.get("plugin_id", "") for b in blocked_plugins}
243
+
244
+ for plugin in existing_plugins:
245
+ if plugin in blocked_ids:
246
+ # Find the block details
247
+ for blocked in blocked_plugins:
248
+ if blocked.get("plugin_id") == plugin:
249
+ reason = blocked.get("reason", "Blocked by policy")
250
+ pattern = blocked.get("pattern", "")
251
+ warnings.append(
252
+ f"⚠️ Plugin '{plugin}' is blocked by team policy: {reason} "
253
+ f"(matched pattern: {pattern})"
254
+ )
255
+ break
256
+
257
+ return warnings