scc-cli 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of scc-cli might be problematic. Click here for more details.

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