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
scc_cli/cli_org.py
ADDED
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provide CLI commands for organization administration.
|
|
3
|
+
|
|
4
|
+
Validate and inspect organization configurations including schema validation
|
|
5
|
+
and semantic checks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
import typer
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from .cli_common import console, handle_errors
|
|
17
|
+
from .config import load_user_config, save_user_config
|
|
18
|
+
from .constants import CLI_VERSION
|
|
19
|
+
from .exit_codes import EXIT_CONFIG, EXIT_VALIDATION
|
|
20
|
+
from .json_output import build_envelope
|
|
21
|
+
from .kinds import Kind
|
|
22
|
+
from .marketplace.team_fetch import fetch_team_config
|
|
23
|
+
from .org_templates import (
|
|
24
|
+
TemplateNotFoundError,
|
|
25
|
+
TemplateVars,
|
|
26
|
+
list_templates,
|
|
27
|
+
render_template_string,
|
|
28
|
+
)
|
|
29
|
+
from .output_mode import json_output_mode, print_json, set_pretty_mode
|
|
30
|
+
from .panels import create_error_panel, create_success_panel, create_warning_panel
|
|
31
|
+
from .remote import is_cache_valid, load_from_cache, load_org_config, save_to_cache
|
|
32
|
+
from .source_resolver import ResolveError, resolve_source
|
|
33
|
+
from .validate import check_version_compatibility, load_bundled_schema, validate_org_config
|
|
34
|
+
|
|
35
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
# Org App
|
|
37
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
org_app = typer.Typer(
|
|
40
|
+
name="org",
|
|
41
|
+
help="Organization configuration management and validation.",
|
|
42
|
+
no_args_is_help=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
# Pure Functions
|
|
48
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_validation_data(
|
|
52
|
+
source: str,
|
|
53
|
+
schema_errors: list[str],
|
|
54
|
+
semantic_errors: list[str],
|
|
55
|
+
schema_version: str,
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Build validation result data for JSON output.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
source: Path or URL of validated config
|
|
61
|
+
schema_errors: List of JSON schema validation errors
|
|
62
|
+
semantic_errors: List of semantic validation errors
|
|
63
|
+
schema_version: Schema version used for validation
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary with validation results
|
|
67
|
+
"""
|
|
68
|
+
is_valid = len(schema_errors) == 0 and len(semantic_errors) == 0
|
|
69
|
+
return {
|
|
70
|
+
"source": source,
|
|
71
|
+
"schema_version": schema_version,
|
|
72
|
+
"valid": is_valid,
|
|
73
|
+
"schema_errors": schema_errors,
|
|
74
|
+
"semantic_errors": semantic_errors,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_semantic_errors(config: dict[str, Any]) -> list[str]:
|
|
79
|
+
"""Check for semantic errors beyond JSON schema validation.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
config: Parsed organization config
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
List of semantic error messages
|
|
86
|
+
"""
|
|
87
|
+
errors: list[str] = []
|
|
88
|
+
org = config.get("organization", {})
|
|
89
|
+
profiles = org.get("profiles", [])
|
|
90
|
+
|
|
91
|
+
# Check for duplicate profile names
|
|
92
|
+
profile_names: list[str] = []
|
|
93
|
+
for profile in profiles:
|
|
94
|
+
name = profile.get("name", "")
|
|
95
|
+
if name in profile_names:
|
|
96
|
+
errors.append(f"Duplicate profile name: '{name}'")
|
|
97
|
+
else:
|
|
98
|
+
profile_names.append(name)
|
|
99
|
+
|
|
100
|
+
# Check if default_profile references existing profile
|
|
101
|
+
default_profile = org.get("default_profile")
|
|
102
|
+
if default_profile and default_profile not in profile_names:
|
|
103
|
+
errors.append(f"default_profile '{default_profile}' references non-existent profile")
|
|
104
|
+
|
|
105
|
+
return errors
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_import_preview_data(
|
|
109
|
+
source: str,
|
|
110
|
+
resolved_url: str,
|
|
111
|
+
config: dict[str, Any],
|
|
112
|
+
validation_errors: list[str],
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Build import preview data for display and JSON output.
|
|
115
|
+
|
|
116
|
+
Pure function that assembles preview information for an organization config
|
|
117
|
+
before it is imported.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
source: Original source string (URL or shorthand like github:org/repo)
|
|
121
|
+
resolved_url: Resolved URL after shorthand expansion
|
|
122
|
+
config: Parsed organization config dict
|
|
123
|
+
validation_errors: List of validation error messages
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dictionary with preview information including org details and validation status
|
|
127
|
+
"""
|
|
128
|
+
org_data = config.get("organization", {})
|
|
129
|
+
profiles_dict = config.get("profiles", {})
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"source": source,
|
|
133
|
+
"resolved_url": resolved_url,
|
|
134
|
+
"organization": {
|
|
135
|
+
"name": org_data.get("name", ""),
|
|
136
|
+
"id": org_data.get("id", ""),
|
|
137
|
+
"contact": org_data.get("contact", ""),
|
|
138
|
+
},
|
|
139
|
+
"valid": len(validation_errors) == 0,
|
|
140
|
+
"validation_errors": validation_errors,
|
|
141
|
+
"available_profiles": list(profiles_dict.keys()),
|
|
142
|
+
"schema_version": config.get("schema_version", ""),
|
|
143
|
+
"min_cli_version": config.get("min_cli_version", ""),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_status_data(
|
|
148
|
+
user_config: dict[str, Any],
|
|
149
|
+
org_config: dict[str, Any] | None,
|
|
150
|
+
cache_meta: dict[str, Any] | None,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Build status data for JSON output and display.
|
|
153
|
+
|
|
154
|
+
Pure function that assembles status information from various sources.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
user_config: User configuration dict
|
|
158
|
+
org_config: Cached organization config (may be None)
|
|
159
|
+
cache_meta: Cache metadata dict (may be None)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with complete status information
|
|
163
|
+
"""
|
|
164
|
+
# Determine mode
|
|
165
|
+
is_standalone = user_config.get("standalone", False) or not user_config.get(
|
|
166
|
+
"organization_source"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if is_standalone:
|
|
170
|
+
return {
|
|
171
|
+
"mode": "standalone",
|
|
172
|
+
"organization": None,
|
|
173
|
+
"cache": None,
|
|
174
|
+
"version_compatibility": None,
|
|
175
|
+
"selected_profile": None,
|
|
176
|
+
"available_profiles": [],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Organization connected mode
|
|
180
|
+
org_source = user_config.get("organization_source", {})
|
|
181
|
+
source_url = org_source.get("url", "")
|
|
182
|
+
|
|
183
|
+
# Organization info
|
|
184
|
+
org_info: dict[str, Any] | None = None
|
|
185
|
+
available_profiles: list[str] = []
|
|
186
|
+
if org_config:
|
|
187
|
+
org_data = org_config.get("organization", {})
|
|
188
|
+
org_info = {
|
|
189
|
+
"name": org_data.get("name", "unknown"),
|
|
190
|
+
"id": org_data.get("id", ""),
|
|
191
|
+
"contact": org_data.get("contact", ""),
|
|
192
|
+
"source_url": source_url,
|
|
193
|
+
}
|
|
194
|
+
# Extract available profiles
|
|
195
|
+
profiles_dict = org_config.get("profiles", {})
|
|
196
|
+
available_profiles = list(profiles_dict.keys())
|
|
197
|
+
else:
|
|
198
|
+
org_info = {
|
|
199
|
+
"name": None,
|
|
200
|
+
"source_url": source_url,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Cache status
|
|
204
|
+
cache_info: dict[str, Any] | None = None
|
|
205
|
+
if cache_meta:
|
|
206
|
+
org_cache = cache_meta.get("org_config", {})
|
|
207
|
+
cache_info = {
|
|
208
|
+
"fetched_at": org_cache.get("fetched_at"),
|
|
209
|
+
"expires_at": org_cache.get("expires_at"),
|
|
210
|
+
"etag": org_cache.get("etag"),
|
|
211
|
+
"valid": is_cache_valid(cache_meta),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Version compatibility
|
|
215
|
+
version_compat: dict[str, Any] | None = None
|
|
216
|
+
if org_config:
|
|
217
|
+
compat = check_version_compatibility(org_config)
|
|
218
|
+
version_compat = {
|
|
219
|
+
"compatible": compat.compatible,
|
|
220
|
+
"blocking_error": compat.blocking_error,
|
|
221
|
+
"warnings": compat.warnings,
|
|
222
|
+
"schema_version": compat.schema_version,
|
|
223
|
+
"min_cli_version": compat.min_cli_version,
|
|
224
|
+
"current_cli_version": compat.current_cli_version,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"mode": "organization",
|
|
229
|
+
"organization": org_info,
|
|
230
|
+
"cache": cache_info,
|
|
231
|
+
"version_compatibility": version_compat,
|
|
232
|
+
"selected_profile": user_config.get("selected_profile"),
|
|
233
|
+
"available_profiles": available_profiles,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_update_data(
|
|
238
|
+
org_config: dict[str, Any] | None,
|
|
239
|
+
team_results: list[dict[str, Any]] | None = None,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Build update result data for JSON output.
|
|
242
|
+
|
|
243
|
+
Pure function that assembles update result information.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
org_config: Updated organization config (may be None on failure)
|
|
247
|
+
team_results: List of team update results (optional)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dictionary with update results including org and team info
|
|
251
|
+
"""
|
|
252
|
+
result: dict[str, Any] = {
|
|
253
|
+
"org_updated": org_config is not None,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if org_config:
|
|
257
|
+
org_data = org_config.get("organization", {})
|
|
258
|
+
result["organization"] = {
|
|
259
|
+
"name": org_data.get("name", ""),
|
|
260
|
+
"id": org_data.get("id", ""),
|
|
261
|
+
}
|
|
262
|
+
result["schema_version"] = org_config.get("schema_version", "")
|
|
263
|
+
|
|
264
|
+
if team_results is not None:
|
|
265
|
+
result["teams_updated"] = team_results
|
|
266
|
+
result["teams_success_count"] = sum(1 for t in team_results if t.get("success"))
|
|
267
|
+
result["teams_failed_count"] = sum(1 for t in team_results if not t.get("success"))
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _parse_config_source(source_dict: dict[str, Any]) -> Any:
|
|
273
|
+
"""Parse a config_source dict into the appropriate ConfigSource type.
|
|
274
|
+
|
|
275
|
+
Handles discriminated union parsing for github, git, url sources.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
source_dict: Raw config_source dict from org config
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
ConfigSource object (ConfigSourceGitHub, ConfigSourceGit, or ConfigSourceURL)
|
|
282
|
+
"""
|
|
283
|
+
# Import here to avoid circular imports
|
|
284
|
+
from .marketplace.schema import (
|
|
285
|
+
ConfigSourceGit,
|
|
286
|
+
ConfigSourceGitHub,
|
|
287
|
+
ConfigSourceURL,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if "github" in source_dict:
|
|
291
|
+
github_data = source_dict["github"]
|
|
292
|
+
# Add source discriminator for Pydantic model
|
|
293
|
+
return ConfigSourceGitHub(source="github", **github_data)
|
|
294
|
+
elif "git" in source_dict:
|
|
295
|
+
git_data = source_dict["git"]
|
|
296
|
+
return ConfigSourceGit(source="git", **git_data)
|
|
297
|
+
elif "url" in source_dict:
|
|
298
|
+
url_data = source_dict["url"]
|
|
299
|
+
return ConfigSourceURL(source="url", **url_data)
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(f"Unknown config_source type: {list(source_dict.keys())}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
# Org Commands
|
|
306
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@org_app.command("validate")
|
|
310
|
+
@handle_errors
|
|
311
|
+
def org_validate_cmd(
|
|
312
|
+
source: str = typer.Argument(..., help="Path to config file to validate"),
|
|
313
|
+
schema_version: str = typer.Option(
|
|
314
|
+
"v1", "--schema-version", "-s", help="Schema version (default: v1)"
|
|
315
|
+
),
|
|
316
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
317
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
318
|
+
) -> None:
|
|
319
|
+
"""Validate an organization configuration file.
|
|
320
|
+
|
|
321
|
+
Performs both JSON schema validation and semantic checks.
|
|
322
|
+
|
|
323
|
+
Examples:
|
|
324
|
+
scc org validate ./org-config.json
|
|
325
|
+
scc org validate ./org-config.json --json
|
|
326
|
+
"""
|
|
327
|
+
# --pretty implies --json
|
|
328
|
+
if pretty:
|
|
329
|
+
json_output = True
|
|
330
|
+
set_pretty_mode(True)
|
|
331
|
+
|
|
332
|
+
# Load config file
|
|
333
|
+
config_path = Path(source).expanduser().resolve()
|
|
334
|
+
if not config_path.exists():
|
|
335
|
+
if json_output:
|
|
336
|
+
with json_output_mode():
|
|
337
|
+
data = build_validation_data(
|
|
338
|
+
source=source,
|
|
339
|
+
schema_errors=[f"File not found: {source}"],
|
|
340
|
+
semantic_errors=[],
|
|
341
|
+
schema_version=schema_version,
|
|
342
|
+
)
|
|
343
|
+
envelope = build_envelope(Kind.ORG_VALIDATION, data=data, ok=False)
|
|
344
|
+
print_json(envelope)
|
|
345
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
346
|
+
console.print(create_error_panel("File Not Found", f"Cannot find config file: {source}"))
|
|
347
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
348
|
+
|
|
349
|
+
# Parse JSON
|
|
350
|
+
try:
|
|
351
|
+
config = json.loads(config_path.read_text())
|
|
352
|
+
except json.JSONDecodeError as e:
|
|
353
|
+
if json_output:
|
|
354
|
+
with json_output_mode():
|
|
355
|
+
data = build_validation_data(
|
|
356
|
+
source=source,
|
|
357
|
+
schema_errors=[f"Invalid JSON: {e}"],
|
|
358
|
+
semantic_errors=[],
|
|
359
|
+
schema_version=schema_version,
|
|
360
|
+
)
|
|
361
|
+
envelope = build_envelope(Kind.ORG_VALIDATION, data=data, ok=False)
|
|
362
|
+
print_json(envelope)
|
|
363
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
364
|
+
console.print(create_error_panel("Invalid JSON", f"Failed to parse JSON: {e}"))
|
|
365
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
366
|
+
|
|
367
|
+
# Validate against schema
|
|
368
|
+
schema_errors = validate_org_config(config, schema_version)
|
|
369
|
+
|
|
370
|
+
# Check semantic errors (only if schema is valid)
|
|
371
|
+
semantic_errors: list[str] = []
|
|
372
|
+
if not schema_errors:
|
|
373
|
+
semantic_errors = check_semantic_errors(config)
|
|
374
|
+
|
|
375
|
+
# Build result data
|
|
376
|
+
data = build_validation_data(
|
|
377
|
+
source=source,
|
|
378
|
+
schema_errors=schema_errors,
|
|
379
|
+
semantic_errors=semantic_errors,
|
|
380
|
+
schema_version=schema_version,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# JSON output mode
|
|
384
|
+
if json_output:
|
|
385
|
+
with json_output_mode():
|
|
386
|
+
is_valid = data["valid"]
|
|
387
|
+
all_errors = schema_errors + semantic_errors
|
|
388
|
+
envelope = build_envelope(
|
|
389
|
+
Kind.ORG_VALIDATION,
|
|
390
|
+
data=data,
|
|
391
|
+
ok=is_valid,
|
|
392
|
+
errors=all_errors if not is_valid else None,
|
|
393
|
+
)
|
|
394
|
+
print_json(envelope)
|
|
395
|
+
raise typer.Exit(0 if is_valid else EXIT_VALIDATION)
|
|
396
|
+
|
|
397
|
+
# Human-readable output
|
|
398
|
+
if data["valid"]:
|
|
399
|
+
console.print(
|
|
400
|
+
create_success_panel(
|
|
401
|
+
"Validation Passed",
|
|
402
|
+
{
|
|
403
|
+
"Source": source,
|
|
404
|
+
"Schema Version": schema_version,
|
|
405
|
+
"Status": "Valid",
|
|
406
|
+
},
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
raise typer.Exit(0)
|
|
410
|
+
|
|
411
|
+
# Show errors
|
|
412
|
+
if schema_errors:
|
|
413
|
+
console.print(
|
|
414
|
+
create_error_panel(
|
|
415
|
+
"Schema Validation Failed",
|
|
416
|
+
"\n".join(f"• {e}" for e in schema_errors),
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
if semantic_errors:
|
|
421
|
+
console.print(
|
|
422
|
+
create_warning_panel(
|
|
423
|
+
"Semantic Issues",
|
|
424
|
+
"\n".join(f"• {e}" for e in semantic_errors),
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
raise typer.Exit(EXIT_VALIDATION)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@org_app.command("update")
|
|
432
|
+
@handle_errors
|
|
433
|
+
def org_update_cmd(
|
|
434
|
+
team: str | None = typer.Option(
|
|
435
|
+
None, "--team", "-t", help="Refresh a specific federated team's config"
|
|
436
|
+
),
|
|
437
|
+
all_teams: bool = typer.Option(
|
|
438
|
+
False, "--all-teams", "-a", help="Refresh all federated team configs"
|
|
439
|
+
),
|
|
440
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
441
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
442
|
+
) -> None:
|
|
443
|
+
"""Refresh organization config and optionally team configs.
|
|
444
|
+
|
|
445
|
+
By default, refreshes the organization config from its remote source.
|
|
446
|
+
With --team or --all-teams, also refreshes federated team configurations.
|
|
447
|
+
|
|
448
|
+
Examples:
|
|
449
|
+
scc org update # Refresh org config only
|
|
450
|
+
scc org update --team dev # Also refresh 'dev' team config
|
|
451
|
+
scc org update --all-teams # Refresh all federated team configs
|
|
452
|
+
"""
|
|
453
|
+
# --pretty implies --json
|
|
454
|
+
if pretty:
|
|
455
|
+
json_output = True
|
|
456
|
+
set_pretty_mode(True)
|
|
457
|
+
|
|
458
|
+
# Load user config
|
|
459
|
+
user_config = load_user_config()
|
|
460
|
+
|
|
461
|
+
# Check for standalone mode
|
|
462
|
+
is_standalone = user_config.get("standalone", False)
|
|
463
|
+
if is_standalone:
|
|
464
|
+
if json_output:
|
|
465
|
+
with json_output_mode():
|
|
466
|
+
envelope = build_envelope(
|
|
467
|
+
Kind.ORG_UPDATE,
|
|
468
|
+
data={"error": "Cannot update in standalone mode"},
|
|
469
|
+
ok=False,
|
|
470
|
+
errors=["CLI is running in standalone mode"],
|
|
471
|
+
)
|
|
472
|
+
print_json(envelope)
|
|
473
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
474
|
+
console.print(
|
|
475
|
+
create_error_panel(
|
|
476
|
+
"Standalone Mode",
|
|
477
|
+
"Cannot update organization config in standalone mode.",
|
|
478
|
+
hint="Use 'scc setup' to connect to an organization.",
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
482
|
+
|
|
483
|
+
# Check for organization source
|
|
484
|
+
org_source = user_config.get("organization_source")
|
|
485
|
+
if not org_source:
|
|
486
|
+
if json_output:
|
|
487
|
+
with json_output_mode():
|
|
488
|
+
envelope = build_envelope(
|
|
489
|
+
Kind.ORG_UPDATE,
|
|
490
|
+
data={"error": "No organization source configured"},
|
|
491
|
+
ok=False,
|
|
492
|
+
errors=["No organization source configured"],
|
|
493
|
+
)
|
|
494
|
+
print_json(envelope)
|
|
495
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
496
|
+
console.print(
|
|
497
|
+
create_error_panel(
|
|
498
|
+
"No Organization",
|
|
499
|
+
"No organization source is configured.",
|
|
500
|
+
hint="Use 'scc setup' to connect to an organization.",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
504
|
+
|
|
505
|
+
# Force refresh org config
|
|
506
|
+
org_config = load_org_config(user_config, force_refresh=True)
|
|
507
|
+
if org_config is None:
|
|
508
|
+
if json_output:
|
|
509
|
+
with json_output_mode():
|
|
510
|
+
envelope = build_envelope(
|
|
511
|
+
Kind.ORG_UPDATE,
|
|
512
|
+
data=build_update_data(None),
|
|
513
|
+
ok=False,
|
|
514
|
+
errors=["Failed to fetch organization config"],
|
|
515
|
+
)
|
|
516
|
+
print_json(envelope)
|
|
517
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
518
|
+
console.print(
|
|
519
|
+
create_error_panel(
|
|
520
|
+
"Update Failed",
|
|
521
|
+
"Failed to fetch organization config from remote.",
|
|
522
|
+
hint="Check network connection and organization URL.",
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
526
|
+
|
|
527
|
+
# Get profiles from org config
|
|
528
|
+
profiles = org_config.get("profiles", {})
|
|
529
|
+
|
|
530
|
+
# Handle --team option (single team update)
|
|
531
|
+
team_results: list[dict[str, Any]] | None = None
|
|
532
|
+
if team is not None:
|
|
533
|
+
# Validate team exists
|
|
534
|
+
if team not in profiles:
|
|
535
|
+
if json_output:
|
|
536
|
+
with json_output_mode():
|
|
537
|
+
envelope = build_envelope(
|
|
538
|
+
Kind.ORG_UPDATE,
|
|
539
|
+
data=build_update_data(org_config),
|
|
540
|
+
ok=False,
|
|
541
|
+
errors=[f"Team '{team}' not found in organization config"],
|
|
542
|
+
)
|
|
543
|
+
print_json(envelope)
|
|
544
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
545
|
+
console.print(
|
|
546
|
+
create_error_panel(
|
|
547
|
+
"Team Not Found",
|
|
548
|
+
f"Team '{team}' not found in organization config.",
|
|
549
|
+
hint=f"Available teams: {', '.join(profiles.keys())}",
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
553
|
+
|
|
554
|
+
profile = profiles[team]
|
|
555
|
+
config_source_dict = profile.get("config_source")
|
|
556
|
+
|
|
557
|
+
# Check if team is federated
|
|
558
|
+
if config_source_dict is None:
|
|
559
|
+
team_results = [{"team": team, "success": True, "inline": True}]
|
|
560
|
+
if json_output:
|
|
561
|
+
with json_output_mode():
|
|
562
|
+
data = build_update_data(org_config, team_results)
|
|
563
|
+
envelope = build_envelope(Kind.ORG_UPDATE, data=data)
|
|
564
|
+
print_json(envelope)
|
|
565
|
+
raise typer.Exit(0)
|
|
566
|
+
console.print(
|
|
567
|
+
create_warning_panel(
|
|
568
|
+
"Inline Team",
|
|
569
|
+
f"Team '{team}' is not federated (inline config).",
|
|
570
|
+
hint="Inline teams don't have external configs to refresh.",
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
raise typer.Exit(0)
|
|
574
|
+
|
|
575
|
+
# Fetch team config
|
|
576
|
+
try:
|
|
577
|
+
config_source = _parse_config_source(config_source_dict)
|
|
578
|
+
result = fetch_team_config(config_source, team)
|
|
579
|
+
if result.success:
|
|
580
|
+
team_results = [
|
|
581
|
+
{
|
|
582
|
+
"team": team,
|
|
583
|
+
"success": True,
|
|
584
|
+
"commit_sha": result.commit_sha,
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
else:
|
|
588
|
+
team_results = [
|
|
589
|
+
{
|
|
590
|
+
"team": team,
|
|
591
|
+
"success": False,
|
|
592
|
+
"error": result.error,
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
if json_output:
|
|
596
|
+
with json_output_mode():
|
|
597
|
+
data = build_update_data(org_config, team_results)
|
|
598
|
+
envelope = build_envelope(
|
|
599
|
+
Kind.ORG_UPDATE,
|
|
600
|
+
data=data,
|
|
601
|
+
ok=False,
|
|
602
|
+
errors=[f"Failed to fetch team config: {result.error}"],
|
|
603
|
+
)
|
|
604
|
+
print_json(envelope)
|
|
605
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
606
|
+
console.print(
|
|
607
|
+
create_error_panel(
|
|
608
|
+
"Team Update Failed",
|
|
609
|
+
f"Failed to fetch config for team '{team}'.",
|
|
610
|
+
hint=str(result.error),
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
614
|
+
except Exception as e:
|
|
615
|
+
if json_output:
|
|
616
|
+
with json_output_mode():
|
|
617
|
+
envelope = build_envelope(
|
|
618
|
+
Kind.ORG_UPDATE,
|
|
619
|
+
data=build_update_data(org_config),
|
|
620
|
+
ok=False,
|
|
621
|
+
errors=[f"Error parsing config source: {e}"],
|
|
622
|
+
)
|
|
623
|
+
print_json(envelope)
|
|
624
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
625
|
+
console.print(create_error_panel("Config Error", f"Error parsing config source: {e}"))
|
|
626
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
627
|
+
|
|
628
|
+
# Handle --all-teams option
|
|
629
|
+
elif all_teams:
|
|
630
|
+
team_results = []
|
|
631
|
+
federated_teams = [
|
|
632
|
+
(name, profile)
|
|
633
|
+
for name, profile in profiles.items()
|
|
634
|
+
if profile.get("config_source") is not None
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
if not federated_teams:
|
|
638
|
+
team_results = []
|
|
639
|
+
if json_output:
|
|
640
|
+
with json_output_mode():
|
|
641
|
+
data = build_update_data(org_config, team_results)
|
|
642
|
+
envelope = build_envelope(Kind.ORG_UPDATE, data=data)
|
|
643
|
+
print_json(envelope)
|
|
644
|
+
raise typer.Exit(0)
|
|
645
|
+
console.print(
|
|
646
|
+
create_warning_panel(
|
|
647
|
+
"No Federated Teams",
|
|
648
|
+
"No federated teams found in organization config.",
|
|
649
|
+
hint="All teams use inline configuration.",
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
raise typer.Exit(0)
|
|
653
|
+
|
|
654
|
+
# Fetch all federated team configs
|
|
655
|
+
for team_name, profile in federated_teams:
|
|
656
|
+
config_source_dict = profile["config_source"]
|
|
657
|
+
try:
|
|
658
|
+
config_source = _parse_config_source(config_source_dict)
|
|
659
|
+
result = fetch_team_config(config_source, team_name)
|
|
660
|
+
if result.success:
|
|
661
|
+
team_results.append(
|
|
662
|
+
{
|
|
663
|
+
"team": team_name,
|
|
664
|
+
"success": True,
|
|
665
|
+
"commit_sha": result.commit_sha,
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
else:
|
|
669
|
+
team_results.append(
|
|
670
|
+
{
|
|
671
|
+
"team": team_name,
|
|
672
|
+
"success": False,
|
|
673
|
+
"error": result.error,
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
team_results.append(
|
|
678
|
+
{
|
|
679
|
+
"team": team_name,
|
|
680
|
+
"success": False,
|
|
681
|
+
"error": str(e),
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Build output data
|
|
686
|
+
data = build_update_data(org_config, team_results)
|
|
687
|
+
|
|
688
|
+
# JSON output
|
|
689
|
+
if json_output:
|
|
690
|
+
with json_output_mode():
|
|
691
|
+
# Determine overall success
|
|
692
|
+
has_team_failures = team_results is not None and any(
|
|
693
|
+
not t.get("success") for t in team_results
|
|
694
|
+
)
|
|
695
|
+
envelope = build_envelope(
|
|
696
|
+
Kind.ORG_UPDATE,
|
|
697
|
+
data=data,
|
|
698
|
+
ok=not has_team_failures,
|
|
699
|
+
)
|
|
700
|
+
print_json(envelope)
|
|
701
|
+
raise typer.Exit(0)
|
|
702
|
+
|
|
703
|
+
# Human-readable output
|
|
704
|
+
org_data = org_config.get("organization", {})
|
|
705
|
+
org_name = org_data.get("name", "Unknown")
|
|
706
|
+
|
|
707
|
+
if team_results is None:
|
|
708
|
+
# Org-only update
|
|
709
|
+
console.print(
|
|
710
|
+
create_success_panel(
|
|
711
|
+
"Organization Updated",
|
|
712
|
+
{
|
|
713
|
+
"Organization": org_name,
|
|
714
|
+
"Status": "Refreshed from remote",
|
|
715
|
+
},
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
else:
|
|
719
|
+
# Team updates included
|
|
720
|
+
success_count = sum(1 for t in team_results if t.get("success"))
|
|
721
|
+
failed_count = len(team_results) - success_count
|
|
722
|
+
|
|
723
|
+
if failed_count == 0:
|
|
724
|
+
console.print(
|
|
725
|
+
create_success_panel(
|
|
726
|
+
"Update Complete",
|
|
727
|
+
{
|
|
728
|
+
"Organization": org_name,
|
|
729
|
+
"Teams Updated": str(success_count),
|
|
730
|
+
},
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
else:
|
|
734
|
+
console.print(
|
|
735
|
+
create_warning_panel(
|
|
736
|
+
"Partial Update",
|
|
737
|
+
f"Organization updated. {success_count} team(s) succeeded, {failed_count} failed.",
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
raise typer.Exit(0)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@org_app.command("schema")
|
|
745
|
+
@handle_errors
|
|
746
|
+
def org_schema_cmd(
|
|
747
|
+
schema_version: str = typer.Option(
|
|
748
|
+
"v1", "--version", "-v", help="Schema version to print (default: v1)"
|
|
749
|
+
),
|
|
750
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
751
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Print the bundled organization config schema.
|
|
754
|
+
|
|
755
|
+
Useful for understanding the expected configuration format
|
|
756
|
+
or for use with external validators.
|
|
757
|
+
|
|
758
|
+
Examples:
|
|
759
|
+
scc org schema
|
|
760
|
+
scc org schema --json
|
|
761
|
+
"""
|
|
762
|
+
# --pretty implies --json
|
|
763
|
+
if pretty:
|
|
764
|
+
json_output = True
|
|
765
|
+
set_pretty_mode(True)
|
|
766
|
+
|
|
767
|
+
# Load schema
|
|
768
|
+
try:
|
|
769
|
+
schema = load_bundled_schema(schema_version)
|
|
770
|
+
except FileNotFoundError:
|
|
771
|
+
if json_output:
|
|
772
|
+
with json_output_mode():
|
|
773
|
+
envelope = build_envelope(
|
|
774
|
+
Kind.ORG_SCHEMA,
|
|
775
|
+
data={"error": f"Schema version '{schema_version}' not found"},
|
|
776
|
+
ok=False,
|
|
777
|
+
errors=[f"Schema version '{schema_version}' not found"],
|
|
778
|
+
)
|
|
779
|
+
print_json(envelope)
|
|
780
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
781
|
+
console.print(
|
|
782
|
+
create_error_panel(
|
|
783
|
+
"Schema Not Found",
|
|
784
|
+
f"Schema version '{schema_version}' does not exist.",
|
|
785
|
+
"Available version: v1",
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
789
|
+
|
|
790
|
+
# JSON envelope output
|
|
791
|
+
if json_output:
|
|
792
|
+
with json_output_mode():
|
|
793
|
+
data = {
|
|
794
|
+
"schema_version": schema_version,
|
|
795
|
+
"schema": schema,
|
|
796
|
+
}
|
|
797
|
+
envelope = build_envelope(Kind.ORG_SCHEMA, data=data)
|
|
798
|
+
print_json(envelope)
|
|
799
|
+
raise typer.Exit(0)
|
|
800
|
+
|
|
801
|
+
# Raw schema output (for piping to files or validators)
|
|
802
|
+
print(json.dumps(schema, indent=2))
|
|
803
|
+
raise typer.Exit(0)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@org_app.command("status")
|
|
807
|
+
@handle_errors
|
|
808
|
+
def org_status_cmd(
|
|
809
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
810
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
811
|
+
) -> None:
|
|
812
|
+
"""Show current organization configuration status.
|
|
813
|
+
|
|
814
|
+
Displays connection mode (standalone or organization), cache freshness,
|
|
815
|
+
version compatibility, and selected profile.
|
|
816
|
+
|
|
817
|
+
Examples:
|
|
818
|
+
scc org status
|
|
819
|
+
scc org status --json
|
|
820
|
+
scc org status --pretty
|
|
821
|
+
"""
|
|
822
|
+
# --pretty implies --json
|
|
823
|
+
if pretty:
|
|
824
|
+
json_output = True
|
|
825
|
+
set_pretty_mode(True)
|
|
826
|
+
|
|
827
|
+
# Load configuration data
|
|
828
|
+
user_config = load_user_config()
|
|
829
|
+
org_config, cache_meta = load_from_cache()
|
|
830
|
+
|
|
831
|
+
# Build status data
|
|
832
|
+
status_data = build_status_data(user_config, org_config, cache_meta)
|
|
833
|
+
|
|
834
|
+
# JSON output mode
|
|
835
|
+
if json_output:
|
|
836
|
+
with json_output_mode():
|
|
837
|
+
envelope = build_envelope(Kind.ORG_STATUS, data=status_data)
|
|
838
|
+
print_json(envelope)
|
|
839
|
+
raise typer.Exit(0)
|
|
840
|
+
|
|
841
|
+
# Human-readable output
|
|
842
|
+
_render_status_human(status_data)
|
|
843
|
+
raise typer.Exit(0)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _render_status_human(status: dict[str, Any]) -> None:
|
|
847
|
+
"""Render status data as human-readable Rich output.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
status: Status data from build_status_data
|
|
851
|
+
"""
|
|
852
|
+
# Mode header
|
|
853
|
+
mode = status["mode"]
|
|
854
|
+
if mode == "standalone":
|
|
855
|
+
console.print("\n[bold cyan]Organization Status[/bold cyan]")
|
|
856
|
+
console.print(" Mode: [yellow]Standalone[/yellow] (no organization configured)")
|
|
857
|
+
console.print("\n [dim]Tip: Run 'scc setup' to connect to an organization[/dim]\n")
|
|
858
|
+
return
|
|
859
|
+
|
|
860
|
+
# Organization mode
|
|
861
|
+
console.print("\n[bold cyan]Organization Status[/bold cyan]")
|
|
862
|
+
|
|
863
|
+
# Create a table for organization info
|
|
864
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
865
|
+
table.add_column("Key", style="dim")
|
|
866
|
+
table.add_column("Value")
|
|
867
|
+
|
|
868
|
+
# Organization info
|
|
869
|
+
org = status.get("organization", {})
|
|
870
|
+
if org:
|
|
871
|
+
org_name = org.get("name") or "[not fetched]"
|
|
872
|
+
table.add_row("Organization", f"[bold]{org_name}[/bold]")
|
|
873
|
+
table.add_row("Source URL", org.get("source_url", "[not configured]"))
|
|
874
|
+
|
|
875
|
+
# Selected profile
|
|
876
|
+
profile = status.get("selected_profile")
|
|
877
|
+
if profile:
|
|
878
|
+
table.add_row("Selected Profile", f"[green]{profile}[/green]")
|
|
879
|
+
else:
|
|
880
|
+
table.add_row("Selected Profile", "[yellow]None[/yellow]")
|
|
881
|
+
|
|
882
|
+
# Available profiles
|
|
883
|
+
available = status.get("available_profiles", [])
|
|
884
|
+
if available:
|
|
885
|
+
table.add_row("Available Profiles", ", ".join(available))
|
|
886
|
+
|
|
887
|
+
console.print(table)
|
|
888
|
+
|
|
889
|
+
# Cache status
|
|
890
|
+
cache = status.get("cache")
|
|
891
|
+
if cache:
|
|
892
|
+
console.print("\n[bold]Cache Status[/bold]")
|
|
893
|
+
cache_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
894
|
+
cache_table.add_column("Key", style="dim")
|
|
895
|
+
cache_table.add_column("Value")
|
|
896
|
+
|
|
897
|
+
if cache.get("valid"):
|
|
898
|
+
cache_table.add_row("Status", "[green]✓ Fresh[/green]")
|
|
899
|
+
else:
|
|
900
|
+
cache_table.add_row("Status", "[yellow]⚠ Expired[/yellow]")
|
|
901
|
+
|
|
902
|
+
if cache.get("fetched_at"):
|
|
903
|
+
cache_table.add_row("Fetched At", cache["fetched_at"])
|
|
904
|
+
if cache.get("expires_at"):
|
|
905
|
+
cache_table.add_row("Expires At", cache["expires_at"])
|
|
906
|
+
|
|
907
|
+
console.print(cache_table)
|
|
908
|
+
else:
|
|
909
|
+
console.print("\n[yellow]Cache:[/yellow] Not fetched yet")
|
|
910
|
+
console.print(
|
|
911
|
+
" [dim]Run 'scc start' or 'scc doctor' to fetch the organization config[/dim]"
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
# Version compatibility
|
|
915
|
+
compat = status.get("version_compatibility")
|
|
916
|
+
if compat:
|
|
917
|
+
console.print("\n[bold]Version Compatibility[/bold]")
|
|
918
|
+
compat_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
919
|
+
compat_table.add_column("Key", style="dim")
|
|
920
|
+
compat_table.add_column("Value")
|
|
921
|
+
|
|
922
|
+
if compat.get("compatible"):
|
|
923
|
+
compat_table.add_row("Status", "[green]✓ Compatible[/green]")
|
|
924
|
+
else:
|
|
925
|
+
if compat.get("blocking_error"):
|
|
926
|
+
compat_table.add_row("Status", "[red]✗ Incompatible[/red]")
|
|
927
|
+
compat_table.add_row("Error", f"[red]{compat['blocking_error']}[/red]")
|
|
928
|
+
else:
|
|
929
|
+
compat_table.add_row("Status", "[yellow]⚠ Warnings[/yellow]")
|
|
930
|
+
|
|
931
|
+
if compat.get("schema_version"):
|
|
932
|
+
compat_table.add_row("Schema Version", compat["schema_version"])
|
|
933
|
+
if compat.get("min_cli_version"):
|
|
934
|
+
compat_table.add_row("Min CLI Version", compat["min_cli_version"])
|
|
935
|
+
compat_table.add_row("Current CLI", compat.get("current_cli_version", CLI_VERSION))
|
|
936
|
+
|
|
937
|
+
# Show warnings if any
|
|
938
|
+
warnings = compat.get("warnings", [])
|
|
939
|
+
for warning in warnings:
|
|
940
|
+
console.print(f" [yellow]⚠ {warning}[/yellow]")
|
|
941
|
+
|
|
942
|
+
console.print(compat_table)
|
|
943
|
+
|
|
944
|
+
console.print() # Final newline
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
@org_app.command("import")
|
|
948
|
+
@handle_errors
|
|
949
|
+
def org_import_cmd(
|
|
950
|
+
source: str = typer.Argument(..., help="URL or shorthand (e.g., github:org/repo)"),
|
|
951
|
+
preview: bool = typer.Option(False, "--preview", "-p", help="Preview import without saving"),
|
|
952
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
|
|
953
|
+
pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
|
|
954
|
+
) -> None:
|
|
955
|
+
"""Import an organization configuration from a URL.
|
|
956
|
+
|
|
957
|
+
Supports direct URLs and shorthands like github:org/repo.
|
|
958
|
+
Use --preview to validate without saving.
|
|
959
|
+
|
|
960
|
+
Examples:
|
|
961
|
+
scc org import https://example.com/org-config.json
|
|
962
|
+
scc org import github:acme/configs
|
|
963
|
+
scc org import github:acme/configs --preview
|
|
964
|
+
scc org import https://example.com/org.json --json
|
|
965
|
+
"""
|
|
966
|
+
# --pretty implies --json
|
|
967
|
+
if pretty:
|
|
968
|
+
json_output = True
|
|
969
|
+
set_pretty_mode(True)
|
|
970
|
+
|
|
971
|
+
# Resolve source URL (handles shorthands like github:org/repo)
|
|
972
|
+
resolved = resolve_source(source)
|
|
973
|
+
if isinstance(resolved, ResolveError):
|
|
974
|
+
error_msg = resolved.message
|
|
975
|
+
if resolved.suggestion:
|
|
976
|
+
error_msg = f"{resolved.message}\n{resolved.suggestion}"
|
|
977
|
+
if json_output:
|
|
978
|
+
with json_output_mode():
|
|
979
|
+
envelope = build_envelope(
|
|
980
|
+
Kind.ORG_IMPORT_PREVIEW if preview else Kind.ORG_IMPORT,
|
|
981
|
+
data={"error": error_msg, "source": source},
|
|
982
|
+
ok=False,
|
|
983
|
+
errors=[error_msg],
|
|
984
|
+
)
|
|
985
|
+
print_json(envelope)
|
|
986
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
987
|
+
console.print(create_error_panel("Invalid Source", error_msg))
|
|
988
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
989
|
+
|
|
990
|
+
resolved_url = resolved.resolved_url
|
|
991
|
+
|
|
992
|
+
# Fetch the config from URL
|
|
993
|
+
try:
|
|
994
|
+
response = requests.get(resolved_url, timeout=30)
|
|
995
|
+
except requests.RequestException as e:
|
|
996
|
+
error_msg = f"Failed to fetch config: {e}"
|
|
997
|
+
if json_output:
|
|
998
|
+
with json_output_mode():
|
|
999
|
+
envelope = build_envelope(
|
|
1000
|
+
Kind.ORG_IMPORT_PREVIEW if preview else Kind.ORG_IMPORT,
|
|
1001
|
+
data={"error": error_msg, "source": source, "resolved_url": resolved_url},
|
|
1002
|
+
ok=False,
|
|
1003
|
+
errors=[error_msg],
|
|
1004
|
+
)
|
|
1005
|
+
print_json(envelope)
|
|
1006
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1007
|
+
console.print(create_error_panel("Network Error", error_msg))
|
|
1008
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1009
|
+
|
|
1010
|
+
# Check HTTP status
|
|
1011
|
+
if response.status_code == 404:
|
|
1012
|
+
error_msg = f"Config not found at {resolved_url}"
|
|
1013
|
+
if json_output:
|
|
1014
|
+
with json_output_mode():
|
|
1015
|
+
envelope = build_envelope(
|
|
1016
|
+
Kind.ORG_IMPORT_PREVIEW if preview else Kind.ORG_IMPORT,
|
|
1017
|
+
data={"error": error_msg, "source": source, "resolved_url": resolved_url},
|
|
1018
|
+
ok=False,
|
|
1019
|
+
errors=[error_msg],
|
|
1020
|
+
)
|
|
1021
|
+
print_json(envelope)
|
|
1022
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1023
|
+
console.print(create_error_panel("Not Found", error_msg))
|
|
1024
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1025
|
+
|
|
1026
|
+
if response.status_code != 200:
|
|
1027
|
+
error_msg = f"HTTP {response.status_code} from {resolved_url}"
|
|
1028
|
+
if json_output:
|
|
1029
|
+
with json_output_mode():
|
|
1030
|
+
envelope = build_envelope(
|
|
1031
|
+
Kind.ORG_IMPORT_PREVIEW if preview else Kind.ORG_IMPORT,
|
|
1032
|
+
data={"error": error_msg, "source": source, "resolved_url": resolved_url},
|
|
1033
|
+
ok=False,
|
|
1034
|
+
errors=[error_msg],
|
|
1035
|
+
)
|
|
1036
|
+
print_json(envelope)
|
|
1037
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1038
|
+
console.print(create_error_panel("HTTP Error", error_msg))
|
|
1039
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1040
|
+
|
|
1041
|
+
# Parse JSON response
|
|
1042
|
+
try:
|
|
1043
|
+
config = response.json()
|
|
1044
|
+
except json.JSONDecodeError as e:
|
|
1045
|
+
error_msg = f"Invalid JSON in response: {e}"
|
|
1046
|
+
if json_output:
|
|
1047
|
+
with json_output_mode():
|
|
1048
|
+
envelope = build_envelope(
|
|
1049
|
+
Kind.ORG_IMPORT_PREVIEW if preview else Kind.ORG_IMPORT,
|
|
1050
|
+
data={"error": error_msg, "source": source, "resolved_url": resolved_url},
|
|
1051
|
+
ok=False,
|
|
1052
|
+
errors=[error_msg],
|
|
1053
|
+
)
|
|
1054
|
+
print_json(envelope)
|
|
1055
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1056
|
+
console.print(create_error_panel("Invalid JSON", error_msg))
|
|
1057
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1058
|
+
|
|
1059
|
+
# Validate config against schema
|
|
1060
|
+
validation_errors = validate_org_config(config, "v1")
|
|
1061
|
+
|
|
1062
|
+
# Build preview data
|
|
1063
|
+
preview_data = build_import_preview_data(
|
|
1064
|
+
source=source,
|
|
1065
|
+
resolved_url=resolved_url,
|
|
1066
|
+
config=config,
|
|
1067
|
+
validation_errors=validation_errors,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# Preview mode: show info without saving
|
|
1071
|
+
if preview:
|
|
1072
|
+
if json_output:
|
|
1073
|
+
with json_output_mode():
|
|
1074
|
+
envelope = build_envelope(Kind.ORG_IMPORT_PREVIEW, data=preview_data)
|
|
1075
|
+
print_json(envelope)
|
|
1076
|
+
raise typer.Exit(0)
|
|
1077
|
+
|
|
1078
|
+
# Human-readable preview
|
|
1079
|
+
_render_import_preview(preview_data)
|
|
1080
|
+
raise typer.Exit(0)
|
|
1081
|
+
|
|
1082
|
+
# Import mode: validate and save
|
|
1083
|
+
if not preview_data["valid"]:
|
|
1084
|
+
if json_output:
|
|
1085
|
+
with json_output_mode():
|
|
1086
|
+
envelope = build_envelope(
|
|
1087
|
+
Kind.ORG_IMPORT,
|
|
1088
|
+
data=preview_data,
|
|
1089
|
+
ok=False,
|
|
1090
|
+
errors=validation_errors,
|
|
1091
|
+
)
|
|
1092
|
+
print_json(envelope)
|
|
1093
|
+
raise typer.Exit(EXIT_VALIDATION)
|
|
1094
|
+
console.print(
|
|
1095
|
+
create_error_panel(
|
|
1096
|
+
"Validation Failed",
|
|
1097
|
+
"\n".join(f"• {e}" for e in validation_errors),
|
|
1098
|
+
)
|
|
1099
|
+
)
|
|
1100
|
+
raise typer.Exit(EXIT_VALIDATION)
|
|
1101
|
+
|
|
1102
|
+
# Save to user config
|
|
1103
|
+
user_config = load_user_config()
|
|
1104
|
+
user_config["organization_source"] = {
|
|
1105
|
+
"url": resolved_url,
|
|
1106
|
+
"auth": getattr(resolved, "auth_spec", None),
|
|
1107
|
+
}
|
|
1108
|
+
user_config["standalone"] = False
|
|
1109
|
+
save_user_config(user_config)
|
|
1110
|
+
|
|
1111
|
+
# Cache the fetched config
|
|
1112
|
+
etag = response.headers.get("ETag")
|
|
1113
|
+
save_to_cache(config, source_url=resolved_url, etag=etag, ttl_hours=24)
|
|
1114
|
+
|
|
1115
|
+
# Build import result data
|
|
1116
|
+
import_data = {
|
|
1117
|
+
**preview_data,
|
|
1118
|
+
"imported": True,
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if json_output:
|
|
1122
|
+
with json_output_mode():
|
|
1123
|
+
envelope = build_envelope(Kind.ORG_IMPORT, data=import_data)
|
|
1124
|
+
print_json(envelope)
|
|
1125
|
+
raise typer.Exit(0)
|
|
1126
|
+
|
|
1127
|
+
# Human-readable success
|
|
1128
|
+
org_name = preview_data["organization"]["name"] or "organization"
|
|
1129
|
+
console.print(
|
|
1130
|
+
create_success_panel(
|
|
1131
|
+
"Import Successful",
|
|
1132
|
+
{
|
|
1133
|
+
"Organization": org_name,
|
|
1134
|
+
"Source": source,
|
|
1135
|
+
"Profiles": ", ".join(preview_data["available_profiles"]) or "None",
|
|
1136
|
+
},
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
raise typer.Exit(0)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _render_import_preview(preview: dict[str, Any]) -> None:
|
|
1143
|
+
"""Render import preview as human-readable Rich output.
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
preview: Preview data from build_import_preview_data
|
|
1147
|
+
"""
|
|
1148
|
+
console.print("\n[bold cyan]Organization Config Preview[/bold cyan]")
|
|
1149
|
+
|
|
1150
|
+
# Create info table
|
|
1151
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1152
|
+
table.add_column("Key", style="dim")
|
|
1153
|
+
table.add_column("Value")
|
|
1154
|
+
|
|
1155
|
+
org = preview.get("organization", {})
|
|
1156
|
+
table.add_row("Organization", f"[bold]{org.get('name') or '[unnamed]'}[/bold]")
|
|
1157
|
+
|
|
1158
|
+
if preview["source"] != preview["resolved_url"]:
|
|
1159
|
+
table.add_row("Source", preview["source"])
|
|
1160
|
+
table.add_row("Resolved URL", preview["resolved_url"])
|
|
1161
|
+
else:
|
|
1162
|
+
table.add_row("Source", preview["source"])
|
|
1163
|
+
|
|
1164
|
+
if preview.get("schema_version"):
|
|
1165
|
+
table.add_row("Schema Version", preview["schema_version"])
|
|
1166
|
+
if preview.get("min_cli_version"):
|
|
1167
|
+
table.add_row("Min CLI Version", preview["min_cli_version"])
|
|
1168
|
+
|
|
1169
|
+
profiles = preview.get("available_profiles", [])
|
|
1170
|
+
if profiles:
|
|
1171
|
+
table.add_row("Available Profiles", ", ".join(profiles))
|
|
1172
|
+
|
|
1173
|
+
console.print(table)
|
|
1174
|
+
|
|
1175
|
+
# Validation status
|
|
1176
|
+
if preview["valid"]:
|
|
1177
|
+
console.print("\n[green]✓ Configuration is valid[/green]")
|
|
1178
|
+
else:
|
|
1179
|
+
console.print("\n[red]✗ Configuration is invalid[/red]")
|
|
1180
|
+
for error in preview.get("validation_errors", []):
|
|
1181
|
+
console.print(f" [red]• {error}[/red]")
|
|
1182
|
+
|
|
1183
|
+
console.print("\n[dim]Use 'scc org import <source>' without --preview to import[/dim]\n")
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1187
|
+
# Init Command
|
|
1188
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
@org_app.command("init")
|
|
1192
|
+
@handle_errors
|
|
1193
|
+
def org_init_cmd(
|
|
1194
|
+
template: str = typer.Option(
|
|
1195
|
+
"minimal",
|
|
1196
|
+
"--template",
|
|
1197
|
+
"-t",
|
|
1198
|
+
help="Template to use (minimal, teams, strict, reference).",
|
|
1199
|
+
),
|
|
1200
|
+
org_name: str = typer.Option(
|
|
1201
|
+
"my-org",
|
|
1202
|
+
"--org-name",
|
|
1203
|
+
"-n",
|
|
1204
|
+
help="Organization name for template substitution.",
|
|
1205
|
+
),
|
|
1206
|
+
org_domain: str = typer.Option(
|
|
1207
|
+
"example.com",
|
|
1208
|
+
"--org-domain",
|
|
1209
|
+
"-d",
|
|
1210
|
+
help="Organization domain for template substitution.",
|
|
1211
|
+
),
|
|
1212
|
+
stdout: bool = typer.Option(
|
|
1213
|
+
False,
|
|
1214
|
+
"--stdout",
|
|
1215
|
+
help="Print generated config to stdout instead of writing to file.",
|
|
1216
|
+
),
|
|
1217
|
+
output: Path | None = typer.Option(
|
|
1218
|
+
None,
|
|
1219
|
+
"--output",
|
|
1220
|
+
"-o",
|
|
1221
|
+
help="Write config to specified file path.",
|
|
1222
|
+
),
|
|
1223
|
+
force: bool = typer.Option(
|
|
1224
|
+
False,
|
|
1225
|
+
"--force",
|
|
1226
|
+
"-f",
|
|
1227
|
+
help="Overwrite existing file without prompting.",
|
|
1228
|
+
),
|
|
1229
|
+
list_templates_flag: bool = typer.Option(
|
|
1230
|
+
False,
|
|
1231
|
+
"--list-templates",
|
|
1232
|
+
"-l",
|
|
1233
|
+
help="List available templates and exit.",
|
|
1234
|
+
),
|
|
1235
|
+
json_output: bool = typer.Option(
|
|
1236
|
+
False,
|
|
1237
|
+
"--json",
|
|
1238
|
+
help="Output in JSON envelope format.",
|
|
1239
|
+
),
|
|
1240
|
+
pretty: bool = typer.Option(
|
|
1241
|
+
False,
|
|
1242
|
+
"--pretty",
|
|
1243
|
+
help="Pretty-print JSON output with indentation.",
|
|
1244
|
+
),
|
|
1245
|
+
) -> None:
|
|
1246
|
+
"""Generate an organization config skeleton from templates.
|
|
1247
|
+
|
|
1248
|
+
Templates provide starting points for organization configurations:
|
|
1249
|
+
- minimal: Simple quickstart with sensible defaults
|
|
1250
|
+
- teams: Multi-team setup with delegation
|
|
1251
|
+
- strict: Security-focused for regulated industries
|
|
1252
|
+
- reference: Complete reference with all fields documented
|
|
1253
|
+
|
|
1254
|
+
Examples:
|
|
1255
|
+
scc org init --list-templates # Show available templates
|
|
1256
|
+
scc org init --stdout # Print minimal config to stdout
|
|
1257
|
+
scc org init -t teams --stdout # Print teams template
|
|
1258
|
+
scc org init -o org.json # Write to org.json
|
|
1259
|
+
scc org init -n acme -d acme.com -o . # Customize and write
|
|
1260
|
+
"""
|
|
1261
|
+
if pretty:
|
|
1262
|
+
set_pretty_mode(True)
|
|
1263
|
+
|
|
1264
|
+
# Handle --list-templates
|
|
1265
|
+
if list_templates_flag:
|
|
1266
|
+
_handle_list_templates(json_output)
|
|
1267
|
+
return
|
|
1268
|
+
|
|
1269
|
+
# Require either --stdout or --output
|
|
1270
|
+
if not stdout and output is None:
|
|
1271
|
+
if json_output:
|
|
1272
|
+
with json_output_mode():
|
|
1273
|
+
envelope = build_envelope(
|
|
1274
|
+
Kind.ORG_INIT,
|
|
1275
|
+
data={"error": "Must specify --stdout or --output"},
|
|
1276
|
+
ok=False,
|
|
1277
|
+
)
|
|
1278
|
+
print_json(envelope)
|
|
1279
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1280
|
+
console.print(
|
|
1281
|
+
create_warning_panel(
|
|
1282
|
+
"Output Required",
|
|
1283
|
+
"Must specify either --stdout or --output to generate config.",
|
|
1284
|
+
hint="Use --list-templates to see available templates.",
|
|
1285
|
+
)
|
|
1286
|
+
)
|
|
1287
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1288
|
+
|
|
1289
|
+
# Generate config from template
|
|
1290
|
+
try:
|
|
1291
|
+
vars = TemplateVars(org_name=org_name, org_domain=org_domain)
|
|
1292
|
+
config_json = render_template_string(template, vars)
|
|
1293
|
+
except TemplateNotFoundError as e:
|
|
1294
|
+
if json_output:
|
|
1295
|
+
with json_output_mode():
|
|
1296
|
+
envelope = build_envelope(
|
|
1297
|
+
Kind.ORG_INIT,
|
|
1298
|
+
data={
|
|
1299
|
+
"error": str(e),
|
|
1300
|
+
"available_templates": e.available,
|
|
1301
|
+
},
|
|
1302
|
+
ok=False,
|
|
1303
|
+
)
|
|
1304
|
+
print_json(envelope)
|
|
1305
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1306
|
+
console.print(
|
|
1307
|
+
create_error_panel(
|
|
1308
|
+
"Template Not Found",
|
|
1309
|
+
str(e),
|
|
1310
|
+
hint=f"Available templates: {', '.join(e.available)}",
|
|
1311
|
+
)
|
|
1312
|
+
)
|
|
1313
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1314
|
+
|
|
1315
|
+
# Handle --stdout
|
|
1316
|
+
if stdout:
|
|
1317
|
+
if json_output:
|
|
1318
|
+
# In JSON mode with --stdout, just print the raw config
|
|
1319
|
+
# The config itself is the output, not wrapped in envelope
|
|
1320
|
+
console.print(config_json)
|
|
1321
|
+
else:
|
|
1322
|
+
console.print(config_json)
|
|
1323
|
+
raise typer.Exit(0)
|
|
1324
|
+
|
|
1325
|
+
# Handle --output
|
|
1326
|
+
if output is not None:
|
|
1327
|
+
# Resolve output path
|
|
1328
|
+
if output.is_dir():
|
|
1329
|
+
output_path = output / "org-config.json"
|
|
1330
|
+
else:
|
|
1331
|
+
output_path = output
|
|
1332
|
+
|
|
1333
|
+
# Check for existing file
|
|
1334
|
+
if output_path.exists() and not force:
|
|
1335
|
+
if json_output:
|
|
1336
|
+
with json_output_mode():
|
|
1337
|
+
envelope = build_envelope(
|
|
1338
|
+
Kind.ORG_INIT,
|
|
1339
|
+
data={
|
|
1340
|
+
"error": f"File already exists: {output_path}",
|
|
1341
|
+
"file": str(output_path),
|
|
1342
|
+
},
|
|
1343
|
+
ok=False,
|
|
1344
|
+
)
|
|
1345
|
+
print_json(envelope)
|
|
1346
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1347
|
+
console.print(
|
|
1348
|
+
create_error_panel(
|
|
1349
|
+
"File Exists",
|
|
1350
|
+
f"File already exists: {output_path}",
|
|
1351
|
+
hint="Use --force to overwrite.",
|
|
1352
|
+
)
|
|
1353
|
+
)
|
|
1354
|
+
raise typer.Exit(EXIT_CONFIG)
|
|
1355
|
+
|
|
1356
|
+
# Write file
|
|
1357
|
+
output_path.write_text(config_json)
|
|
1358
|
+
|
|
1359
|
+
if json_output:
|
|
1360
|
+
with json_output_mode():
|
|
1361
|
+
envelope = build_envelope(
|
|
1362
|
+
Kind.ORG_INIT,
|
|
1363
|
+
data={
|
|
1364
|
+
"file": str(output_path),
|
|
1365
|
+
"template": template,
|
|
1366
|
+
"org_name": org_name,
|
|
1367
|
+
"org_domain": org_domain,
|
|
1368
|
+
},
|
|
1369
|
+
)
|
|
1370
|
+
print_json(envelope)
|
|
1371
|
+
else:
|
|
1372
|
+
console.print(
|
|
1373
|
+
create_success_panel(
|
|
1374
|
+
"Config Created",
|
|
1375
|
+
{
|
|
1376
|
+
"File": str(output_path),
|
|
1377
|
+
"Template": template,
|
|
1378
|
+
},
|
|
1379
|
+
)
|
|
1380
|
+
)
|
|
1381
|
+
raise typer.Exit(0)
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def _handle_list_templates(json_output: bool) -> None:
|
|
1385
|
+
"""Handle --list-templates flag.
|
|
1386
|
+
|
|
1387
|
+
Args:
|
|
1388
|
+
json_output: Whether to output JSON envelope format.
|
|
1389
|
+
"""
|
|
1390
|
+
templates = list_templates()
|
|
1391
|
+
|
|
1392
|
+
if json_output:
|
|
1393
|
+
with json_output_mode():
|
|
1394
|
+
template_data = [
|
|
1395
|
+
{
|
|
1396
|
+
"name": t.name,
|
|
1397
|
+
"description": t.description,
|
|
1398
|
+
"level": t.level,
|
|
1399
|
+
"use_case": t.use_case,
|
|
1400
|
+
}
|
|
1401
|
+
for t in templates
|
|
1402
|
+
]
|
|
1403
|
+
envelope = build_envelope(
|
|
1404
|
+
Kind.ORG_TEMPLATE_LIST,
|
|
1405
|
+
data={"templates": template_data},
|
|
1406
|
+
)
|
|
1407
|
+
print_json(envelope)
|
|
1408
|
+
raise typer.Exit(0)
|
|
1409
|
+
|
|
1410
|
+
# Human-readable output
|
|
1411
|
+
console.print("\n[bold cyan]Available Organization Config Templates[/bold cyan]\n")
|
|
1412
|
+
|
|
1413
|
+
table = Table(show_header=True, header_style="bold")
|
|
1414
|
+
table.add_column("Template", style="cyan")
|
|
1415
|
+
table.add_column("Level")
|
|
1416
|
+
table.add_column("Description")
|
|
1417
|
+
|
|
1418
|
+
for t in templates:
|
|
1419
|
+
level_style = {
|
|
1420
|
+
"beginner": "green",
|
|
1421
|
+
"intermediate": "yellow",
|
|
1422
|
+
"advanced": "red",
|
|
1423
|
+
"reference": "blue",
|
|
1424
|
+
}.get(t.level, "")
|
|
1425
|
+
table.add_row(
|
|
1426
|
+
t.name,
|
|
1427
|
+
f"[{level_style}]{t.level}[/{level_style}]" if level_style else t.level,
|
|
1428
|
+
t.description,
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
console.print(table)
|
|
1432
|
+
console.print("\n[dim]Use: scc org init --template <name> --stdout[/dim]\n")
|
|
1433
|
+
raise typer.Exit(0)
|