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,166 @@
1
+ """Health check functions for the doctor module.
2
+
3
+ This package contains all check functions organized by category:
4
+ - JSON validation helpers (json_helpers.py)
5
+ - Environment checks (environment.py) - Git, Docker, WSL2, Workspace
6
+ - Git Worktree checks (worktree.py)
7
+ - Configuration checks (config.py)
8
+ - Organization & Marketplace checks (organization.py)
9
+ - Cache & State checks (cache.py)
10
+
11
+ All check functions return CheckResult or CheckResult | None.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from ..types import CheckResult
17
+
18
+ # Cache & State checks
19
+ from .cache import (
20
+ check_cache_readable,
21
+ check_cache_ttl_status,
22
+ check_exception_stores,
23
+ check_migration_status,
24
+ check_proxy_environment,
25
+ )
26
+
27
+ # Configuration checks
28
+ from .config import (
29
+ check_config_directory,
30
+ check_user_config_valid,
31
+ )
32
+
33
+ # Environment checks
34
+ from .environment import (
35
+ check_docker,
36
+ check_docker_running,
37
+ check_docker_sandbox,
38
+ check_git,
39
+ check_workspace_path,
40
+ check_wsl2,
41
+ )
42
+
43
+ # JSON validation helpers
44
+ from .json_helpers import (
45
+ _escape_rich,
46
+ format_code_frame,
47
+ get_json_error_hints,
48
+ validate_json_file,
49
+ )
50
+
51
+ # Organization & Marketplace checks
52
+ from .organization import (
53
+ check_credential_injection,
54
+ check_marketplace_auth_available,
55
+ check_org_config_reachable,
56
+ load_cached_org_config,
57
+ )
58
+
59
+ # Worktree checks
60
+ from .worktree import (
61
+ check_git_version_for_worktrees,
62
+ check_worktree_branch_conflicts,
63
+ check_worktree_health,
64
+ )
65
+
66
+
67
+ def run_all_checks() -> list[CheckResult]:
68
+ """Run all health checks and return list of results.
69
+
70
+ Includes both environment checks and organization/marketplace checks.
71
+
72
+ Returns:
73
+ List of all CheckResult objects (excluding None results).
74
+ """
75
+ results: list[CheckResult] = []
76
+
77
+ # Environment checks
78
+ results.append(check_git())
79
+ results.append(check_docker())
80
+ results.append(check_docker_sandbox())
81
+ results.append(check_docker_running())
82
+
83
+ wsl2_result, _ = check_wsl2()
84
+ results.append(wsl2_result)
85
+
86
+ results.append(check_config_directory())
87
+
88
+ # Git worktree checks (may return None if not in a git repo)
89
+ git_version_check = check_git_version_for_worktrees()
90
+ if git_version_check is not None:
91
+ results.append(git_version_check)
92
+
93
+ worktree_check = check_worktree_health()
94
+ if worktree_check is not None:
95
+ results.append(worktree_check)
96
+
97
+ branch_conflict_check = check_worktree_branch_conflicts()
98
+ if branch_conflict_check is not None:
99
+ results.append(branch_conflict_check)
100
+
101
+ # User config validation (JSON syntax check)
102
+ results.append(check_user_config_valid())
103
+
104
+ # Organization checks (may return None)
105
+ org_check = check_org_config_reachable()
106
+ if org_check is not None:
107
+ results.append(org_check)
108
+
109
+ auth_check = check_marketplace_auth_available()
110
+ if auth_check is not None:
111
+ results.append(auth_check)
112
+
113
+ injection_check = check_credential_injection()
114
+ if injection_check is not None:
115
+ results.append(injection_check)
116
+
117
+ # Cache checks
118
+ results.append(check_cache_readable())
119
+
120
+ ttl_check = check_cache_ttl_status()
121
+ if ttl_check is not None:
122
+ results.append(ttl_check)
123
+
124
+ # Migration check
125
+ results.append(check_migration_status())
126
+
127
+ # Exception stores check
128
+ results.append(check_exception_stores())
129
+
130
+ return results
131
+
132
+
133
+ __all__ = [
134
+ # JSON validation helpers
135
+ "validate_json_file",
136
+ "format_code_frame",
137
+ "_escape_rich",
138
+ "get_json_error_hints",
139
+ # Environment checks
140
+ "check_git",
141
+ "check_docker",
142
+ "check_docker_sandbox",
143
+ "check_docker_running",
144
+ "check_wsl2",
145
+ "check_workspace_path",
146
+ # Worktree checks
147
+ "check_worktree_health",
148
+ "check_git_version_for_worktrees",
149
+ "check_worktree_branch_conflicts",
150
+ # Config checks
151
+ "check_user_config_valid",
152
+ "check_config_directory",
153
+ # Organization checks
154
+ "load_cached_org_config",
155
+ "check_org_config_reachable",
156
+ "check_marketplace_auth_available",
157
+ "check_credential_injection",
158
+ # Cache & state checks
159
+ "check_cache_readable",
160
+ "check_cache_ttl_status",
161
+ "check_migration_status",
162
+ "check_exception_stores",
163
+ "check_proxy_environment",
164
+ # Orchestration
165
+ "run_all_checks",
166
+ ]
@@ -0,0 +1,314 @@
1
+ """Cache and state health checks for doctor module.
2
+
3
+ Checks for cache validity, TTL, migration status, exception stores, and proxy.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ from ..types import CheckResult
15
+ from .json_helpers import get_json_error_hints, validate_json_file
16
+
17
+
18
+ def check_cache_readable() -> CheckResult:
19
+ """Check if organization config cache is readable and valid.
20
+
21
+ Uses enhanced error display with code frames for JSON syntax errors.
22
+
23
+ Returns:
24
+ CheckResult with cache status.
25
+ """
26
+ from ... import config
27
+
28
+ cache_file = config.CACHE_DIR / "org_config.json"
29
+
30
+ if not cache_file.exists():
31
+ return CheckResult(
32
+ name="Org Cache",
33
+ passed=True,
34
+ message="No cache file (will fetch on first use)",
35
+ severity="info",
36
+ )
37
+
38
+ # Use the new validation helper for enhanced error display
39
+ result = validate_json_file(cache_file)
40
+
41
+ if result.valid:
42
+ try:
43
+ content = cache_file.read_text()
44
+ org_config = json.loads(content)
45
+
46
+ # Calculate fingerprint
47
+ fingerprint = hashlib.sha256(content.encode()).hexdigest()[:12]
48
+
49
+ org_name = org_config.get("organization", {}).get("name", "Unknown")
50
+ return CheckResult(
51
+ name="Org Cache",
52
+ passed=True,
53
+ message=f"Cache valid: {org_name} (fingerprint: {fingerprint})",
54
+ )
55
+ except (json.JSONDecodeError, OSError) as e:
56
+ return CheckResult(
57
+ name="Org Cache",
58
+ passed=False,
59
+ message=f"Cannot read cache file: {e}",
60
+ fix_hint="Run 'scc setup' to refresh organization config",
61
+ severity="error",
62
+ )
63
+
64
+ # Invalid JSON - build detailed error message
65
+ error_msg = "Cache file is corrupted (invalid JSON)"
66
+ if result.line is not None:
67
+ error_msg += f" at line {result.line}"
68
+ if result.column is not None:
69
+ error_msg += f", column {result.column}"
70
+
71
+ # Get helpful hints
72
+ hints = get_json_error_hints(result.error_message or "")
73
+ fix_hint = f"Error: {result.error_message}\n"
74
+ fix_hint += "Hints:\n"
75
+ for hint in hints:
76
+ fix_hint += f" • {hint}\n"
77
+ fix_hint += "Fix: Run 'scc setup' to refresh organization config"
78
+
79
+ return CheckResult(
80
+ name="Org Cache",
81
+ passed=False,
82
+ message=error_msg,
83
+ fix_hint=fix_hint,
84
+ severity="error",
85
+ code_frame=result.code_frame,
86
+ )
87
+
88
+
89
+ def check_cache_ttl_status() -> CheckResult | None:
90
+ """Check if cache is within TTL (time-to-live).
91
+
92
+ Returns:
93
+ CheckResult with TTL status, None if no cache metadata.
94
+ """
95
+ from ... import config
96
+
97
+ meta_file = config.CACHE_DIR / "cache_meta.json"
98
+
99
+ if not meta_file.exists():
100
+ return None
101
+
102
+ try:
103
+ content = meta_file.read_text()
104
+ meta = json.loads(content)
105
+ except (json.JSONDecodeError, OSError):
106
+ return CheckResult(
107
+ name="Cache TTL",
108
+ passed=False,
109
+ message="Cache metadata is corrupted",
110
+ fix_hint="Run 'scc setup' to refresh organization config",
111
+ severity="warning",
112
+ )
113
+
114
+ org_meta = meta.get("org_config", {})
115
+ expires_at_str = org_meta.get("expires_at")
116
+
117
+ if not expires_at_str:
118
+ return CheckResult(
119
+ name="Cache TTL",
120
+ passed=True,
121
+ message="No expiration set in cache",
122
+ severity="info",
123
+ )
124
+
125
+ try:
126
+ # Parse ISO format datetime
127
+ expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00"))
128
+ now = datetime.now(timezone.utc)
129
+
130
+ if now < expires_at:
131
+ remaining = expires_at - now
132
+ hours = remaining.total_seconds() / 3600
133
+ return CheckResult(
134
+ name="Cache TTL",
135
+ passed=True,
136
+ message=f"Cache valid for {hours:.1f} more hours",
137
+ )
138
+ else:
139
+ elapsed = now - expires_at
140
+ hours = elapsed.total_seconds() / 3600
141
+ return CheckResult(
142
+ name="Cache TTL",
143
+ passed=False,
144
+ message=f"Cache expired {hours:.1f} hours ago",
145
+ fix_hint="Run 'scc setup' to refresh organization config",
146
+ severity="warning",
147
+ )
148
+ except (ValueError, TypeError):
149
+ return CheckResult(
150
+ name="Cache TTL",
151
+ passed=False,
152
+ message="Invalid expiration date in cache metadata",
153
+ fix_hint="Run 'scc setup' to refresh organization config",
154
+ severity="warning",
155
+ )
156
+
157
+
158
+ def check_migration_status() -> CheckResult:
159
+ """Check if legacy configuration has been migrated.
160
+
161
+ Returns:
162
+ CheckResult with migration status.
163
+ """
164
+ from ... import config
165
+
166
+ legacy_dir = config.LEGACY_CONFIG_DIR
167
+ new_dir = config.CONFIG_DIR
168
+
169
+ # Both new and legacy exist - warn about cleanup
170
+ if legacy_dir.exists() and new_dir.exists():
171
+ return CheckResult(
172
+ name="Migration",
173
+ passed=False,
174
+ message=f"Legacy config still exists at {legacy_dir}",
175
+ fix_hint="You may delete the old directory manually",
176
+ severity="warning",
177
+ )
178
+
179
+ # Only legacy exists - needs migration
180
+ if legacy_dir.exists() and not new_dir.exists():
181
+ return CheckResult(
182
+ name="Migration",
183
+ passed=False,
184
+ message="Config migration needed",
185
+ fix_hint="Run any scc command to trigger automatic migration",
186
+ severity="warning",
187
+ )
188
+
189
+ # New config exists or fresh install
190
+ return CheckResult(
191
+ name="Migration",
192
+ passed=True,
193
+ message="No legacy configuration found",
194
+ )
195
+
196
+
197
+ def check_exception_stores() -> CheckResult:
198
+ """Check if exception stores are readable and valid.
199
+
200
+ Validates both user and repo exception stores:
201
+ - JSON parse errors
202
+ - Schema version compatibility
203
+ - Backup files from corruption recovery
204
+
205
+ Returns:
206
+ CheckResult with exception store status.
207
+ """
208
+ from ...stores.exception_store import RepoStore, UserStore
209
+
210
+ issues: list[str] = []
211
+ warnings: list[str] = []
212
+
213
+ # Check user store
214
+ user_store = UserStore()
215
+ user_path = user_store.path
216
+
217
+ if user_path.exists():
218
+ try:
219
+ user_file = user_store.read()
220
+ if user_file.schema_version > 1:
221
+ warnings.append(f"User store uses newer schema v{user_file.schema_version}")
222
+ except Exception as e:
223
+ issues.append(f"User store corrupt: {e}")
224
+
225
+ # Check for backup files indicating past corruption
226
+ backup_pattern = f"{user_path.name}.bak-*"
227
+ backup_dir = user_path.parent
228
+ backups = list(backup_dir.glob(backup_pattern))
229
+ if backups:
230
+ warnings.append(f"Found {len(backups)} user store backup(s)")
231
+
232
+ # Check repo store (if in a git repo)
233
+ try:
234
+ repo_store = RepoStore(Path.cwd())
235
+ repo_path = repo_store.path
236
+
237
+ if repo_path.exists():
238
+ try:
239
+ repo_file = repo_store.read()
240
+ if repo_file.schema_version > 1:
241
+ warnings.append(f"Repo store uses newer schema v{repo_file.schema_version}")
242
+ except Exception as e:
243
+ issues.append(f"Repo store corrupt: {e}")
244
+
245
+ # Check for backup files
246
+ backup_pattern = f"{repo_path.name}.bak-*"
247
+ backup_dir = repo_path.parent
248
+ backups = list(backup_dir.glob(backup_pattern))
249
+ if backups:
250
+ warnings.append(f"Found {len(backups)} repo store backup(s)")
251
+ except Exception:
252
+ # Not in a repo or repo store not accessible - that's fine
253
+ pass
254
+
255
+ # Build result
256
+ if issues:
257
+ return CheckResult(
258
+ name="Exception Stores",
259
+ passed=False,
260
+ message="; ".join(issues),
261
+ fix_hint="Run 'scc exceptions reset --user --yes' to reset corrupt stores",
262
+ severity="error",
263
+ )
264
+
265
+ if warnings:
266
+ return CheckResult(
267
+ name="Exception Stores",
268
+ passed=True,
269
+ message="; ".join(warnings),
270
+ fix_hint="Consider upgrading SCC or running 'scc exceptions cleanup'",
271
+ severity="warning",
272
+ )
273
+
274
+ return CheckResult(
275
+ name="Exception Stores",
276
+ passed=True,
277
+ message="Exception stores OK",
278
+ )
279
+
280
+
281
+ def check_proxy_environment() -> CheckResult:
282
+ """Check for proxy environment variables.
283
+
284
+ This is an informational check that detects common proxy configurations.
285
+ It never fails - just provides visibility into the environment.
286
+
287
+ Returns:
288
+ CheckResult with proxy environment info (always passes, severity=info).
289
+ """
290
+ proxy_vars = {
291
+ "HTTP_PROXY": os.environ.get("HTTP_PROXY"),
292
+ "http_proxy": os.environ.get("http_proxy"),
293
+ "HTTPS_PROXY": os.environ.get("HTTPS_PROXY"),
294
+ "https_proxy": os.environ.get("https_proxy"),
295
+ "NO_PROXY": os.environ.get("NO_PROXY"),
296
+ "no_proxy": os.environ.get("no_proxy"),
297
+ }
298
+
299
+ # Find which ones are set
300
+ configured = {k: v for k, v in proxy_vars.items() if v}
301
+
302
+ if configured:
303
+ # Summarize what's configured
304
+ proxy_names = ", ".join(configured.keys())
305
+ message = f"Proxy configured: {proxy_names}"
306
+ else:
307
+ message = "No proxy environment variables detected"
308
+
309
+ return CheckResult(
310
+ name="Proxy Environment",
311
+ passed=True,
312
+ message=message,
313
+ severity="info",
314
+ )
@@ -0,0 +1,107 @@
1
+ """Configuration health checks for doctor module.
2
+
3
+ Checks for user config validity and config directory accessibility.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ..types import CheckResult
9
+ from .json_helpers import get_json_error_hints, validate_json_file
10
+
11
+
12
+ def check_user_config_valid() -> CheckResult:
13
+ """Check if user configuration file is valid JSON.
14
+
15
+ Validates ~/.config/scc/config.json for JSON syntax errors
16
+ and provides helpful error messages with code frames.
17
+
18
+ Returns:
19
+ CheckResult with user config validation status.
20
+ """
21
+ from ... import config
22
+
23
+ config_file = config.CONFIG_FILE
24
+
25
+ if not config_file.exists():
26
+ return CheckResult(
27
+ name="User Config",
28
+ passed=True,
29
+ message="No user config file (using defaults)",
30
+ severity="info",
31
+ )
32
+
33
+ result = validate_json_file(config_file)
34
+
35
+ if result.valid:
36
+ return CheckResult(
37
+ name="User Config",
38
+ passed=True,
39
+ message=f"User config is valid JSON: {config_file}",
40
+ )
41
+
42
+ # Build error message with hints
43
+ error_msg = f"Invalid JSON in {config_file.name}"
44
+ if result.line is not None:
45
+ error_msg += f" at line {result.line}"
46
+ if result.column is not None:
47
+ error_msg += f", column {result.column}"
48
+
49
+ # Get helpful hints
50
+ hints = get_json_error_hints(result.error_message or "")
51
+ fix_hint = f"Error: {result.error_message}\n"
52
+ fix_hint += "Hints:\n"
53
+ for hint in hints:
54
+ fix_hint += f" • {hint}\n"
55
+ fix_hint += f"Edit with: $EDITOR {config_file}"
56
+
57
+ return CheckResult(
58
+ name="User Config",
59
+ passed=False,
60
+ message=error_msg,
61
+ fix_hint=fix_hint,
62
+ severity="error",
63
+ code_frame=result.code_frame,
64
+ )
65
+
66
+
67
+ def check_config_directory() -> CheckResult:
68
+ """Check if configuration directory exists and is writable."""
69
+ from ... import config
70
+
71
+ config_dir = config.CONFIG_DIR
72
+
73
+ if not config_dir.exists():
74
+ try:
75
+ config_dir.mkdir(parents=True, exist_ok=True)
76
+ return CheckResult(
77
+ name="Config Directory",
78
+ passed=True,
79
+ message=f"Created config directory: {config_dir}",
80
+ )
81
+ except PermissionError:
82
+ return CheckResult(
83
+ name="Config Directory",
84
+ passed=False,
85
+ message=f"Cannot create config directory: {config_dir}",
86
+ fix_hint="Check permissions on parent directory",
87
+ severity="error",
88
+ )
89
+
90
+ # Check if writable
91
+ test_file = config_dir / ".write_test"
92
+ try:
93
+ test_file.touch()
94
+ test_file.unlink()
95
+ return CheckResult(
96
+ name="Config Directory",
97
+ passed=True,
98
+ message=f"Config directory is writable: {config_dir}",
99
+ )
100
+ except (PermissionError, OSError):
101
+ return CheckResult(
102
+ name="Config Directory",
103
+ passed=False,
104
+ message=f"Config directory is not writable: {config_dir}",
105
+ fix_hint=f"Check permissions: chmod 755 {config_dir}",
106
+ severity="error",
107
+ )