scc-cli 1.4.0__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 +259 -0
- scc_cli/cli_admin.py +683 -0
- scc_cli/cli_audit.py +245 -0
- scc_cli/cli_common.py +166 -0
- scc_cli/cli_config.py +527 -0
- scc_cli/cli_exceptions.py +705 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/cli_init.py +272 -0
- scc_cli/cli_launch.py +1400 -0
- scc_cli/cli_org.py +1433 -0
- scc_cli/cli_support.py +322 -0
- scc_cli/cli_team.py +858 -0
- scc_cli/cli_worktree.py +865 -0
- scc_cli/config.py +583 -0
- scc_cli/console.py +562 -0
- scc_cli/constants.py +79 -0
- scc_cli/contexts.py +377 -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 +466 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +603 -0
- scc_cli/doctor/__init__.py +99 -0
- scc_cli/doctor/checks.py +1082 -0
- scc_cli/doctor/render.py +346 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/errors.py +288 -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/exit_codes.py +55 -0
- scc_cli/git.py +1405 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +96 -0
- scc_cli/kinds.py +62 -0
- scc_cli/marketplace/__init__.py +123 -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 +723 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +238 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +502 -0
- scc_cli/marketplace/sync.py +257 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +688 -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 +1034 -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/sessions.py +425 -0
- scc_cli/setup.py +582 -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 +339 -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 +124 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +395 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +669 -0
- scc_cli/ui/dashboard/loaders.py +369 -0
- scc_cli/ui/dashboard/models.py +184 -0
- scc_cli/ui/dashboard/orchestrator.py +337 -0
- scc_cli/ui/formatters.py +443 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +521 -0
- scc_cli/ui/list_screen.py +431 -0
- scc_cli/ui/picker.py +700 -0
- scc_cli/ui/prompts.py +200 -0
- scc_cli/ui/wizard.py +490 -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 +101 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.4.0.dist-info/METADATA +369 -0
- scc_cli-1.4.0.dist-info/RECORD +112 -0
- scc_cli-1.4.0.dist-info/WHEEL +4 -0
- scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
- scc_cli-1.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provide subprocess utilities for consistent error handling.
|
|
3
|
+
|
|
4
|
+
Define wrapper functions for subprocess execution with graceful timeout
|
|
5
|
+
and missing executable handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_command(
|
|
13
|
+
cmd: list[str],
|
|
14
|
+
timeout: int = 10,
|
|
15
|
+
cwd: str | None = None,
|
|
16
|
+
) -> str | None:
|
|
17
|
+
"""Run command, return stdout if successful, None otherwise.
|
|
18
|
+
|
|
19
|
+
Handle timeouts and missing executables gracefully.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cmd: Command and arguments as list of strings.
|
|
23
|
+
timeout: Maximum seconds to wait for command.
|
|
24
|
+
cwd: Working directory for command execution.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Stripped stdout on success, None on any failure.
|
|
28
|
+
"""
|
|
29
|
+
# Pre-check: handle empty command list
|
|
30
|
+
if not cmd:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
# Pre-check: is the executable available?
|
|
34
|
+
if not shutil.which(cmd[0]):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
cmd,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
cwd=cwd,
|
|
44
|
+
)
|
|
45
|
+
if result.returncode == 0:
|
|
46
|
+
return result.stdout.strip()
|
|
47
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
48
|
+
pass
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_command_bool(
|
|
53
|
+
cmd: list[str],
|
|
54
|
+
timeout: int = 10,
|
|
55
|
+
cwd: str | None = None,
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""Run command, return True if exit code is 0.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
cmd: Command and arguments as list of strings.
|
|
61
|
+
timeout: Maximum seconds to wait for command.
|
|
62
|
+
cwd: Working directory for command execution.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if command succeeded (exit code 0), False otherwise.
|
|
66
|
+
"""
|
|
67
|
+
return run_command(cmd, timeout, cwd) is not None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run_command_lines(
|
|
71
|
+
cmd: list[str],
|
|
72
|
+
timeout: int = 10,
|
|
73
|
+
cwd: str | None = None,
|
|
74
|
+
) -> list[str]:
|
|
75
|
+
"""Run command, return stdout split into lines.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
cmd: Command and arguments as list of strings.
|
|
79
|
+
timeout: Maximum seconds to wait for command.
|
|
80
|
+
cwd: Working directory for command execution.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of output lines on success, empty list on failure.
|
|
84
|
+
"""
|
|
85
|
+
output = run_command(cmd, timeout, cwd)
|
|
86
|
+
if output is None:
|
|
87
|
+
return []
|
|
88
|
+
return [line for line in output.split("\n") if line.strip()]
|
scc_cli/teams.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Team profile management.
|
|
3
|
+
|
|
4
|
+
Simplified architecture: SCC generates extraKnownMarketplaces + enabledPlugins,
|
|
5
|
+
Claude Code handles plugin fetching, installation, and updates natively.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from . import config as config_module
|
|
14
|
+
from .theme import Indicators
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .ui.list_screen import ListItem
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TeamInfo:
|
|
22
|
+
"""Information about a team profile.
|
|
23
|
+
|
|
24
|
+
Provides a typed representation of team data for use in the UI layer.
|
|
25
|
+
Use from_dict() to construct from raw config dicts, and to_list_item()
|
|
26
|
+
to convert for display in pickers.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: Team/profile name (unique identifier).
|
|
30
|
+
description: Human-readable team description.
|
|
31
|
+
plugin: Optional plugin name for the team.
|
|
32
|
+
marketplace: Optional marketplace name.
|
|
33
|
+
marketplace_type: Optional marketplace type (e.g., "github").
|
|
34
|
+
marketplace_repo: Optional marketplace repository path.
|
|
35
|
+
credential_status: Credential state ("valid", "expired", "expiring", None).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
description: str = ""
|
|
40
|
+
plugin: str | None = None
|
|
41
|
+
marketplace: str | None = None
|
|
42
|
+
marketplace_type: str | None = None
|
|
43
|
+
marketplace_repo: str | None = None
|
|
44
|
+
credential_status: str | None = None
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(cls, data: dict[str, Any]) -> TeamInfo:
|
|
48
|
+
"""Create TeamInfo from a dict representation.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
data: Dict with team fields (from list_teams or get_team_details).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
TeamInfo dataclass instance.
|
|
55
|
+
"""
|
|
56
|
+
return cls(
|
|
57
|
+
name=data.get("name", "unknown"),
|
|
58
|
+
description=data.get("description", ""),
|
|
59
|
+
plugin=data.get("plugin"),
|
|
60
|
+
marketplace=data.get("marketplace"),
|
|
61
|
+
marketplace_type=data.get("marketplace_type"),
|
|
62
|
+
marketplace_repo=data.get("marketplace_repo"),
|
|
63
|
+
credential_status=data.get("credential_status"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def to_list_item(self, *, current_team: str | None = None) -> ListItem[TeamInfo]:
|
|
67
|
+
"""Convert to ListItem for display in pickers.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
current_team: Currently selected team name (marked with indicator).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
ListItem suitable for ListScreen display.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> team = TeamInfo(name="platform", description="Platform team")
|
|
77
|
+
>>> item = team.to_list_item(current_team="platform")
|
|
78
|
+
>>> item.label
|
|
79
|
+
'✓ platform'
|
|
80
|
+
"""
|
|
81
|
+
from .ui.list_screen import ListItem
|
|
82
|
+
|
|
83
|
+
is_current = current_team is not None and self.name == current_team
|
|
84
|
+
|
|
85
|
+
# Build label with current indicator
|
|
86
|
+
label = f"{Indicators.get('PASS')} {self.name}" if is_current else self.name
|
|
87
|
+
|
|
88
|
+
# Check for credential/governance status
|
|
89
|
+
governance_status: str | None = None
|
|
90
|
+
if self.credential_status == "expired":
|
|
91
|
+
governance_status = "blocked"
|
|
92
|
+
elif self.credential_status == "expiring":
|
|
93
|
+
governance_status = "warning"
|
|
94
|
+
|
|
95
|
+
# Build description parts
|
|
96
|
+
desc_parts: list[str] = []
|
|
97
|
+
if self.description:
|
|
98
|
+
desc_parts.append(self.description)
|
|
99
|
+
if self.credential_status == "expired":
|
|
100
|
+
desc_parts.append("(credentials expired)")
|
|
101
|
+
elif self.credential_status == "expiring":
|
|
102
|
+
desc_parts.append("(credentials expiring)")
|
|
103
|
+
|
|
104
|
+
return ListItem(
|
|
105
|
+
value=self,
|
|
106
|
+
label=label,
|
|
107
|
+
description=" ".join(desc_parts),
|
|
108
|
+
governance_status=governance_status,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def list_teams(
|
|
113
|
+
cfg: dict[str, Any], org_config: dict[str, Any] | None = None
|
|
114
|
+
) -> list[dict[str, Any]]:
|
|
115
|
+
"""List available teams from configuration.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
cfg: User config (used for legacy fallback)
|
|
119
|
+
org_config: Organization config with profiles. If provided, uses
|
|
120
|
+
NEW architecture. If None, falls back to legacy behavior.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of team dicts with name, description, plugin
|
|
124
|
+
"""
|
|
125
|
+
# NEW architecture: use org_config for profiles
|
|
126
|
+
if org_config is not None:
|
|
127
|
+
profiles = org_config.get("profiles", {})
|
|
128
|
+
else:
|
|
129
|
+
# Legacy fallback
|
|
130
|
+
profiles = cfg.get("profiles", {})
|
|
131
|
+
|
|
132
|
+
teams = []
|
|
133
|
+
for name, info in profiles.items():
|
|
134
|
+
teams.append(
|
|
135
|
+
{
|
|
136
|
+
"name": name,
|
|
137
|
+
"description": info.get("description", ""),
|
|
138
|
+
"plugin": info.get("plugin"),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return teams
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_team_details(
|
|
146
|
+
team: str, cfg: dict[str, Any], org_config: dict[str, Any] | None = None
|
|
147
|
+
) -> dict[str, Any] | None:
|
|
148
|
+
"""Get detailed information for a specific team.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
team: Team/profile name.
|
|
152
|
+
cfg: User config (used for legacy fallback).
|
|
153
|
+
org_config: Organization config. If provided, uses NEW architecture.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Team details dict, or None if team doesn't exist.
|
|
157
|
+
"""
|
|
158
|
+
# NEW architecture: use org_config for profiles
|
|
159
|
+
if org_config is not None:
|
|
160
|
+
profiles = org_config.get("profiles", {})
|
|
161
|
+
marketplaces = org_config.get("marketplaces", [])
|
|
162
|
+
else:
|
|
163
|
+
# Legacy fallback
|
|
164
|
+
profiles = cfg.get("profiles", {})
|
|
165
|
+
marketplaces = []
|
|
166
|
+
|
|
167
|
+
team_info = profiles.get(team)
|
|
168
|
+
if not team_info:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
# Get marketplace info
|
|
172
|
+
if org_config is not None:
|
|
173
|
+
# NEW: look up marketplace by name from org_config
|
|
174
|
+
marketplace_name = team_info.get("marketplace")
|
|
175
|
+
marketplace: dict[str, Any] = next(
|
|
176
|
+
(m for m in marketplaces if m.get("name") == marketplace_name),
|
|
177
|
+
{},
|
|
178
|
+
)
|
|
179
|
+
return {
|
|
180
|
+
"name": team,
|
|
181
|
+
"description": team_info.get("description", ""),
|
|
182
|
+
"plugin": team_info.get("plugin"),
|
|
183
|
+
"marketplace": marketplace.get("name"),
|
|
184
|
+
"marketplace_type": marketplace.get("type"),
|
|
185
|
+
"marketplace_repo": marketplace.get("repo"),
|
|
186
|
+
}
|
|
187
|
+
else:
|
|
188
|
+
# Legacy: single marketplace in cfg
|
|
189
|
+
marketplace = cfg.get("marketplace", {})
|
|
190
|
+
return {
|
|
191
|
+
"name": team,
|
|
192
|
+
"description": team_info.get("description", ""),
|
|
193
|
+
"plugin": team_info.get("plugin"),
|
|
194
|
+
"marketplace": marketplace.get("name"),
|
|
195
|
+
"marketplace_repo": marketplace.get("repo"),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_team_sandbox_settings(team_name: str, cfg: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
200
|
+
"""Generate sandbox settings for a team profile.
|
|
201
|
+
|
|
202
|
+
Return settings.json content with extraKnownMarketplaces
|
|
203
|
+
and enabledPlugins configured for Claude Code.
|
|
204
|
+
|
|
205
|
+
This is the core function of the simplified architecture:
|
|
206
|
+
- SCC injects these settings into the Docker sandbox volume
|
|
207
|
+
- Claude Code sees extraKnownMarketplaces and fetches the marketplace
|
|
208
|
+
- Claude Code installs the specified plugin automatically
|
|
209
|
+
- Teams maintain their plugins in the marketplace repo
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
team_name: Name of the team profile (e.g., "api-team").
|
|
213
|
+
cfg: Optional config dict. If None, load from config file.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dict with extraKnownMarketplaces and enabledPlugins for settings.json.
|
|
217
|
+
Return empty dict if team has no plugin configured.
|
|
218
|
+
"""
|
|
219
|
+
if cfg is None:
|
|
220
|
+
cfg = config_module.load_config()
|
|
221
|
+
|
|
222
|
+
marketplace = cfg.get("marketplace", {})
|
|
223
|
+
marketplace_name = marketplace.get("name", "sundsvall")
|
|
224
|
+
marketplace_repo = marketplace.get("repo", "sundsvall/claude-plugins-marketplace")
|
|
225
|
+
|
|
226
|
+
profile = cfg.get("profiles", {}).get(team_name, {})
|
|
227
|
+
plugin_name = profile.get("plugin")
|
|
228
|
+
|
|
229
|
+
# No plugin configured for this profile
|
|
230
|
+
if not plugin_name:
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
# Generate settings that Claude Code understands
|
|
234
|
+
return {
|
|
235
|
+
"extraKnownMarketplaces": {
|
|
236
|
+
marketplace_name: {
|
|
237
|
+
"source": {
|
|
238
|
+
"source": "github",
|
|
239
|
+
"repo": marketplace_repo,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"enabledPlugins": [f"{plugin_name}@{marketplace_name}"],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_team_plugin_id(team_name: str, cfg: dict[str, Any] | None = None) -> str | None:
|
|
248
|
+
"""Get the full plugin ID for a team (e.g., "api-team@sundsvall").
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
team_name: Name of the team profile.
|
|
252
|
+
cfg: Optional config dict. If None, load from config file.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Full plugin ID string, or None if team has no plugin configured.
|
|
256
|
+
"""
|
|
257
|
+
if cfg is None:
|
|
258
|
+
cfg = config_module.load_config()
|
|
259
|
+
|
|
260
|
+
marketplace = cfg.get("marketplace", {})
|
|
261
|
+
marketplace_name = marketplace.get("name", "sundsvall")
|
|
262
|
+
|
|
263
|
+
profile = cfg.get("profiles", {}).get(team_name, {})
|
|
264
|
+
plugin_name = profile.get("plugin")
|
|
265
|
+
|
|
266
|
+
if not plugin_name:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
return f"{plugin_name}@{marketplace_name}"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def validate_team_profile(
|
|
273
|
+
team_name: str,
|
|
274
|
+
cfg: dict[str, Any] | None = None,
|
|
275
|
+
org_config: dict[str, Any] | None = None,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
"""Validate a team profile configuration.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
team_name: Name of the team/profile to validate.
|
|
281
|
+
cfg: User config (deprecated, kept for backward compatibility).
|
|
282
|
+
org_config: Organization config with profiles and marketplaces.
|
|
283
|
+
If provided, use NEW architecture. If None, fall back to
|
|
284
|
+
legacy behavior (reading profiles from cfg).
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Dict with keys: valid (bool), team (str), plugin (str or None),
|
|
288
|
+
errors (list of str), warnings (list of str).
|
|
289
|
+
"""
|
|
290
|
+
if cfg is None:
|
|
291
|
+
cfg = config_module.load_config()
|
|
292
|
+
|
|
293
|
+
result: dict[str, Any] = {
|
|
294
|
+
"valid": True,
|
|
295
|
+
"team": team_name,
|
|
296
|
+
"plugin": None,
|
|
297
|
+
"errors": [],
|
|
298
|
+
"warnings": [],
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# NEW architecture: use org_config for profiles
|
|
302
|
+
if org_config is not None:
|
|
303
|
+
profiles = org_config.get("profiles", {})
|
|
304
|
+
marketplaces = org_config.get("marketplaces", [])
|
|
305
|
+
else:
|
|
306
|
+
# Legacy fallback: read from user config (deprecated)
|
|
307
|
+
profiles = cfg.get("profiles", {})
|
|
308
|
+
marketplaces = []
|
|
309
|
+
|
|
310
|
+
# Check if team exists
|
|
311
|
+
if team_name not in profiles:
|
|
312
|
+
result["valid"] = False
|
|
313
|
+
result["errors"].append(f"Team '{team_name}' not found in profiles")
|
|
314
|
+
return result
|
|
315
|
+
|
|
316
|
+
profile = profiles[team_name]
|
|
317
|
+
result["plugin"] = profile.get("plugin")
|
|
318
|
+
|
|
319
|
+
# Check marketplace configuration (NEW architecture)
|
|
320
|
+
if org_config is not None:
|
|
321
|
+
marketplace_name = profile.get("marketplace")
|
|
322
|
+
if marketplace_name:
|
|
323
|
+
# Find the marketplace in org_config
|
|
324
|
+
marketplace_found = any(m.get("name") == marketplace_name for m in marketplaces)
|
|
325
|
+
if not marketplace_found:
|
|
326
|
+
result["warnings"].append(f"Marketplace '{marketplace_name}' not found")
|
|
327
|
+
else:
|
|
328
|
+
# Legacy: check single marketplace
|
|
329
|
+
marketplace = cfg.get("marketplace", {})
|
|
330
|
+
if not marketplace.get("repo"):
|
|
331
|
+
result["warnings"].append("No marketplace repo configured")
|
|
332
|
+
|
|
333
|
+
# Check if plugin is configured (not required for 'base' profile)
|
|
334
|
+
if not result["plugin"] and team_name != "base":
|
|
335
|
+
result["warnings"].append(
|
|
336
|
+
f"Team '{team_name}' has no plugin configured - using base settings"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://scc-cli.dev/schemas/org-v1.json",
|
|
3
|
+
"schema_version": "{{SCHEMA_VERSION}}",
|
|
4
|
+
"min_cli_version": "{{MIN_CLI_VERSION}}",
|
|
5
|
+
"organization": {
|
|
6
|
+
"name": "{{ORG_NAME}}",
|
|
7
|
+
"id": "{{ORG_NAME}}",
|
|
8
|
+
"contact": "admin@{{ORG_DOMAIN}}"
|
|
9
|
+
},
|
|
10
|
+
"defaults": {
|
|
11
|
+
"profile": "base",
|
|
12
|
+
"cache_ttl_hours": 24
|
|
13
|
+
},
|
|
14
|
+
"profiles": {
|
|
15
|
+
"base": {
|
|
16
|
+
"description": "Default profile with no plugins"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://scc-cli.dev/schemas/org-v1.json",
|
|
3
|
+
"schema_version": "{{SCHEMA_VERSION}}",
|
|
4
|
+
"min_cli_version": "{{MIN_CLI_VERSION}}",
|
|
5
|
+
"organization": {
|
|
6
|
+
"name": "{{ORG_NAME}}",
|
|
7
|
+
"id": "{{ORG_NAME}}",
|
|
8
|
+
"contact": "admin@{{ORG_DOMAIN}}"
|
|
9
|
+
},
|
|
10
|
+
"marketplaces": [
|
|
11
|
+
{
|
|
12
|
+
"name": "github-public",
|
|
13
|
+
"type": "github",
|
|
14
|
+
"repo": "{{ORG_NAME}}/claude-plugins-marketplace",
|
|
15
|
+
"ref": "main",
|
|
16
|
+
"auth": null
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "github-private",
|
|
20
|
+
"type": "github",
|
|
21
|
+
"repo": "{{ORG_NAME}}/claude-plugins-private",
|
|
22
|
+
"ref": "v1.0.0",
|
|
23
|
+
"auth": "env:GITHUB_TOKEN"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "gitlab-self-hosted",
|
|
27
|
+
"type": "gitlab",
|
|
28
|
+
"host": "gitlab.{{ORG_DOMAIN}}",
|
|
29
|
+
"repo": "devtools/plugins",
|
|
30
|
+
"ref": "main",
|
|
31
|
+
"auth": "env:GITLAB_TOKEN"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "custom-https",
|
|
35
|
+
"type": "https",
|
|
36
|
+
"url": "https://plugins.{{ORG_DOMAIN}}/marketplace.json",
|
|
37
|
+
"auth": "env:CUSTOM_API_KEY"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"defaults": {
|
|
41
|
+
"profile": "base",
|
|
42
|
+
"cache_ttl_hours": 24
|
|
43
|
+
},
|
|
44
|
+
"profiles": {
|
|
45
|
+
"base": {
|
|
46
|
+
"description": "Default profile without plugins - safe baseline"
|
|
47
|
+
},
|
|
48
|
+
"platform": {
|
|
49
|
+
"description": "Platform engineering with infrastructure tools",
|
|
50
|
+
"plugin": "platform-tools",
|
|
51
|
+
"marketplace": "github-public"
|
|
52
|
+
},
|
|
53
|
+
"backend": {
|
|
54
|
+
"description": "Backend development with API and database tools",
|
|
55
|
+
"plugin": "backend-tools",
|
|
56
|
+
"marketplace": "github-public"
|
|
57
|
+
},
|
|
58
|
+
"frontend": {
|
|
59
|
+
"description": "Frontend development with UI component tools",
|
|
60
|
+
"plugin": "frontend-tools",
|
|
61
|
+
"marketplace": "github-public"
|
|
62
|
+
},
|
|
63
|
+
"security": {
|
|
64
|
+
"description": "Security team with audit and scanning tools",
|
|
65
|
+
"plugin": "security-tools",
|
|
66
|
+
"marketplace": "github-private"
|
|
67
|
+
},
|
|
68
|
+
"devops": {
|
|
69
|
+
"description": "DevOps with CI/CD and deployment tools",
|
|
70
|
+
"plugin": "devops-tools",
|
|
71
|
+
"marketplace": "gitlab-self-hosted"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://scc-cli.dev/schemas/org-v1.json",
|
|
3
|
+
"schema_version": "{{SCHEMA_VERSION}}",
|
|
4
|
+
"min_cli_version": "{{MIN_CLI_VERSION}}",
|
|
5
|
+
"organization": {
|
|
6
|
+
"name": "{{ORG_NAME}}",
|
|
7
|
+
"id": "{{ORG_NAME}}",
|
|
8
|
+
"contact": "security@{{ORG_DOMAIN}}"
|
|
9
|
+
},
|
|
10
|
+
"marketplaces": [
|
|
11
|
+
{
|
|
12
|
+
"name": "{{ORG_NAME}}-secure",
|
|
13
|
+
"type": "github",
|
|
14
|
+
"repo": "{{ORG_NAME}}/claude-plugins-secure",
|
|
15
|
+
"ref": "v1.0.0",
|
|
16
|
+
"auth": "env:GITHUB_TOKEN"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"defaults": {
|
|
20
|
+
"profile": "restricted",
|
|
21
|
+
"cache_ttl_hours": 1
|
|
22
|
+
},
|
|
23
|
+
"profiles": {
|
|
24
|
+
"restricted": {
|
|
25
|
+
"description": "Restricted access - security-audited plugins only"
|
|
26
|
+
},
|
|
27
|
+
"approved": {
|
|
28
|
+
"description": "Approved plugins for general development",
|
|
29
|
+
"plugin": "approved-base",
|
|
30
|
+
"marketplace": "{{ORG_NAME}}-secure"
|
|
31
|
+
},
|
|
32
|
+
"audit": {
|
|
33
|
+
"description": "Audit and compliance team",
|
|
34
|
+
"plugin": "audit-tools",
|
|
35
|
+
"marketplace": "{{ORG_NAME}}-secure"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://scc-cli.dev/schemas/org-v1.json",
|
|
3
|
+
"schema_version": "{{SCHEMA_VERSION}}",
|
|
4
|
+
"min_cli_version": "{{MIN_CLI_VERSION}}",
|
|
5
|
+
"organization": {
|
|
6
|
+
"name": "{{ORG_NAME}}",
|
|
7
|
+
"id": "{{ORG_NAME}}",
|
|
8
|
+
"contact": "admin@{{ORG_DOMAIN}}"
|
|
9
|
+
},
|
|
10
|
+
"marketplaces": [
|
|
11
|
+
{
|
|
12
|
+
"name": "{{ORG_NAME}}-plugins",
|
|
13
|
+
"type": "github",
|
|
14
|
+
"repo": "{{ORG_NAME}}/claude-plugins-marketplace",
|
|
15
|
+
"ref": "main"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"defaults": {
|
|
19
|
+
"profile": "base",
|
|
20
|
+
"cache_ttl_hours": 24
|
|
21
|
+
},
|
|
22
|
+
"profiles": {
|
|
23
|
+
"base": {
|
|
24
|
+
"description": "Default profile without team-specific plugins"
|
|
25
|
+
},
|
|
26
|
+
"platform": {
|
|
27
|
+
"description": "Platform engineering team",
|
|
28
|
+
"plugin": "platform-team",
|
|
29
|
+
"marketplace": "{{ORG_NAME}}-plugins"
|
|
30
|
+
},
|
|
31
|
+
"backend": {
|
|
32
|
+
"description": "Backend development team",
|
|
33
|
+
"plugin": "backend-team",
|
|
34
|
+
"marketplace": "{{ORG_NAME}}-plugins"
|
|
35
|
+
},
|
|
36
|
+
"frontend": {
|
|
37
|
+
"description": "Frontend development team",
|
|
38
|
+
"plugin": "frontend-team",
|
|
39
|
+
"marketplace": "{{ORG_NAME}}-plugins"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SCC Status Line for Claude Code
|
|
3
|
+
# Shows: Model | Git branch/worktree | Lines changed
|
|
4
|
+
#
|
|
5
|
+
# Install: scc statusline --install
|
|
6
|
+
# This script receives JSON from Claude Code via stdin
|
|
7
|
+
|
|
8
|
+
# Read JSON input from stdin
|
|
9
|
+
input=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract values using jq
|
|
12
|
+
MODEL=$(echo "$input" | jq -r '.model.display_name // "Unknown"')
|
|
13
|
+
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "."')
|
|
14
|
+
PROJECT_DIR=$(echo "$input" | jq -r '.workspace.project_dir // "."')
|
|
15
|
+
LINES_ADDED=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
|
|
16
|
+
LINES_REMOVED=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
|
|
17
|
+
|
|
18
|
+
# Colors
|
|
19
|
+
CYAN="\033[36m"
|
|
20
|
+
GREEN="\033[32m"
|
|
21
|
+
RED="\033[31m"
|
|
22
|
+
MAGENTA="\033[35m"
|
|
23
|
+
YELLOW="\033[33m"
|
|
24
|
+
WHITE="\033[1;37m"
|
|
25
|
+
DIM="\033[2m"
|
|
26
|
+
RESET="\033[0m"
|
|
27
|
+
|
|
28
|
+
# Git information
|
|
29
|
+
GIT_INFO=""
|
|
30
|
+
cd "$CURRENT_DIR" 2>/dev/null || cd "$PROJECT_DIR" 2>/dev/null
|
|
31
|
+
|
|
32
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
33
|
+
# Get branch name
|
|
34
|
+
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
35
|
+
if [ -z "$BRANCH" ]; then
|
|
36
|
+
# Detached HEAD - show short SHA
|
|
37
|
+
BRANCH=$(git rev-parse --short HEAD 2>/dev/null || echo "detached")
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Check if we're in a worktree
|
|
41
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
42
|
+
|
|
43
|
+
if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then
|
|
44
|
+
# In a worktree - show ⎇ icon
|
|
45
|
+
WORKTREE_NAME=$(basename "$(dirname "$GIT_DIR")" 2>/dev/null)
|
|
46
|
+
GIT_INFO="${MAGENTA}⎇ ${WORKTREE_NAME}${RESET}:${CYAN}${BRANCH}${RESET}"
|
|
47
|
+
else
|
|
48
|
+
# Regular repo - show 🌿 icon
|
|
49
|
+
GIT_INFO="${CYAN}🌿 ${BRANCH}${RESET}"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Check for uncommitted changes
|
|
53
|
+
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
|
|
54
|
+
GIT_INFO="${GIT_INFO}${YELLOW}*${RESET}"
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Lines changed (only show if any changes made)
|
|
59
|
+
LINES_INFO=""
|
|
60
|
+
if [ "$LINES_ADDED" -gt 0 ] || [ "$LINES_REMOVED" -gt 0 ]; then
|
|
61
|
+
LINES_INFO=" ${DIM}|${RESET} ${GREEN}+${LINES_ADDED}${RESET} ${RED}-${LINES_REMOVED}${RESET}"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Build the status line
|
|
65
|
+
# Format: [Model] 🌿 branch* | +156 -23
|
|
66
|
+
OUTPUT="${WHITE}[${MODEL}]${RESET}"
|
|
67
|
+
|
|
68
|
+
if [ -n "$GIT_INFO" ]; then
|
|
69
|
+
OUTPUT="${OUTPUT} ${GIT_INFO}"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
OUTPUT="${OUTPUT}${LINES_INFO}"
|
|
73
|
+
|
|
74
|
+
# Output (printf handles escape codes)
|
|
75
|
+
printf "%b\n" "$OUTPUT"
|