scc-cli 1.5.3__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (153) hide show
  1. scc_cli/__init__.py +15 -0
  2. scc_cli/audit/__init__.py +37 -0
  3. scc_cli/audit/parser.py +191 -0
  4. scc_cli/audit/reader.py +180 -0
  5. scc_cli/auth.py +145 -0
  6. scc_cli/claude_adapter.py +485 -0
  7. scc_cli/cli.py +311 -0
  8. scc_cli/cli_common.py +190 -0
  9. scc_cli/cli_helpers.py +244 -0
  10. scc_cli/commands/__init__.py +20 -0
  11. scc_cli/commands/admin.py +708 -0
  12. scc_cli/commands/audit.py +246 -0
  13. scc_cli/commands/config.py +528 -0
  14. scc_cli/commands/exceptions.py +696 -0
  15. scc_cli/commands/init.py +272 -0
  16. scc_cli/commands/launch/__init__.py +73 -0
  17. scc_cli/commands/launch/app.py +1247 -0
  18. scc_cli/commands/launch/render.py +309 -0
  19. scc_cli/commands/launch/sandbox.py +135 -0
  20. scc_cli/commands/launch/workspace.py +339 -0
  21. scc_cli/commands/org/__init__.py +49 -0
  22. scc_cli/commands/org/_builders.py +264 -0
  23. scc_cli/commands/org/app.py +41 -0
  24. scc_cli/commands/org/import_cmd.py +267 -0
  25. scc_cli/commands/org/init_cmd.py +269 -0
  26. scc_cli/commands/org/schema_cmd.py +76 -0
  27. scc_cli/commands/org/status_cmd.py +157 -0
  28. scc_cli/commands/org/update_cmd.py +330 -0
  29. scc_cli/commands/org/validate_cmd.py +138 -0
  30. scc_cli/commands/support.py +323 -0
  31. scc_cli/commands/team.py +910 -0
  32. scc_cli/commands/worktree/__init__.py +72 -0
  33. scc_cli/commands/worktree/_helpers.py +57 -0
  34. scc_cli/commands/worktree/app.py +170 -0
  35. scc_cli/commands/worktree/container_commands.py +385 -0
  36. scc_cli/commands/worktree/context_commands.py +61 -0
  37. scc_cli/commands/worktree/session_commands.py +128 -0
  38. scc_cli/commands/worktree/worktree_commands.py +734 -0
  39. scc_cli/config.py +647 -0
  40. scc_cli/confirm.py +20 -0
  41. scc_cli/console.py +562 -0
  42. scc_cli/contexts.py +394 -0
  43. scc_cli/core/__init__.py +68 -0
  44. scc_cli/core/constants.py +101 -0
  45. scc_cli/core/errors.py +297 -0
  46. scc_cli/core/exit_codes.py +91 -0
  47. scc_cli/core/workspace.py +57 -0
  48. scc_cli/deprecation.py +54 -0
  49. scc_cli/deps.py +189 -0
  50. scc_cli/docker/__init__.py +127 -0
  51. scc_cli/docker/core.py +467 -0
  52. scc_cli/docker/credentials.py +726 -0
  53. scc_cli/docker/launch.py +595 -0
  54. scc_cli/doctor/__init__.py +105 -0
  55. scc_cli/doctor/checks/__init__.py +166 -0
  56. scc_cli/doctor/checks/cache.py +314 -0
  57. scc_cli/doctor/checks/config.py +107 -0
  58. scc_cli/doctor/checks/environment.py +182 -0
  59. scc_cli/doctor/checks/json_helpers.py +157 -0
  60. scc_cli/doctor/checks/organization.py +264 -0
  61. scc_cli/doctor/checks/worktree.py +278 -0
  62. scc_cli/doctor/render.py +365 -0
  63. scc_cli/doctor/types.py +66 -0
  64. scc_cli/evaluation/__init__.py +27 -0
  65. scc_cli/evaluation/apply_exceptions.py +207 -0
  66. scc_cli/evaluation/evaluate.py +97 -0
  67. scc_cli/evaluation/models.py +80 -0
  68. scc_cli/git.py +84 -0
  69. scc_cli/json_command.py +166 -0
  70. scc_cli/json_output.py +159 -0
  71. scc_cli/kinds.py +65 -0
  72. scc_cli/marketplace/__init__.py +123 -0
  73. scc_cli/marketplace/adapter.py +74 -0
  74. scc_cli/marketplace/compute.py +377 -0
  75. scc_cli/marketplace/constants.py +87 -0
  76. scc_cli/marketplace/managed.py +135 -0
  77. scc_cli/marketplace/materialize.py +846 -0
  78. scc_cli/marketplace/normalize.py +548 -0
  79. scc_cli/marketplace/render.py +281 -0
  80. scc_cli/marketplace/resolve.py +459 -0
  81. scc_cli/marketplace/schema.py +506 -0
  82. scc_cli/marketplace/sync.py +279 -0
  83. scc_cli/marketplace/team_cache.py +195 -0
  84. scc_cli/marketplace/team_fetch.py +689 -0
  85. scc_cli/marketplace/trust.py +244 -0
  86. scc_cli/models/__init__.py +41 -0
  87. scc_cli/models/exceptions.py +273 -0
  88. scc_cli/models/plugin_audit.py +434 -0
  89. scc_cli/org_templates.py +269 -0
  90. scc_cli/output_mode.py +167 -0
  91. scc_cli/panels.py +113 -0
  92. scc_cli/platform.py +350 -0
  93. scc_cli/profiles.py +960 -0
  94. scc_cli/remote.py +443 -0
  95. scc_cli/schemas/__init__.py +1 -0
  96. scc_cli/schemas/org-v1.schema.json +456 -0
  97. scc_cli/schemas/team-config.v1.schema.json +163 -0
  98. scc_cli/services/__init__.py +1 -0
  99. scc_cli/services/git/__init__.py +79 -0
  100. scc_cli/services/git/branch.py +151 -0
  101. scc_cli/services/git/core.py +216 -0
  102. scc_cli/services/git/hooks.py +108 -0
  103. scc_cli/services/git/worktree.py +444 -0
  104. scc_cli/services/workspace/__init__.py +36 -0
  105. scc_cli/services/workspace/resolver.py +223 -0
  106. scc_cli/services/workspace/suspicious.py +200 -0
  107. scc_cli/sessions.py +425 -0
  108. scc_cli/setup.py +589 -0
  109. scc_cli/source_resolver.py +470 -0
  110. scc_cli/stats.py +378 -0
  111. scc_cli/stores/__init__.py +13 -0
  112. scc_cli/stores/exception_store.py +251 -0
  113. scc_cli/subprocess_utils.py +88 -0
  114. scc_cli/teams.py +383 -0
  115. scc_cli/templates/__init__.py +2 -0
  116. scc_cli/templates/org/__init__.py +0 -0
  117. scc_cli/templates/org/minimal.json +19 -0
  118. scc_cli/templates/org/reference.json +74 -0
  119. scc_cli/templates/org/strict.json +38 -0
  120. scc_cli/templates/org/teams.json +42 -0
  121. scc_cli/templates/statusline.sh +75 -0
  122. scc_cli/theme.py +348 -0
  123. scc_cli/ui/__init__.py +154 -0
  124. scc_cli/ui/branding.py +68 -0
  125. scc_cli/ui/chrome.py +401 -0
  126. scc_cli/ui/dashboard/__init__.py +62 -0
  127. scc_cli/ui/dashboard/_dashboard.py +794 -0
  128. scc_cli/ui/dashboard/loaders.py +452 -0
  129. scc_cli/ui/dashboard/models.py +185 -0
  130. scc_cli/ui/dashboard/orchestrator.py +735 -0
  131. scc_cli/ui/formatters.py +444 -0
  132. scc_cli/ui/gate.py +350 -0
  133. scc_cli/ui/git_interactive.py +869 -0
  134. scc_cli/ui/git_render.py +176 -0
  135. scc_cli/ui/help.py +157 -0
  136. scc_cli/ui/keys.py +615 -0
  137. scc_cli/ui/list_screen.py +437 -0
  138. scc_cli/ui/picker.py +763 -0
  139. scc_cli/ui/prompts.py +201 -0
  140. scc_cli/ui/quick_resume.py +116 -0
  141. scc_cli/ui/wizard.py +576 -0
  142. scc_cli/update.py +680 -0
  143. scc_cli/utils/__init__.py +39 -0
  144. scc_cli/utils/fixit.py +264 -0
  145. scc_cli/utils/fuzzy.py +124 -0
  146. scc_cli/utils/locks.py +114 -0
  147. scc_cli/utils/ttl.py +376 -0
  148. scc_cli/validate.py +455 -0
  149. scc_cli-1.5.3.dist-info/METADATA +401 -0
  150. scc_cli-1.5.3.dist-info/RECORD +153 -0
  151. scc_cli-1.5.3.dist-info/WHEEL +4 -0
  152. scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
  153. scc_cli-1.5.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,157 @@
1
+ """Org status command for showing organization configuration status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.table import Table
9
+
10
+ from ...cli_common import console, handle_errors
11
+ from ...config import load_user_config
12
+ from ...core.constants import CLI_VERSION
13
+ from ...json_output import build_envelope
14
+ from ...kinds import Kind
15
+ from ...output_mode import json_output_mode, print_json, set_pretty_mode
16
+ from ...remote import load_from_cache
17
+ from ._builders import build_status_data
18
+
19
+
20
+ @handle_errors
21
+ def org_status_cmd(
22
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON envelope"),
23
+ pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
24
+ ) -> None:
25
+ """Show current organization configuration status.
26
+
27
+ Displays connection mode (standalone or organization), cache freshness,
28
+ version compatibility, and selected profile.
29
+
30
+ Examples:
31
+ scc org status
32
+ scc org status --json
33
+ scc org status --pretty
34
+ """
35
+ # --pretty implies --json
36
+ if pretty:
37
+ json_output = True
38
+ set_pretty_mode(True)
39
+
40
+ # Load configuration data
41
+ user_config = load_user_config()
42
+ org_config, cache_meta = load_from_cache()
43
+
44
+ # Build status data
45
+ status_data = build_status_data(user_config, org_config, cache_meta)
46
+
47
+ # JSON output mode
48
+ if json_output:
49
+ with json_output_mode():
50
+ envelope = build_envelope(Kind.ORG_STATUS, data=status_data)
51
+ print_json(envelope)
52
+ raise typer.Exit(0)
53
+
54
+ # Human-readable output
55
+ _render_status_human(status_data)
56
+ raise typer.Exit(0)
57
+
58
+
59
+ def _render_status_human(status: dict[str, Any]) -> None:
60
+ """Render status data as human-readable Rich output.
61
+
62
+ Args:
63
+ status: Status data from build_status_data
64
+ """
65
+ # Mode header
66
+ mode = status["mode"]
67
+ if mode == "standalone":
68
+ console.print("\n[bold cyan]Organization Status[/bold cyan]")
69
+ console.print(" Mode: [yellow]Standalone[/yellow] (no organization configured)")
70
+ console.print("\n [dim]Tip: Run 'scc setup' to connect to an organization[/dim]\n")
71
+ return
72
+
73
+ # Organization mode
74
+ console.print("\n[bold cyan]Organization Status[/bold cyan]")
75
+
76
+ # Create a table for organization info
77
+ table = Table(show_header=False, box=None, padding=(0, 2))
78
+ table.add_column("Key", style="dim")
79
+ table.add_column("Value")
80
+
81
+ # Organization info
82
+ org = status.get("organization", {})
83
+ if org:
84
+ org_name = org.get("name") or "[not fetched]"
85
+ table.add_row("Organization", f"[bold]{org_name}[/bold]")
86
+ table.add_row("Source URL", org.get("source_url", "[not configured]"))
87
+
88
+ # Selected profile
89
+ profile = status.get("selected_profile")
90
+ if profile:
91
+ table.add_row("Selected Profile", f"[green]{profile}[/green]")
92
+ else:
93
+ table.add_row("Selected Profile", "[yellow]None[/yellow]")
94
+
95
+ # Available profiles
96
+ available = status.get("available_profiles", [])
97
+ if available:
98
+ table.add_row("Available Profiles", ", ".join(available))
99
+
100
+ console.print(table)
101
+
102
+ # Cache status
103
+ cache = status.get("cache")
104
+ if cache:
105
+ console.print("\n[bold]Cache Status[/bold]")
106
+ cache_table = Table(show_header=False, box=None, padding=(0, 2))
107
+ cache_table.add_column("Key", style="dim")
108
+ cache_table.add_column("Value")
109
+
110
+ if cache.get("valid"):
111
+ cache_table.add_row("Status", "[green]+ Fresh[/green]")
112
+ else:
113
+ cache_table.add_row("Status", "[yellow]! Expired[/yellow]")
114
+
115
+ if cache.get("fetched_at"):
116
+ cache_table.add_row("Fetched At", cache["fetched_at"])
117
+ if cache.get("expires_at"):
118
+ cache_table.add_row("Expires At", cache["expires_at"])
119
+
120
+ console.print(cache_table)
121
+ else:
122
+ console.print("\n[yellow]Cache:[/yellow] Not fetched yet")
123
+ console.print(
124
+ " [dim]Run 'scc start' or 'scc doctor' to fetch the organization config[/dim]"
125
+ )
126
+
127
+ # Version compatibility
128
+ compat = status.get("version_compatibility")
129
+ if compat:
130
+ console.print("\n[bold]Version Compatibility[/bold]")
131
+ compat_table = Table(show_header=False, box=None, padding=(0, 2))
132
+ compat_table.add_column("Key", style="dim")
133
+ compat_table.add_column("Value")
134
+
135
+ if compat.get("compatible"):
136
+ compat_table.add_row("Status", "[green]+ Compatible[/green]")
137
+ else:
138
+ if compat.get("blocking_error"):
139
+ compat_table.add_row("Status", "[red]x Incompatible[/red]")
140
+ compat_table.add_row("Error", f"[red]{compat['blocking_error']}[/red]")
141
+ else:
142
+ compat_table.add_row("Status", "[yellow]! Warnings[/yellow]")
143
+
144
+ if compat.get("schema_version"):
145
+ compat_table.add_row("Schema Version", compat["schema_version"])
146
+ if compat.get("min_cli_version"):
147
+ compat_table.add_row("Min CLI Version", compat["min_cli_version"])
148
+ compat_table.add_row("Current CLI", compat.get("current_cli_version", CLI_VERSION))
149
+
150
+ # Show warnings if any
151
+ warnings = compat.get("warnings", [])
152
+ for warning in warnings:
153
+ console.print(f" [yellow]! {warning}[/yellow]")
154
+
155
+ console.print(compat_table)
156
+
157
+ console.print() # Final newline
@@ -0,0 +1,330 @@
1
+ """Org update command for refreshing organization and team configs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from ...cli_common import console, handle_errors
10
+ from ...config import load_user_config
11
+ from ...core.exit_codes import EXIT_CONFIG
12
+ from ...json_output import build_envelope
13
+ from ...kinds import Kind
14
+ from ...marketplace.team_fetch import fetch_team_config
15
+ from ...output_mode import json_output_mode, print_json, set_pretty_mode
16
+ from ...panels import create_error_panel, create_success_panel, create_warning_panel
17
+ from ...remote import load_org_config
18
+ from ._builders import _parse_config_source, build_update_data
19
+
20
+
21
+ @handle_errors
22
+ def org_update_cmd(
23
+ team: str | None = typer.Option(
24
+ None, "--team", "-t", help="Refresh a specific federated team's config"
25
+ ),
26
+ all_teams: bool = typer.Option(
27
+ False, "--all-teams", "-a", help="Refresh all federated team configs"
28
+ ),
29
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
30
+ pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
31
+ ) -> None:
32
+ """Refresh organization config and optionally team configs.
33
+
34
+ By default, refreshes the organization config from its remote source.
35
+ With --team or --all-teams, also refreshes federated team configurations.
36
+
37
+ Examples:
38
+ scc org update # Refresh org config only
39
+ scc org update --team dev # Also refresh 'dev' team config
40
+ scc org update --all-teams # Refresh all federated team configs
41
+ """
42
+ # --pretty implies --json
43
+ if pretty:
44
+ json_output = True
45
+ set_pretty_mode(True)
46
+
47
+ # Load user config
48
+ user_config = load_user_config()
49
+
50
+ # Check for standalone mode
51
+ is_standalone = user_config.get("standalone", False)
52
+ if is_standalone:
53
+ if json_output:
54
+ with json_output_mode():
55
+ envelope = build_envelope(
56
+ Kind.ORG_UPDATE,
57
+ data={"error": "Cannot update in standalone mode"},
58
+ ok=False,
59
+ errors=["CLI is running in standalone mode"],
60
+ )
61
+ print_json(envelope)
62
+ raise typer.Exit(EXIT_CONFIG)
63
+ console.print(
64
+ create_error_panel(
65
+ "Standalone Mode",
66
+ "Cannot update organization config in standalone mode.",
67
+ hint="Use 'scc setup' to connect to an organization.",
68
+ )
69
+ )
70
+ raise typer.Exit(EXIT_CONFIG)
71
+
72
+ # Check for organization source
73
+ org_source = user_config.get("organization_source")
74
+ if not org_source:
75
+ if json_output:
76
+ with json_output_mode():
77
+ envelope = build_envelope(
78
+ Kind.ORG_UPDATE,
79
+ data={"error": "No organization source configured"},
80
+ ok=False,
81
+ errors=["No organization source configured"],
82
+ )
83
+ print_json(envelope)
84
+ raise typer.Exit(EXIT_CONFIG)
85
+ console.print(
86
+ create_error_panel(
87
+ "No Organization",
88
+ "No organization source is configured.",
89
+ hint="Use 'scc setup' to connect to an organization.",
90
+ )
91
+ )
92
+ raise typer.Exit(EXIT_CONFIG)
93
+
94
+ # Force refresh org config
95
+ org_config = load_org_config(user_config, force_refresh=True)
96
+ if org_config is None:
97
+ if json_output:
98
+ with json_output_mode():
99
+ envelope = build_envelope(
100
+ Kind.ORG_UPDATE,
101
+ data=build_update_data(None),
102
+ ok=False,
103
+ errors=["Failed to fetch organization config"],
104
+ )
105
+ print_json(envelope)
106
+ raise typer.Exit(EXIT_CONFIG)
107
+ console.print(
108
+ create_error_panel(
109
+ "Update Failed",
110
+ "Failed to fetch organization config from remote.",
111
+ hint="Check network connection and organization URL.",
112
+ )
113
+ )
114
+ raise typer.Exit(EXIT_CONFIG)
115
+
116
+ # Get profiles from org config
117
+ profiles = org_config.get("profiles", {})
118
+
119
+ # Handle --team option (single team update)
120
+ team_results: list[dict[str, Any]] | None = None
121
+ if team is not None:
122
+ # Validate team exists
123
+ if team not in profiles:
124
+ if json_output:
125
+ with json_output_mode():
126
+ envelope = build_envelope(
127
+ Kind.ORG_UPDATE,
128
+ data=build_update_data(org_config),
129
+ ok=False,
130
+ errors=[f"Team '{team}' not found in organization config"],
131
+ )
132
+ print_json(envelope)
133
+ raise typer.Exit(EXIT_CONFIG)
134
+ console.print(
135
+ create_error_panel(
136
+ "Team Not Found",
137
+ f"Team '{team}' not found in organization config.",
138
+ hint=f"Available teams: {', '.join(profiles.keys())}",
139
+ )
140
+ )
141
+ raise typer.Exit(EXIT_CONFIG)
142
+
143
+ profile = profiles[team]
144
+ config_source_dict = profile.get("config_source")
145
+
146
+ # Check if team is federated
147
+ if config_source_dict is None:
148
+ team_results = [{"team": team, "success": True, "inline": True}]
149
+ if json_output:
150
+ with json_output_mode():
151
+ data = build_update_data(org_config, team_results)
152
+ envelope = build_envelope(Kind.ORG_UPDATE, data=data)
153
+ print_json(envelope)
154
+ raise typer.Exit(0)
155
+ console.print(
156
+ create_warning_panel(
157
+ "Inline Team",
158
+ f"Team '{team}' is not federated (inline config).",
159
+ hint="Inline teams don't have external configs to refresh.",
160
+ )
161
+ )
162
+ raise typer.Exit(0)
163
+
164
+ # Fetch team config
165
+ try:
166
+ config_source = _parse_config_source(config_source_dict)
167
+ result = fetch_team_config(config_source, team)
168
+ if result.success:
169
+ team_results = [
170
+ {
171
+ "team": team,
172
+ "success": True,
173
+ "commit_sha": result.commit_sha,
174
+ }
175
+ ]
176
+ else:
177
+ team_results = [
178
+ {
179
+ "team": team,
180
+ "success": False,
181
+ "error": result.error,
182
+ }
183
+ ]
184
+ if json_output:
185
+ with json_output_mode():
186
+ data = build_update_data(org_config, team_results)
187
+ envelope = build_envelope(
188
+ Kind.ORG_UPDATE,
189
+ data=data,
190
+ ok=False,
191
+ errors=[f"Failed to fetch team config: {result.error}"],
192
+ )
193
+ print_json(envelope)
194
+ raise typer.Exit(EXIT_CONFIG)
195
+ console.print(
196
+ create_error_panel(
197
+ "Team Update Failed",
198
+ f"Failed to fetch config for team '{team}'.",
199
+ hint=str(result.error),
200
+ )
201
+ )
202
+ raise typer.Exit(EXIT_CONFIG)
203
+ except Exception as e:
204
+ if json_output:
205
+ with json_output_mode():
206
+ envelope = build_envelope(
207
+ Kind.ORG_UPDATE,
208
+ data=build_update_data(org_config),
209
+ ok=False,
210
+ errors=[f"Error parsing config source: {e}"],
211
+ )
212
+ print_json(envelope)
213
+ raise typer.Exit(EXIT_CONFIG)
214
+ console.print(create_error_panel("Config Error", f"Error parsing config source: {e}"))
215
+ raise typer.Exit(EXIT_CONFIG)
216
+
217
+ # Handle --all-teams option
218
+ elif all_teams:
219
+ team_results = []
220
+ federated_teams = [
221
+ (name, profile)
222
+ for name, profile in profiles.items()
223
+ if profile.get("config_source") is not None
224
+ ]
225
+
226
+ if not federated_teams:
227
+ team_results = []
228
+ if json_output:
229
+ with json_output_mode():
230
+ data = build_update_data(org_config, team_results)
231
+ envelope = build_envelope(Kind.ORG_UPDATE, data=data)
232
+ print_json(envelope)
233
+ raise typer.Exit(0)
234
+ console.print(
235
+ create_warning_panel(
236
+ "No Federated Teams",
237
+ "No federated teams found in organization config.",
238
+ hint="All teams use inline configuration.",
239
+ )
240
+ )
241
+ raise typer.Exit(0)
242
+
243
+ # Fetch all federated team configs
244
+ for team_name, profile in federated_teams:
245
+ config_source_dict = profile["config_source"]
246
+ try:
247
+ config_source = _parse_config_source(config_source_dict)
248
+ result = fetch_team_config(config_source, team_name)
249
+ if result.success:
250
+ team_results.append(
251
+ {
252
+ "team": team_name,
253
+ "success": True,
254
+ "commit_sha": result.commit_sha,
255
+ }
256
+ )
257
+ else:
258
+ team_results.append(
259
+ {
260
+ "team": team_name,
261
+ "success": False,
262
+ "error": result.error,
263
+ }
264
+ )
265
+ except Exception as e:
266
+ team_results.append(
267
+ {
268
+ "team": team_name,
269
+ "success": False,
270
+ "error": str(e),
271
+ }
272
+ )
273
+
274
+ # Build output data
275
+ data = build_update_data(org_config, team_results)
276
+
277
+ # JSON output
278
+ if json_output:
279
+ with json_output_mode():
280
+ # Determine overall success
281
+ has_team_failures = team_results is not None and any(
282
+ not t.get("success") for t in team_results
283
+ )
284
+ envelope = build_envelope(
285
+ Kind.ORG_UPDATE,
286
+ data=data,
287
+ ok=not has_team_failures,
288
+ )
289
+ print_json(envelope)
290
+ raise typer.Exit(0)
291
+
292
+ # Human-readable output
293
+ org_data = org_config.get("organization", {})
294
+ org_name = org_data.get("name", "Unknown")
295
+
296
+ if team_results is None:
297
+ # Org-only update
298
+ console.print(
299
+ create_success_panel(
300
+ "Organization Updated",
301
+ {
302
+ "Organization": org_name,
303
+ "Status": "Refreshed from remote",
304
+ },
305
+ )
306
+ )
307
+ else:
308
+ # Team updates included
309
+ success_count = sum(1 for t in team_results if t.get("success"))
310
+ failed_count = len(team_results) - success_count
311
+
312
+ if failed_count == 0:
313
+ console.print(
314
+ create_success_panel(
315
+ "Update Complete",
316
+ {
317
+ "Organization": org_name,
318
+ "Teams Updated": str(success_count),
319
+ },
320
+ )
321
+ )
322
+ else:
323
+ console.print(
324
+ create_warning_panel(
325
+ "Partial Update",
326
+ f"Organization updated. {success_count} team(s) succeeded, {failed_count} failed.",
327
+ )
328
+ )
329
+
330
+ raise typer.Exit(0)
@@ -0,0 +1,138 @@
1
+ """Org validate command for schema and semantic validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from ...cli_common import console, handle_errors
11
+ from ...core.exit_codes import EXIT_CONFIG, EXIT_VALIDATION
12
+ from ...json_output import build_envelope
13
+ from ...kinds import Kind
14
+ from ...output_mode import json_output_mode, print_json, set_pretty_mode
15
+ from ...panels import create_error_panel, create_success_panel, create_warning_panel
16
+ from ...validate import validate_org_config
17
+ from ._builders import build_validation_data, check_semantic_errors
18
+
19
+
20
+ @handle_errors
21
+ def org_validate_cmd(
22
+ source: str = typer.Argument(..., help="Path to config file to validate"),
23
+ schema_version: str = typer.Option(
24
+ "v1", "--schema-version", "-s", help="Schema version (default: v1)"
25
+ ),
26
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
27
+ pretty: bool = typer.Option(False, "--pretty", help="Pretty-print JSON (implies --json)"),
28
+ ) -> None:
29
+ """Validate an organization configuration file.
30
+
31
+ Performs both JSON schema validation and semantic checks.
32
+
33
+ Examples:
34
+ scc org validate ./org-config.json
35
+ scc org validate ./org-config.json --json
36
+ """
37
+ # --pretty implies --json
38
+ if pretty:
39
+ json_output = True
40
+ set_pretty_mode(True)
41
+
42
+ # Load config file
43
+ config_path = Path(source).expanduser().resolve()
44
+ if not config_path.exists():
45
+ if json_output:
46
+ with json_output_mode():
47
+ data = build_validation_data(
48
+ source=source,
49
+ schema_errors=[f"File not found: {source}"],
50
+ semantic_errors=[],
51
+ schema_version=schema_version,
52
+ )
53
+ envelope = build_envelope(Kind.ORG_VALIDATION, data=data, ok=False)
54
+ print_json(envelope)
55
+ raise typer.Exit(EXIT_CONFIG)
56
+ console.print(create_error_panel("File Not Found", f"Cannot find config file: {source}"))
57
+ raise typer.Exit(EXIT_CONFIG)
58
+
59
+ # Parse JSON
60
+ try:
61
+ config = json.loads(config_path.read_text())
62
+ except json.JSONDecodeError as e:
63
+ if json_output:
64
+ with json_output_mode():
65
+ data = build_validation_data(
66
+ source=source,
67
+ schema_errors=[f"Invalid JSON: {e}"],
68
+ semantic_errors=[],
69
+ schema_version=schema_version,
70
+ )
71
+ envelope = build_envelope(Kind.ORG_VALIDATION, data=data, ok=False)
72
+ print_json(envelope)
73
+ raise typer.Exit(EXIT_CONFIG)
74
+ console.print(create_error_panel("Invalid JSON", f"Failed to parse JSON: {e}"))
75
+ raise typer.Exit(EXIT_CONFIG)
76
+
77
+ # Validate against schema
78
+ schema_errors = validate_org_config(config, schema_version)
79
+
80
+ # Check semantic errors (only if schema is valid)
81
+ semantic_errors: list[str] = []
82
+ if not schema_errors:
83
+ semantic_errors = check_semantic_errors(config)
84
+
85
+ # Build result data
86
+ data = build_validation_data(
87
+ source=source,
88
+ schema_errors=schema_errors,
89
+ semantic_errors=semantic_errors,
90
+ schema_version=schema_version,
91
+ )
92
+
93
+ # JSON output mode
94
+ if json_output:
95
+ with json_output_mode():
96
+ is_valid = data["valid"]
97
+ all_errors = schema_errors + semantic_errors
98
+ envelope = build_envelope(
99
+ Kind.ORG_VALIDATION,
100
+ data=data,
101
+ ok=is_valid,
102
+ errors=all_errors if not is_valid else None,
103
+ )
104
+ print_json(envelope)
105
+ raise typer.Exit(0 if is_valid else EXIT_VALIDATION)
106
+
107
+ # Human-readable output
108
+ if data["valid"]:
109
+ console.print(
110
+ create_success_panel(
111
+ "Validation Passed",
112
+ {
113
+ "Source": source,
114
+ "Schema Version": schema_version,
115
+ "Status": "Valid",
116
+ },
117
+ )
118
+ )
119
+ raise typer.Exit(0)
120
+
121
+ # Show errors
122
+ if schema_errors:
123
+ console.print(
124
+ create_error_panel(
125
+ "Schema Validation Failed",
126
+ "\n".join(f"• {e}" for e in schema_errors),
127
+ )
128
+ )
129
+
130
+ if semantic_errors:
131
+ console.print(
132
+ create_warning_panel(
133
+ "Semantic Issues",
134
+ "\n".join(f"• {e}" for e in semantic_errors),
135
+ )
136
+ )
137
+
138
+ raise typer.Exit(EXIT_VALIDATION)