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,182 @@
1
+ """Environment health checks for doctor module.
2
+
3
+ Checks for Git, Docker, WSL2, and workspace path requirements.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from ..types import CheckResult
12
+
13
+
14
+ def check_git() -> CheckResult:
15
+ """Check if Git is installed and accessible."""
16
+ from ... import git as git_module
17
+
18
+ if not git_module.check_git_installed():
19
+ return CheckResult(
20
+ name="Git",
21
+ passed=False,
22
+ message="Git is not installed or not in PATH",
23
+ fix_hint="Install Git from https://git-scm.com/downloads",
24
+ fix_url="https://git-scm.com/downloads",
25
+ severity="error",
26
+ )
27
+
28
+ version = git_module.get_git_version()
29
+ return CheckResult(
30
+ name="Git",
31
+ passed=True,
32
+ message="Git is installed and accessible",
33
+ version=version,
34
+ )
35
+
36
+
37
+ def check_docker() -> CheckResult:
38
+ """Check if Docker is installed and running."""
39
+ from ... import docker as docker_module
40
+
41
+ version = docker_module.get_docker_version()
42
+
43
+ if version is None:
44
+ return CheckResult(
45
+ name="Docker",
46
+ passed=False,
47
+ message="Docker is not installed or not running",
48
+ fix_hint="Install Docker Desktop from https://docker.com/products/docker-desktop",
49
+ fix_url="https://docker.com/products/docker-desktop",
50
+ severity="error",
51
+ )
52
+
53
+ # Parse and check minimum version
54
+ current = docker_module._parse_version(version)
55
+ required = docker_module._parse_version(docker_module.MIN_DOCKER_VERSION)
56
+
57
+ if current < required:
58
+ return CheckResult(
59
+ name="Docker",
60
+ passed=False,
61
+ message=f"Docker version {'.'.join(map(str, current))} is below minimum {docker_module.MIN_DOCKER_VERSION}",
62
+ version=version,
63
+ fix_hint="Update Docker Desktop to the latest version",
64
+ fix_url="https://docker.com/products/docker-desktop",
65
+ severity="error",
66
+ )
67
+
68
+ return CheckResult(
69
+ name="Docker",
70
+ passed=True,
71
+ message="Docker is installed and meets version requirements",
72
+ version=version,
73
+ )
74
+
75
+
76
+ def check_docker_sandbox() -> CheckResult:
77
+ """Check if Docker sandbox feature is available."""
78
+ from ... import docker as docker_module
79
+
80
+ if not docker_module.check_docker_sandbox():
81
+ return CheckResult(
82
+ name="Docker Sandbox",
83
+ passed=False,
84
+ message="Docker sandbox feature is not available",
85
+ fix_hint=f"Requires Docker Desktop {docker_module.MIN_DOCKER_VERSION}+ with sandbox feature enabled",
86
+ fix_url="https://docs.docker.com/desktop/features/sandbox/",
87
+ severity="error",
88
+ )
89
+
90
+ return CheckResult(
91
+ name="Docker Sandbox",
92
+ passed=True,
93
+ message="Docker sandbox feature is available",
94
+ )
95
+
96
+
97
+ def check_docker_running() -> CheckResult:
98
+ """Check if Docker daemon is running."""
99
+ try:
100
+ result = subprocess.run(
101
+ ["docker", "info"],
102
+ capture_output=True,
103
+ timeout=10,
104
+ )
105
+ if result.returncode == 0:
106
+ return CheckResult(
107
+ name="Docker Daemon",
108
+ passed=True,
109
+ message="Docker daemon is running",
110
+ )
111
+ else:
112
+ return CheckResult(
113
+ name="Docker Daemon",
114
+ passed=False,
115
+ message="Docker daemon is not running",
116
+ fix_hint="Start Docker Desktop or run 'sudo systemctl start docker'",
117
+ severity="error",
118
+ )
119
+ except (subprocess.TimeoutExpired, FileNotFoundError):
120
+ return CheckResult(
121
+ name="Docker Daemon",
122
+ passed=False,
123
+ message="Could not connect to Docker daemon",
124
+ fix_hint="Ensure Docker Desktop is running",
125
+ severity="error",
126
+ )
127
+
128
+
129
+ def check_wsl2() -> tuple[CheckResult, bool]:
130
+ """Check WSL2 environment and return (result, is_wsl2)."""
131
+ from ... import platform as platform_module
132
+
133
+ is_wsl2 = platform_module.is_wsl2()
134
+
135
+ if is_wsl2:
136
+ return (
137
+ CheckResult(
138
+ name="WSL2 Environment",
139
+ passed=True,
140
+ message="Running in WSL2 (recommended for Windows)",
141
+ severity="info",
142
+ ),
143
+ True,
144
+ )
145
+
146
+ return (
147
+ CheckResult(
148
+ name="WSL2 Environment",
149
+ passed=True,
150
+ message="Not running in WSL2",
151
+ severity="info",
152
+ ),
153
+ False,
154
+ )
155
+
156
+
157
+ def check_workspace_path(workspace: Path | None = None) -> CheckResult:
158
+ """Check if workspace path is optimal (not on Windows mount in WSL2)."""
159
+ from ... import platform as platform_module
160
+
161
+ if workspace is None:
162
+ return CheckResult(
163
+ name="Workspace Path",
164
+ passed=True,
165
+ message="No workspace specified",
166
+ severity="info",
167
+ )
168
+
169
+ if platform_module.is_wsl2() and platform_module.is_windows_mount_path(workspace):
170
+ return CheckResult(
171
+ name="Workspace Path",
172
+ passed=False,
173
+ message=f"Workspace is on Windows filesystem: {workspace}",
174
+ fix_hint="Move project to ~/projects inside WSL for better performance",
175
+ severity="warning",
176
+ )
177
+
178
+ return CheckResult(
179
+ name="Workspace Path",
180
+ passed=True,
181
+ message=f"Workspace path is optimal: {workspace}",
182
+ )
@@ -0,0 +1,157 @@
1
+ """JSON validation helpers for doctor module.
2
+
3
+ Provides enhanced JSON validation with code frames and helpful hints.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from ...theme import Indicators
12
+ from ..types import JsonValidationResult
13
+
14
+
15
+ def validate_json_file(file_path: Path) -> JsonValidationResult:
16
+ """
17
+ Validate a JSON file and extract detailed error information.
18
+
19
+ Args:
20
+ file_path: Path to the JSON file to validate
21
+
22
+ Returns:
23
+ JsonValidationResult with validation status and error details
24
+ """
25
+ if not file_path.exists():
26
+ return JsonValidationResult(valid=True, file_path=file_path)
27
+
28
+ try:
29
+ content = file_path.read_text(encoding="utf-8")
30
+ json.loads(content)
31
+ return JsonValidationResult(valid=True, file_path=file_path)
32
+ except json.JSONDecodeError as e:
33
+ code_frame = format_code_frame(content, e.lineno, e.colno, file_path)
34
+ return JsonValidationResult(
35
+ valid=False,
36
+ error_message=e.msg,
37
+ line=e.lineno,
38
+ column=e.colno,
39
+ file_path=file_path,
40
+ code_frame=code_frame,
41
+ )
42
+ except OSError as e:
43
+ return JsonValidationResult(
44
+ valid=False,
45
+ error_message=f"Cannot read file: {e}",
46
+ file_path=file_path,
47
+ )
48
+
49
+
50
+ def format_code_frame(
51
+ content: str,
52
+ error_line: int,
53
+ error_col: int,
54
+ file_path: Path,
55
+ context_lines: int = 2,
56
+ ) -> str:
57
+ """
58
+ Format a code frame showing the error location with context.
59
+
60
+ Creates a visual representation like:
61
+ 10 │ "selected_profile": "dev-team",
62
+ 11 │ "preferences": {
63
+ → 12 │ "auto_update": true
64
+ │ ^
65
+ 13 │ "show_tips": false
66
+ 14 │ }
67
+
68
+ Args:
69
+ content: The file content
70
+ error_line: Line number where error occurred (1-indexed)
71
+ error_col: Column number where error occurred (1-indexed)
72
+ file_path: Path to the file (for display)
73
+ context_lines: Number of lines to show before/after error
74
+
75
+ Returns:
76
+ Formatted code frame string with Rich markup
77
+ """
78
+ lines = content.splitlines()
79
+ total_lines = len(lines)
80
+
81
+ # Calculate line range to display
82
+ start_line = max(1, error_line - context_lines)
83
+ end_line = min(total_lines, error_line + context_lines)
84
+
85
+ # Calculate padding for line numbers
86
+ max_line_num = end_line
87
+ line_num_width = len(str(max_line_num))
88
+
89
+ frame_lines = []
90
+
91
+ # Add file path header
92
+ frame_lines.append(f"[dim]File: {file_path}[/dim]")
93
+ frame_lines.append("")
94
+
95
+ for line_num in range(start_line, end_line + 1):
96
+ line_content = lines[line_num - 1] if line_num <= total_lines else ""
97
+
98
+ # Truncate long lines to prevent secret leakage (keep first 80 chars)
99
+ if len(line_content) > 80:
100
+ line_content = line_content[:77] + "..."
101
+
102
+ if line_num == error_line:
103
+ # Error line with arrow indicator
104
+ frame_lines.append(
105
+ f"[bold red]{Indicators.get('ARROW')} {line_num:>{line_num_width}} │[/bold red] "
106
+ f"[white]{_escape_rich(line_content)}[/white]"
107
+ )
108
+ # Caret line pointing to error column
109
+ caret_padding = " " * (line_num_width + 4 + max(0, error_col - 1))
110
+ frame_lines.append(f"[bold red]{caret_padding}^[/bold red]")
111
+ else:
112
+ # Context line
113
+ frame_lines.append(
114
+ f"[dim] {line_num:>{line_num_width}} │[/dim] "
115
+ f"[dim]{_escape_rich(line_content)}[/dim]"
116
+ )
117
+
118
+ return "\n".join(frame_lines)
119
+
120
+
121
+ def _escape_rich(text: str) -> str:
122
+ """Escape Rich markup characters in text."""
123
+ return text.replace("[", "\\[").replace("]", "\\]")
124
+
125
+
126
+ def get_json_error_hints(error_message: str) -> list[str]:
127
+ """
128
+ Get helpful hints based on common JSON error messages.
129
+
130
+ Args:
131
+ error_message: The JSON decode error message
132
+
133
+ Returns:
134
+ List of helpful hints for fixing the error
135
+ """
136
+ hints = []
137
+ error_lower = error_message.lower()
138
+
139
+ if "expecting" in error_lower and "," in error_lower:
140
+ hints.append("Missing comma between values")
141
+ elif "expecting property name" in error_lower:
142
+ hints.append("Trailing comma after last item (not allowed in JSON)")
143
+ hints.append("Missing closing brace or bracket")
144
+ elif "expecting value" in error_lower:
145
+ hints.append("Missing value after colon or comma")
146
+ hints.append("Empty array or object element")
147
+ elif "expecting ':'" in error_lower:
148
+ hints.append("Missing colon after property name")
149
+ elif "unterminated string" in error_lower or "invalid \\escape" in error_lower:
150
+ hints.append("Unclosed string quote or invalid escape sequence")
151
+ elif "extra data" in error_lower:
152
+ hints.append("Multiple root objects (JSON must have single root)")
153
+
154
+ if not hints:
155
+ hints.append("Check JSON syntax near the indicated line")
156
+
157
+ return hints
@@ -0,0 +1,264 @@
1
+ """Organization and marketplace health checks for doctor module.
2
+
3
+ Checks for org config reachability, marketplace auth, and credential injection.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from typing import Any, cast
10
+
11
+ from ..types import CheckResult
12
+
13
+
14
+ def load_cached_org_config() -> dict[str, Any] | None:
15
+ """Load cached organization config from cache directory.
16
+
17
+ Returns:
18
+ Cached org config dict if valid, None otherwise.
19
+ """
20
+ from ... import config
21
+
22
+ cache_file = config.CACHE_DIR / "org_config.json"
23
+
24
+ if not cache_file.exists():
25
+ return None
26
+
27
+ try:
28
+ content = cache_file.read_text()
29
+ return cast(dict[str, Any], json.loads(content))
30
+ except (json.JSONDecodeError, OSError):
31
+ return None
32
+
33
+
34
+ def check_org_config_reachable() -> CheckResult | None:
35
+ """Check if organization config URL is reachable.
36
+
37
+ Returns:
38
+ CheckResult if org config is configured, None for standalone mode.
39
+ """
40
+ from ... import config
41
+ from ...remote import fetch_org_config
42
+
43
+ user_config = config.load_user_config()
44
+
45
+ # Skip for standalone mode
46
+ if user_config.get("standalone"):
47
+ return None
48
+
49
+ # Skip if no org source configured
50
+ org_source = user_config.get("organization_source")
51
+ if not org_source:
52
+ return None
53
+
54
+ url = org_source.get("url")
55
+ if not url:
56
+ return None
57
+
58
+ auth = org_source.get("auth")
59
+
60
+ # Try to fetch org config
61
+ try:
62
+ org_config, etag, status = fetch_org_config(url, auth=auth, etag=None)
63
+ except Exception as e:
64
+ return CheckResult(
65
+ name="Org Config",
66
+ passed=False,
67
+ message=f"Failed to fetch org config: {e}",
68
+ fix_hint="Check network connection and URL",
69
+ severity="error",
70
+ )
71
+
72
+ if status == 401:
73
+ return CheckResult(
74
+ name="Org Config",
75
+ passed=False,
76
+ message=f"Authentication required (401) for {url}",
77
+ fix_hint="Configure auth with: scc setup",
78
+ severity="error",
79
+ )
80
+
81
+ if status == 403:
82
+ return CheckResult(
83
+ name="Org Config",
84
+ passed=False,
85
+ message=f"Access denied (403) for {url}",
86
+ fix_hint="Check your access permissions",
87
+ severity="error",
88
+ )
89
+
90
+ if status != 200 or org_config is None:
91
+ return CheckResult(
92
+ name="Org Config",
93
+ passed=False,
94
+ message=f"Failed to fetch org config (status: {status})",
95
+ fix_hint="Check URL and network connection",
96
+ severity="error",
97
+ )
98
+
99
+ org_name = org_config.get("organization", {}).get("name", "Unknown")
100
+ return CheckResult(
101
+ name="Org Config",
102
+ passed=True,
103
+ message=f"Connected to: {org_name}",
104
+ )
105
+
106
+
107
+ def check_marketplace_auth_available() -> CheckResult | None:
108
+ """Check if marketplace authentication token is available.
109
+
110
+ Returns:
111
+ CheckResult if marketplace is configured, None otherwise.
112
+ """
113
+ from ... import config
114
+ from ...remote import resolve_auth
115
+
116
+ user_config = config.load_user_config()
117
+ org_config = load_cached_org_config()
118
+
119
+ # Skip if no org config
120
+ if org_config is None:
121
+ return None
122
+
123
+ # Skip if no profile selected
124
+ profile_name = user_config.get("selected_profile")
125
+ if not profile_name:
126
+ return None
127
+
128
+ # Find the profile
129
+ profiles = org_config.get("profiles", {})
130
+ profile = profiles.get(profile_name)
131
+ if not profile:
132
+ return None
133
+
134
+ # Find the marketplace (dict-based schema)
135
+ marketplace_name = profile.get("marketplace")
136
+ marketplaces = org_config.get("marketplaces", {})
137
+ marketplace = marketplaces.get(marketplace_name)
138
+
139
+ if marketplace is None:
140
+ return CheckResult(
141
+ name="Marketplace Auth",
142
+ passed=False,
143
+ message=f"Marketplace '{marketplace_name}' not found in org config",
144
+ severity="error",
145
+ )
146
+
147
+ # Check auth requirement
148
+ auth_spec = marketplace.get("auth")
149
+
150
+ if auth_spec is None:
151
+ return CheckResult(
152
+ name="Marketplace Auth",
153
+ passed=True,
154
+ message="Public marketplace (no auth needed)",
155
+ )
156
+
157
+ # Try to resolve auth
158
+ try:
159
+ token = resolve_auth(auth_spec)
160
+ if token:
161
+ return CheckResult(
162
+ name="Marketplace Auth",
163
+ passed=True,
164
+ message=f"{auth_spec} is set",
165
+ )
166
+ else:
167
+ # Provide helpful hint based on auth type
168
+ if auth_spec.startswith("env:"):
169
+ var_name = auth_spec.split(":", 1)[1]
170
+ hint = f"Set with: export {var_name}=your-token"
171
+ else:
172
+ cmd = auth_spec.split(":", 1)[1] if ":" in auth_spec else auth_spec
173
+ hint = f"Run manually to debug: {cmd}"
174
+
175
+ return CheckResult(
176
+ name="Marketplace Auth",
177
+ passed=False,
178
+ message=f"{auth_spec} not set or invalid",
179
+ fix_hint=hint,
180
+ severity="error",
181
+ )
182
+ except Exception as e:
183
+ return CheckResult(
184
+ name="Marketplace Auth",
185
+ passed=False,
186
+ message=f"Auth resolution failed: {e}",
187
+ severity="error",
188
+ )
189
+
190
+
191
+ def check_credential_injection() -> CheckResult | None:
192
+ """Check what credentials will be injected into Docker container.
193
+
194
+ Shows env var NAMES only, never values. Prevents confusion about
195
+ whether tokens are being passed to the container.
196
+
197
+ Returns:
198
+ CheckResult showing injection status, None if no profile.
199
+ """
200
+ from ... import config
201
+
202
+ user_config = config.load_user_config()
203
+ org_config = load_cached_org_config()
204
+
205
+ # Skip if no org config
206
+ if org_config is None:
207
+ return None
208
+
209
+ # Skip if no profile selected
210
+ profile_name = user_config.get("selected_profile")
211
+ if not profile_name:
212
+ return None
213
+
214
+ # Find the profile
215
+ profiles = org_config.get("profiles", {})
216
+ profile = profiles.get(profile_name)
217
+ if not profile:
218
+ return None
219
+
220
+ # Find the marketplace (dict-based schema)
221
+ marketplace_name = profile.get("marketplace")
222
+ marketplaces = org_config.get("marketplaces", {})
223
+ marketplace = marketplaces.get(marketplace_name)
224
+
225
+ if marketplace is None:
226
+ return None
227
+
228
+ # Check auth requirement
229
+ auth_spec = marketplace.get("auth")
230
+
231
+ if auth_spec is None:
232
+ return CheckResult(
233
+ name="Container Injection",
234
+ passed=True,
235
+ message="No credentials needed (public marketplace)",
236
+ )
237
+
238
+ # Determine what env vars will be injected
239
+ env_vars = []
240
+
241
+ if auth_spec.startswith("env:"):
242
+ var_name = auth_spec.split(":", 1)[1]
243
+ env_vars.append(var_name)
244
+
245
+ # Add standard vars based on marketplace type
246
+ marketplace_type = marketplace.get("type")
247
+ if marketplace_type == "gitlab" and var_name != "GITLAB_TOKEN":
248
+ env_vars.append("GITLAB_TOKEN")
249
+ elif marketplace_type == "github" and var_name != "GITHUB_TOKEN":
250
+ env_vars.append("GITHUB_TOKEN")
251
+
252
+ if env_vars:
253
+ env_list = ", ".join(env_vars)
254
+ return CheckResult(
255
+ name="Container Injection",
256
+ passed=True,
257
+ message=f"Will inject [{env_list}] into Docker env",
258
+ )
259
+ else:
260
+ return CheckResult(
261
+ name="Container Injection",
262
+ passed=True,
263
+ message="Command-based auth (resolved at runtime)",
264
+ )