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.

Files changed (153) 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 +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. 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