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.

Files changed (112) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +259 -0
  8. scc_cli/cli_admin.py +683 -0
  9. scc_cli/cli_audit.py +245 -0
  10. scc_cli/cli_common.py +166 -0
  11. scc_cli/cli_config.py +527 -0
  12. scc_cli/cli_exceptions.py +705 -0
  13. scc_cli/cli_helpers.py +244 -0
  14. scc_cli/cli_init.py +272 -0
  15. scc_cli/cli_launch.py +1400 -0
  16. scc_cli/cli_org.py +1433 -0
  17. scc_cli/cli_support.py +322 -0
  18. scc_cli/cli_team.py +858 -0
  19. scc_cli/cli_worktree.py +865 -0
  20. scc_cli/config.py +583 -0
  21. scc_cli/console.py +562 -0
  22. scc_cli/constants.py +79 -0
  23. scc_cli/contexts.py +377 -0
  24. scc_cli/deprecation.py +54 -0
  25. scc_cli/deps.py +189 -0
  26. scc_cli/docker/__init__.py +127 -0
  27. scc_cli/docker/core.py +466 -0
  28. scc_cli/docker/credentials.py +726 -0
  29. scc_cli/docker/launch.py +603 -0
  30. scc_cli/doctor/__init__.py +99 -0
  31. scc_cli/doctor/checks.py +1082 -0
  32. scc_cli/doctor/render.py +346 -0
  33. scc_cli/doctor/types.py +66 -0
  34. scc_cli/errors.py +288 -0
  35. scc_cli/evaluation/__init__.py +27 -0
  36. scc_cli/evaluation/apply_exceptions.py +207 -0
  37. scc_cli/evaluation/evaluate.py +97 -0
  38. scc_cli/evaluation/models.py +80 -0
  39. scc_cli/exit_codes.py +55 -0
  40. scc_cli/git.py +1405 -0
  41. scc_cli/json_command.py +166 -0
  42. scc_cli/json_output.py +96 -0
  43. scc_cli/kinds.py +62 -0
  44. scc_cli/marketplace/__init__.py +123 -0
  45. scc_cli/marketplace/compute.py +377 -0
  46. scc_cli/marketplace/constants.py +87 -0
  47. scc_cli/marketplace/managed.py +135 -0
  48. scc_cli/marketplace/materialize.py +723 -0
  49. scc_cli/marketplace/normalize.py +548 -0
  50. scc_cli/marketplace/render.py +238 -0
  51. scc_cli/marketplace/resolve.py +459 -0
  52. scc_cli/marketplace/schema.py +502 -0
  53. scc_cli/marketplace/sync.py +257 -0
  54. scc_cli/marketplace/team_cache.py +195 -0
  55. scc_cli/marketplace/team_fetch.py +688 -0
  56. scc_cli/marketplace/trust.py +244 -0
  57. scc_cli/models/__init__.py +41 -0
  58. scc_cli/models/exceptions.py +273 -0
  59. scc_cli/models/plugin_audit.py +434 -0
  60. scc_cli/org_templates.py +269 -0
  61. scc_cli/output_mode.py +167 -0
  62. scc_cli/panels.py +113 -0
  63. scc_cli/platform.py +350 -0
  64. scc_cli/profiles.py +1034 -0
  65. scc_cli/remote.py +443 -0
  66. scc_cli/schemas/__init__.py +1 -0
  67. scc_cli/schemas/org-v1.schema.json +456 -0
  68. scc_cli/schemas/team-config.v1.schema.json +163 -0
  69. scc_cli/sessions.py +425 -0
  70. scc_cli/setup.py +582 -0
  71. scc_cli/source_resolver.py +470 -0
  72. scc_cli/stats.py +378 -0
  73. scc_cli/stores/__init__.py +13 -0
  74. scc_cli/stores/exception_store.py +251 -0
  75. scc_cli/subprocess_utils.py +88 -0
  76. scc_cli/teams.py +339 -0
  77. scc_cli/templates/__init__.py +2 -0
  78. scc_cli/templates/org/__init__.py +0 -0
  79. scc_cli/templates/org/minimal.json +19 -0
  80. scc_cli/templates/org/reference.json +74 -0
  81. scc_cli/templates/org/strict.json +38 -0
  82. scc_cli/templates/org/teams.json +42 -0
  83. scc_cli/templates/statusline.sh +75 -0
  84. scc_cli/theme.py +348 -0
  85. scc_cli/ui/__init__.py +124 -0
  86. scc_cli/ui/branding.py +68 -0
  87. scc_cli/ui/chrome.py +395 -0
  88. scc_cli/ui/dashboard/__init__.py +62 -0
  89. scc_cli/ui/dashboard/_dashboard.py +669 -0
  90. scc_cli/ui/dashboard/loaders.py +369 -0
  91. scc_cli/ui/dashboard/models.py +184 -0
  92. scc_cli/ui/dashboard/orchestrator.py +337 -0
  93. scc_cli/ui/formatters.py +443 -0
  94. scc_cli/ui/gate.py +350 -0
  95. scc_cli/ui/help.py +157 -0
  96. scc_cli/ui/keys.py +521 -0
  97. scc_cli/ui/list_screen.py +431 -0
  98. scc_cli/ui/picker.py +700 -0
  99. scc_cli/ui/prompts.py +200 -0
  100. scc_cli/ui/wizard.py +490 -0
  101. scc_cli/update.py +680 -0
  102. scc_cli/utils/__init__.py +39 -0
  103. scc_cli/utils/fixit.py +264 -0
  104. scc_cli/utils/fuzzy.py +124 -0
  105. scc_cli/utils/locks.py +101 -0
  106. scc_cli/utils/ttl.py +376 -0
  107. scc_cli/validate.py +455 -0
  108. scc_cli-1.4.0.dist-info/METADATA +369 -0
  109. scc_cli-1.4.0.dist-info/RECORD +112 -0
  110. scc_cli-1.4.0.dist-info/WHEEL +4 -0
  111. scc_cli-1.4.0.dist-info/entry_points.txt +2 -0
  112. 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)