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,339 @@
1
+ """
2
+ Workspace validation and preparation functions.
3
+
4
+ This module handles workspace-related operations for the launch command:
5
+ - Path validation and resolution
6
+ - Worktree creation and mounting
7
+ - Dependency installation
8
+ - Team-workspace association resolution
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ import typer
19
+ from rich.status import Status
20
+
21
+ from ... import config, deps, git
22
+ from ... import platform as platform_module
23
+ from ...cli_common import console
24
+ from ...confirm import Confirm
25
+ from ...core.constants import WORKTREE_BRANCH_PREFIX
26
+ from ...core.errors import NotAGitRepoError, WorkspaceNotFoundError
27
+ from ...core.exit_codes import EXIT_CANCELLED
28
+ from ...core.workspace import ResolverResult
29
+ from ...output_mode import print_human
30
+ from ...panels import create_info_panel, create_success_panel, create_warning_panel
31
+ from ...theme import Indicators, Spinners
32
+ from ...ui.gate import is_interactive_allowed
33
+
34
+ if TYPE_CHECKING:
35
+ pass
36
+
37
+
38
+ @dataclass
39
+ class LaunchContext:
40
+ """Display-focused launch context wrapping ResolverResult.
41
+
42
+ This dataclass is used for rendering the launch panel and JSON output.
43
+ It combines resolver results with session-specific information.
44
+ """
45
+
46
+ resolver_result: ResolverResult
47
+ team: str | None
48
+ branch: str | None
49
+ session_name: str | None
50
+ mode: str # "new" or "resume"
51
+
52
+ @property
53
+ def workspace_root(self) -> Path:
54
+ """Workspace root (WR)."""
55
+ return self.resolver_result.workspace_root
56
+
57
+ @property
58
+ def entry_dir(self) -> Path:
59
+ """Entry directory (ED)."""
60
+ return self.resolver_result.entry_dir
61
+
62
+ @property
63
+ def mount_root(self) -> Path:
64
+ """Mount root (MR)."""
65
+ return self.resolver_result.mount_root
66
+
67
+ @property
68
+ def container_workdir(self) -> str:
69
+ """Container working directory (CW)."""
70
+ return self.resolver_result.container_workdir
71
+
72
+ @property
73
+ def entry_dir_relative(self) -> str:
74
+ """Entry dir path relative to workspace root."""
75
+ try:
76
+ return str(self.entry_dir.relative_to(self.workspace_root))
77
+ except ValueError:
78
+ return str(self.entry_dir)
79
+
80
+ @property
81
+ def is_mount_expanded(self) -> bool:
82
+ """Whether mount was expanded for worktree support."""
83
+ return self.resolver_result.is_mount_expanded
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ """Convert to dictionary for JSON output."""
87
+ return {
88
+ "workspace_root": str(self.workspace_root),
89
+ "entry_dir": str(self.entry_dir),
90
+ "entry_dir_relative": self.entry_dir_relative,
91
+ "mount_root": str(self.mount_root),
92
+ "container_workdir": self.container_workdir,
93
+ "is_mount_expanded": self.is_mount_expanded,
94
+ "team": self.team,
95
+ "branch": self.branch,
96
+ "session_name": self.session_name,
97
+ "mode": self.mode,
98
+ "reason": self.resolver_result.reason,
99
+ }
100
+
101
+
102
+ def validate_and_resolve_workspace(
103
+ workspace: str | None,
104
+ *,
105
+ no_interactive: bool = False,
106
+ allow_suspicious: bool = False,
107
+ json_mode: bool = False,
108
+ ) -> Path | None:
109
+ """
110
+ Validate workspace path and handle platform-specific warnings.
111
+
112
+ Args:
113
+ workspace: Workspace path string.
114
+ no_interactive: If True, fail fast instead of prompting.
115
+ allow_suspicious: If True, allow suspicious workspaces in non-interactive mode.
116
+ json_mode: If True, output is JSON (suppress Rich panels).
117
+
118
+ Raises:
119
+ WorkspaceNotFoundError: If workspace path doesn't exist.
120
+ UsageError: If workspace is suspicious in non-interactive mode without --allow-suspicious-workspace.
121
+ typer.Exit: If user declines to continue after warnings.
122
+ """
123
+ from ...core.errors import UsageError
124
+ from ...services.workspace.suspicious import get_suspicious_reason, is_suspicious_directory
125
+
126
+ if workspace is None:
127
+ return None
128
+
129
+ workspace_path = Path(workspace).expanduser().resolve()
130
+
131
+ if not workspace_path.exists():
132
+ raise WorkspaceNotFoundError(path=str(workspace_path))
133
+
134
+ # Check for suspicious workspace (home, /tmp, system directories)
135
+ if is_suspicious_directory(workspace_path):
136
+ reason = get_suspicious_reason(workspace_path) or "Suspicious directory"
137
+
138
+ # If --allow-suspicious-workspace is set, skip confirmation entirely
139
+ if allow_suspicious:
140
+ print_human(
141
+ f"[yellow]Warning:[/yellow] {reason}",
142
+ file=sys.stderr,
143
+ highlight=False,
144
+ )
145
+ elif is_interactive_allowed(json_mode=json_mode, no_interactive_flag=no_interactive):
146
+ # Interactive mode: warn but allow user to continue
147
+ console.print()
148
+ console.print(
149
+ create_warning_panel(
150
+ "Suspicious Workspace",
151
+ reason,
152
+ "Consider using a project-specific directory instead.",
153
+ )
154
+ )
155
+ console.print()
156
+ if not Confirm.ask("[cyan]Continue anyway?[/cyan]", default=True):
157
+ console.print("[dim]Cancelled.[/dim]")
158
+ raise typer.Exit(EXIT_CANCELLED)
159
+ else:
160
+ # Non-interactive mode without flag: block
161
+ raise UsageError(
162
+ user_message=f"Refusing to start in suspicious directory: {workspace_path}\n → {reason}",
163
+ suggested_action=(
164
+ "Either:\n"
165
+ f" • Run: scc start --allow-suspicious-workspace {workspace_path}\n"
166
+ " • Run: scc start --interactive (to choose a different workspace)\n"
167
+ " • Run from a project directory inside a git repository"
168
+ ),
169
+ )
170
+
171
+ # WSL2 performance warning
172
+ if platform_module.is_wsl2():
173
+ is_optimal, warning = platform_module.check_path_performance(workspace_path)
174
+ if not is_optimal and warning:
175
+ print_human(
176
+ "[yellow]Warning:[/yellow] Workspace is on the Windows filesystem."
177
+ " Performance may be slow.",
178
+ file=sys.stderr,
179
+ highlight=False,
180
+ )
181
+ if is_interactive_allowed(no_interactive_flag=no_interactive):
182
+ console.print()
183
+ console.print(
184
+ create_warning_panel(
185
+ "Performance Warning",
186
+ "Your workspace is on the Windows filesystem.",
187
+ "For better performance, move to ~/projects inside WSL.",
188
+ )
189
+ )
190
+ console.print()
191
+ if not Confirm.ask("[cyan]Continue anyway?[/cyan]", default=True):
192
+ console.print("[dim]Cancelled.[/dim]")
193
+ raise typer.Exit(EXIT_CANCELLED)
194
+
195
+ return workspace_path
196
+
197
+
198
+ def prepare_workspace(
199
+ workspace_path: Path | None,
200
+ worktree_name: str | None,
201
+ install_deps: bool,
202
+ ) -> Path | None:
203
+ """
204
+ Prepare workspace: create worktree, install deps, check git safety.
205
+
206
+ Returns:
207
+ The (possibly updated) workspace path after worktree creation.
208
+ """
209
+ if workspace_path is None:
210
+ return None
211
+
212
+ # Handle worktree creation
213
+ if worktree_name:
214
+ workspace_path = git.create_worktree(workspace_path, worktree_name)
215
+ console.print(
216
+ create_success_panel(
217
+ "Worktree Created",
218
+ {
219
+ "Path": str(workspace_path),
220
+ "Branch": f"{WORKTREE_BRANCH_PREFIX}{worktree_name}",
221
+ },
222
+ )
223
+ )
224
+
225
+ # Install dependencies if requested
226
+ if install_deps:
227
+ with Status(
228
+ "[cyan]Installing dependencies...[/cyan]", console=console, spinner=Spinners.SETUP
229
+ ):
230
+ success = deps.auto_install_dependencies(workspace_path)
231
+ if success:
232
+ console.print(f"[green]{Indicators.get('PASS')} Dependencies installed[/green]")
233
+ else:
234
+ console.print("[yellow]⚠ Could not detect package manager or install failed[/yellow]")
235
+
236
+ # Check git safety (handles protected branch warnings)
237
+ if workspace_path.exists():
238
+ if not git.check_branch_safety(workspace_path, console):
239
+ console.print("[dim]Cancelled.[/dim]")
240
+ raise typer.Exit(EXIT_CANCELLED)
241
+
242
+ return workspace_path
243
+
244
+
245
+ def resolve_workspace_team(
246
+ workspace_path: Path | None,
247
+ team: str | None,
248
+ cfg: dict[str, Any],
249
+ *,
250
+ json_mode: bool = False,
251
+ standalone: bool = False,
252
+ no_interactive: bool = False,
253
+ ) -> str | None:
254
+ """Resolve team selection with proper priority.
255
+
256
+ Resolution priority:
257
+ 1. Explicit --team flag (if provided)
258
+ 2. selected_profile (explicit user choice via `scc team switch`)
259
+ 3. Workspace-pinned team (auto-saved from previous session)
260
+
261
+ In interactive mode, prompts user when pinned team differs from selected profile.
262
+ In non-interactive mode, prefers selected_profile (explicit user action).
263
+ """
264
+ if standalone or workspace_path is None:
265
+ return team
266
+
267
+ if team:
268
+ return team
269
+
270
+ pinned_team = config.get_workspace_team_from_config(cfg, workspace_path)
271
+ selected_profile: str | None = cfg.get("selected_profile")
272
+
273
+ if pinned_team and selected_profile and pinned_team != selected_profile:
274
+ if is_interactive_allowed(json_mode=json_mode, no_interactive_flag=no_interactive):
275
+ # Default to selected_profile (explicit user choice) for consistency
276
+ # with non-interactive mode behavior
277
+ message = (
278
+ f"[yellow]Note:[/yellow] This workspace was last used with team "
279
+ f"'[cyan]{pinned_team}[/cyan]', but your current profile is "
280
+ f"'[cyan]{selected_profile}[/cyan]'.\n"
281
+ f"Use workspace's previous team '{pinned_team}' instead?"
282
+ )
283
+ if Confirm.ask(message, default=False):
284
+ return pinned_team
285
+ return selected_profile
286
+
287
+ # Non-interactive: prefer selected_profile (explicit user choice via `scc team switch`)
288
+ # over workspace pinning (auto-saved from previous session)
289
+ if not json_mode:
290
+ print_human(
291
+ "[yellow]Notice:[/yellow] "
292
+ f"Workspace was last used with team '{pinned_team}', "
293
+ f"but current profile is '{selected_profile}'. Using '{selected_profile}'.",
294
+ file=sys.stderr,
295
+ highlight=False,
296
+ )
297
+ return selected_profile
298
+
299
+ if pinned_team:
300
+ return pinned_team
301
+
302
+ return selected_profile
303
+
304
+
305
+ def resolve_mount_and_branch(
306
+ workspace_path: Path | None,
307
+ *,
308
+ json_mode: bool = False,
309
+ ) -> tuple[Path | None, str | None]:
310
+ """
311
+ Resolve mount path for worktrees and get current branch.
312
+
313
+ For worktrees, expands mount scope to include main repo.
314
+ Returns (mount_path, current_branch).
315
+ """
316
+ if workspace_path is None:
317
+ return None, None
318
+
319
+ # Get current branch
320
+ current_branch = None
321
+ try:
322
+ current_branch = git.get_current_branch(workspace_path)
323
+ except (NotAGitRepoError, OSError):
324
+ pass
325
+
326
+ # Handle worktree mounting
327
+ mount_path, is_expanded = git.get_workspace_mount_path(workspace_path)
328
+ if is_expanded and not json_mode:
329
+ console.print()
330
+ console.print(
331
+ create_info_panel(
332
+ "Worktree Detected",
333
+ f"Mounting parent directory for worktree support:\n{mount_path}",
334
+ "Both worktree and main repo will be accessible",
335
+ )
336
+ )
337
+ console.print()
338
+
339
+ return mount_path, current_branch
@@ -0,0 +1,49 @@
1
+ """
2
+ Org package - organization configuration management commands.
3
+
4
+ This package contains the decomposed org functionality:
5
+ - app.py: Typer app definitions and command wiring
6
+ - validate_cmd.py: Schema and semantic validation
7
+ - update_cmd.py: Organization and team config refresh
8
+ - schema_cmd.py: Print bundled schema
9
+ - status_cmd.py: Current organization status
10
+ - import_cmd.py: Import organization config from URL
11
+ - init_cmd.py: Generate config skeleton from templates
12
+ - _builders.py: Pure builder functions
13
+
14
+ Public API re-exports for backward compatibility.
15
+ """
16
+
17
+ # Re-export pure builders for testing
18
+ from ._builders import (
19
+ build_import_preview_data,
20
+ build_status_data,
21
+ build_update_data,
22
+ build_validation_data,
23
+ check_semantic_errors,
24
+ )
25
+ from .app import org_app
26
+ from .import_cmd import org_import_cmd
27
+ from .init_cmd import org_init_cmd
28
+ from .schema_cmd import org_schema_cmd
29
+ from .status_cmd import org_status_cmd
30
+ from .update_cmd import org_update_cmd
31
+ from .validate_cmd import org_validate_cmd
32
+
33
+ __all__ = [
34
+ # Typer app
35
+ "org_app",
36
+ # Commands
37
+ "org_validate_cmd",
38
+ "org_update_cmd",
39
+ "org_schema_cmd",
40
+ "org_status_cmd",
41
+ "org_import_cmd",
42
+ "org_init_cmd",
43
+ # Pure builders
44
+ "build_validation_data",
45
+ "check_semantic_errors",
46
+ "build_import_preview_data",
47
+ "build_status_data",
48
+ "build_update_data",
49
+ ]
@@ -0,0 +1,264 @@
1
+ """Pure builder functions for org commands.
2
+
3
+ These functions build data structures for JSON output and display.
4
+ They have no side effects and are ideal for unit testing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ pass
13
+
14
+
15
+ def build_validation_data(
16
+ source: str,
17
+ schema_errors: list[str],
18
+ semantic_errors: list[str],
19
+ schema_version: str,
20
+ ) -> dict[str, Any]:
21
+ """Build validation result data for JSON output.
22
+
23
+ Args:
24
+ source: Path or URL of validated config
25
+ schema_errors: List of JSON schema validation errors
26
+ semantic_errors: List of semantic validation errors
27
+ schema_version: Schema version used for validation
28
+
29
+ Returns:
30
+ Dictionary with validation results
31
+ """
32
+ is_valid = len(schema_errors) == 0 and len(semantic_errors) == 0
33
+ return {
34
+ "source": source,
35
+ "schema_version": schema_version,
36
+ "valid": is_valid,
37
+ "schema_errors": schema_errors,
38
+ "semantic_errors": semantic_errors,
39
+ }
40
+
41
+
42
+ def check_semantic_errors(config: dict[str, Any]) -> list[str]:
43
+ """Check for semantic errors beyond JSON schema validation.
44
+
45
+ Args:
46
+ config: Parsed organization config
47
+
48
+ Returns:
49
+ List of semantic error messages
50
+ """
51
+ errors: list[str] = []
52
+ org = config.get("organization", {})
53
+
54
+ # Profiles are at TOP LEVEL of config as a DICT (not under "organization")
55
+ # Dict keys are unique, so no duplicate name checking needed
56
+ profiles = config.get("profiles", {})
57
+ profile_names: list[str] = list(profiles.keys())
58
+
59
+ # Check if default_profile references existing profile
60
+ default_profile = org.get("default_profile")
61
+ if default_profile and default_profile not in profile_names:
62
+ errors.append(f"default_profile '{default_profile}' references non-existent profile")
63
+
64
+ return errors
65
+
66
+
67
+ def build_import_preview_data(
68
+ source: str,
69
+ resolved_url: str,
70
+ config: dict[str, Any],
71
+ validation_errors: list[str],
72
+ ) -> dict[str, Any]:
73
+ """Build import preview data for display and JSON output.
74
+
75
+ Pure function that assembles preview information for an organization config
76
+ before it is imported.
77
+
78
+ Args:
79
+ source: Original source string (URL or shorthand like github:org/repo)
80
+ resolved_url: Resolved URL after shorthand expansion
81
+ config: Parsed organization config dict
82
+ validation_errors: List of validation error messages
83
+
84
+ Returns:
85
+ Dictionary with preview information including org details and validation status
86
+ """
87
+ org_data = config.get("organization", {})
88
+ profiles_dict = config.get("profiles", {})
89
+
90
+ return {
91
+ "source": source,
92
+ "resolved_url": resolved_url,
93
+ "organization": {
94
+ "name": org_data.get("name", ""),
95
+ "id": org_data.get("id", ""),
96
+ "contact": org_data.get("contact", ""),
97
+ },
98
+ "valid": len(validation_errors) == 0,
99
+ "validation_errors": validation_errors,
100
+ "available_profiles": list(profiles_dict.keys()),
101
+ "schema_version": config.get("schema_version", ""),
102
+ "min_cli_version": config.get("min_cli_version", ""),
103
+ }
104
+
105
+
106
+ def build_status_data(
107
+ user_config: dict[str, Any],
108
+ org_config: dict[str, Any] | None,
109
+ cache_meta: dict[str, Any] | None,
110
+ ) -> dict[str, Any]:
111
+ """Build status data for JSON output and display.
112
+
113
+ Pure function that assembles status information from various sources.
114
+
115
+ Args:
116
+ user_config: User configuration dict
117
+ org_config: Cached organization config (may be None)
118
+ cache_meta: Cache metadata dict (may be None)
119
+
120
+ Returns:
121
+ Dictionary with complete status information
122
+ """
123
+ # Import here to avoid circular imports at module level
124
+ from ...remote import is_cache_valid
125
+ from ...validate import check_version_compatibility
126
+
127
+ # Determine mode
128
+ is_standalone = user_config.get("standalone", False) or not user_config.get(
129
+ "organization_source"
130
+ )
131
+
132
+ if is_standalone:
133
+ return {
134
+ "mode": "standalone",
135
+ "organization": None,
136
+ "cache": None,
137
+ "version_compatibility": None,
138
+ "selected_profile": None,
139
+ "available_profiles": [],
140
+ }
141
+
142
+ # Organization connected mode
143
+ org_source = user_config.get("organization_source", {})
144
+ source_url = org_source.get("url", "")
145
+
146
+ # Organization info
147
+ org_info: dict[str, Any] | None = None
148
+ available_profiles: list[str] = []
149
+ if org_config:
150
+ org_data = org_config.get("organization", {})
151
+ org_info = {
152
+ "name": org_data.get("name", "unknown"),
153
+ "id": org_data.get("id", ""),
154
+ "contact": org_data.get("contact", ""),
155
+ "source_url": source_url,
156
+ }
157
+ # Extract available profiles
158
+ profiles_dict = org_config.get("profiles", {})
159
+ available_profiles = list(profiles_dict.keys())
160
+ else:
161
+ org_info = {
162
+ "name": None,
163
+ "source_url": source_url,
164
+ }
165
+
166
+ # Cache status
167
+ cache_info: dict[str, Any] | None = None
168
+ if cache_meta:
169
+ org_cache = cache_meta.get("org_config", {})
170
+ cache_info = {
171
+ "fetched_at": org_cache.get("fetched_at"),
172
+ "expires_at": org_cache.get("expires_at"),
173
+ "etag": org_cache.get("etag"),
174
+ "valid": is_cache_valid(cache_meta),
175
+ }
176
+
177
+ # Version compatibility
178
+ version_compat: dict[str, Any] | None = None
179
+ if org_config:
180
+ compat = check_version_compatibility(org_config)
181
+ version_compat = {
182
+ "compatible": compat.compatible,
183
+ "blocking_error": compat.blocking_error,
184
+ "warnings": compat.warnings,
185
+ "schema_version": compat.schema_version,
186
+ "min_cli_version": compat.min_cli_version,
187
+ "current_cli_version": compat.current_cli_version,
188
+ }
189
+
190
+ return {
191
+ "mode": "organization",
192
+ "organization": org_info,
193
+ "cache": cache_info,
194
+ "version_compatibility": version_compat,
195
+ "selected_profile": user_config.get("selected_profile"),
196
+ "available_profiles": available_profiles,
197
+ }
198
+
199
+
200
+ def build_update_data(
201
+ org_config: dict[str, Any] | None,
202
+ team_results: list[dict[str, Any]] | None = None,
203
+ ) -> dict[str, Any]:
204
+ """Build update result data for JSON output.
205
+
206
+ Pure function that assembles update result information.
207
+
208
+ Args:
209
+ org_config: Updated organization config (may be None on failure)
210
+ team_results: List of team update results (optional)
211
+
212
+ Returns:
213
+ Dictionary with update results including org and team info
214
+ """
215
+ result: dict[str, Any] = {
216
+ "org_updated": org_config is not None,
217
+ }
218
+
219
+ if org_config:
220
+ org_data = org_config.get("organization", {})
221
+ result["organization"] = {
222
+ "name": org_data.get("name", ""),
223
+ "id": org_data.get("id", ""),
224
+ }
225
+ result["schema_version"] = org_config.get("schema_version", "")
226
+
227
+ if team_results is not None:
228
+ result["teams_updated"] = team_results
229
+ result["teams_success_count"] = sum(1 for t in team_results if t.get("success"))
230
+ result["teams_failed_count"] = sum(1 for t in team_results if not t.get("success"))
231
+
232
+ return result
233
+
234
+
235
+ def _parse_config_source(source_dict: dict[str, Any]) -> Any:
236
+ """Parse a config_source dict into the appropriate ConfigSource type.
237
+
238
+ Handles discriminated union parsing for github, git, url sources.
239
+
240
+ Args:
241
+ source_dict: Raw config_source dict from org config
242
+
243
+ Returns:
244
+ ConfigSource object (ConfigSourceGitHub, ConfigSourceGit, or ConfigSourceURL)
245
+ """
246
+ # Import here to avoid circular imports
247
+ from ...marketplace.schema import (
248
+ ConfigSourceGit,
249
+ ConfigSourceGitHub,
250
+ ConfigSourceURL,
251
+ )
252
+
253
+ if "github" in source_dict:
254
+ github_data = source_dict["github"]
255
+ # Add source discriminator for Pydantic model
256
+ return ConfigSourceGitHub(source="github", **github_data)
257
+ elif "git" in source_dict:
258
+ git_data = source_dict["git"]
259
+ return ConfigSourceGit(source="git", **git_data)
260
+ elif "url" in source_dict:
261
+ url_data = source_dict["url"]
262
+ return ConfigSourceURL(source="url", **url_data)
263
+ else:
264
+ raise ValueError(f"Unknown config_source type: {list(source_dict.keys())}")
@@ -0,0 +1,41 @@
1
+ """
2
+ Org package - Typer app definitions and command wiring.
3
+
4
+ This module contains the Typer app definitions and wires commands from:
5
+ - validate_cmd.py: Schema and semantic validation
6
+ - update_cmd.py: Organization and team config refresh
7
+ - schema_cmd.py: Print bundled schema
8
+ - status_cmd.py: Current organization status
9
+ - import_cmd.py: Import organization config from URL
10
+ - init_cmd.py: Generate config skeleton from templates
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import typer
16
+
17
+ from .import_cmd import org_import_cmd
18
+ from .init_cmd import org_init_cmd
19
+ from .schema_cmd import org_schema_cmd
20
+ from .status_cmd import org_status_cmd
21
+ from .update_cmd import org_update_cmd
22
+ from .validate_cmd import org_validate_cmd
23
+
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ # Org App
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ org_app = typer.Typer(
29
+ name="org",
30
+ help="Organization configuration management and validation.",
31
+ no_args_is_help=True,
32
+ context_settings={"help_option_names": ["-h", "--help"]},
33
+ )
34
+
35
+ # Wire org commands
36
+ org_app.command("validate")(org_validate_cmd)
37
+ org_app.command("update")(org_update_cmd)
38
+ org_app.command("schema")(org_schema_cmd)
39
+ org_app.command("status")(org_status_cmd)
40
+ org_app.command("import")(org_import_cmd)
41
+ org_app.command("init")(org_init_cmd)