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.
- scc_cli/__init__.py +15 -0
- scc_cli/audit/__init__.py +37 -0
- scc_cli/audit/parser.py +191 -0
- scc_cli/audit/reader.py +180 -0
- scc_cli/auth.py +145 -0
- scc_cli/claude_adapter.py +485 -0
- scc_cli/cli.py +311 -0
- scc_cli/cli_common.py +190 -0
- scc_cli/cli_helpers.py +244 -0
- scc_cli/commands/__init__.py +20 -0
- scc_cli/commands/admin.py +708 -0
- scc_cli/commands/audit.py +246 -0
- scc_cli/commands/config.py +528 -0
- scc_cli/commands/exceptions.py +696 -0
- scc_cli/commands/init.py +272 -0
- scc_cli/commands/launch/__init__.py +73 -0
- scc_cli/commands/launch/app.py +1247 -0
- scc_cli/commands/launch/render.py +309 -0
- scc_cli/commands/launch/sandbox.py +135 -0
- scc_cli/commands/launch/workspace.py +339 -0
- scc_cli/commands/org/__init__.py +49 -0
- scc_cli/commands/org/_builders.py +264 -0
- scc_cli/commands/org/app.py +41 -0
- scc_cli/commands/org/import_cmd.py +267 -0
- scc_cli/commands/org/init_cmd.py +269 -0
- scc_cli/commands/org/schema_cmd.py +76 -0
- scc_cli/commands/org/status_cmd.py +157 -0
- scc_cli/commands/org/update_cmd.py +330 -0
- scc_cli/commands/org/validate_cmd.py +138 -0
- scc_cli/commands/support.py +323 -0
- scc_cli/commands/team.py +910 -0
- scc_cli/commands/worktree/__init__.py +72 -0
- scc_cli/commands/worktree/_helpers.py +57 -0
- scc_cli/commands/worktree/app.py +170 -0
- scc_cli/commands/worktree/container_commands.py +385 -0
- scc_cli/commands/worktree/context_commands.py +61 -0
- scc_cli/commands/worktree/session_commands.py +128 -0
- scc_cli/commands/worktree/worktree_commands.py +734 -0
- scc_cli/config.py +647 -0
- scc_cli/confirm.py +20 -0
- scc_cli/console.py +562 -0
- scc_cli/contexts.py +394 -0
- scc_cli/core/__init__.py +68 -0
- scc_cli/core/constants.py +101 -0
- scc_cli/core/errors.py +297 -0
- scc_cli/core/exit_codes.py +91 -0
- scc_cli/core/workspace.py +57 -0
- scc_cli/deprecation.py +54 -0
- scc_cli/deps.py +189 -0
- scc_cli/docker/__init__.py +127 -0
- scc_cli/docker/core.py +467 -0
- scc_cli/docker/credentials.py +726 -0
- scc_cli/docker/launch.py +595 -0
- scc_cli/doctor/__init__.py +105 -0
- scc_cli/doctor/checks/__init__.py +166 -0
- scc_cli/doctor/checks/cache.py +314 -0
- scc_cli/doctor/checks/config.py +107 -0
- scc_cli/doctor/checks/environment.py +182 -0
- scc_cli/doctor/checks/json_helpers.py +157 -0
- scc_cli/doctor/checks/organization.py +264 -0
- scc_cli/doctor/checks/worktree.py +278 -0
- scc_cli/doctor/render.py +365 -0
- scc_cli/doctor/types.py +66 -0
- scc_cli/evaluation/__init__.py +27 -0
- scc_cli/evaluation/apply_exceptions.py +207 -0
- scc_cli/evaluation/evaluate.py +97 -0
- scc_cli/evaluation/models.py +80 -0
- scc_cli/git.py +84 -0
- scc_cli/json_command.py +166 -0
- scc_cli/json_output.py +159 -0
- scc_cli/kinds.py +65 -0
- scc_cli/marketplace/__init__.py +123 -0
- scc_cli/marketplace/adapter.py +74 -0
- scc_cli/marketplace/compute.py +377 -0
- scc_cli/marketplace/constants.py +87 -0
- scc_cli/marketplace/managed.py +135 -0
- scc_cli/marketplace/materialize.py +846 -0
- scc_cli/marketplace/normalize.py +548 -0
- scc_cli/marketplace/render.py +281 -0
- scc_cli/marketplace/resolve.py +459 -0
- scc_cli/marketplace/schema.py +506 -0
- scc_cli/marketplace/sync.py +279 -0
- scc_cli/marketplace/team_cache.py +195 -0
- scc_cli/marketplace/team_fetch.py +689 -0
- scc_cli/marketplace/trust.py +244 -0
- scc_cli/models/__init__.py +41 -0
- scc_cli/models/exceptions.py +273 -0
- scc_cli/models/plugin_audit.py +434 -0
- scc_cli/org_templates.py +269 -0
- scc_cli/output_mode.py +167 -0
- scc_cli/panels.py +113 -0
- scc_cli/platform.py +350 -0
- scc_cli/profiles.py +960 -0
- scc_cli/remote.py +443 -0
- scc_cli/schemas/__init__.py +1 -0
- scc_cli/schemas/org-v1.schema.json +456 -0
- scc_cli/schemas/team-config.v1.schema.json +163 -0
- scc_cli/services/__init__.py +1 -0
- scc_cli/services/git/__init__.py +79 -0
- scc_cli/services/git/branch.py +151 -0
- scc_cli/services/git/core.py +216 -0
- scc_cli/services/git/hooks.py +108 -0
- scc_cli/services/git/worktree.py +444 -0
- scc_cli/services/workspace/__init__.py +36 -0
- scc_cli/services/workspace/resolver.py +223 -0
- scc_cli/services/workspace/suspicious.py +200 -0
- scc_cli/sessions.py +425 -0
- scc_cli/setup.py +589 -0
- scc_cli/source_resolver.py +470 -0
- scc_cli/stats.py +378 -0
- scc_cli/stores/__init__.py +13 -0
- scc_cli/stores/exception_store.py +251 -0
- scc_cli/subprocess_utils.py +88 -0
- scc_cli/teams.py +383 -0
- scc_cli/templates/__init__.py +2 -0
- scc_cli/templates/org/__init__.py +0 -0
- scc_cli/templates/org/minimal.json +19 -0
- scc_cli/templates/org/reference.json +74 -0
- scc_cli/templates/org/strict.json +38 -0
- scc_cli/templates/org/teams.json +42 -0
- scc_cli/templates/statusline.sh +75 -0
- scc_cli/theme.py +348 -0
- scc_cli/ui/__init__.py +154 -0
- scc_cli/ui/branding.py +68 -0
- scc_cli/ui/chrome.py +401 -0
- scc_cli/ui/dashboard/__init__.py +62 -0
- scc_cli/ui/dashboard/_dashboard.py +794 -0
- scc_cli/ui/dashboard/loaders.py +452 -0
- scc_cli/ui/dashboard/models.py +185 -0
- scc_cli/ui/dashboard/orchestrator.py +735 -0
- scc_cli/ui/formatters.py +444 -0
- scc_cli/ui/gate.py +350 -0
- scc_cli/ui/git_interactive.py +869 -0
- scc_cli/ui/git_render.py +176 -0
- scc_cli/ui/help.py +157 -0
- scc_cli/ui/keys.py +615 -0
- scc_cli/ui/list_screen.py +437 -0
- scc_cli/ui/picker.py +763 -0
- scc_cli/ui/prompts.py +201 -0
- scc_cli/ui/quick_resume.py +116 -0
- scc_cli/ui/wizard.py +576 -0
- scc_cli/update.py +680 -0
- scc_cli/utils/__init__.py +39 -0
- scc_cli/utils/fixit.py +264 -0
- scc_cli/utils/fuzzy.py +124 -0
- scc_cli/utils/locks.py +114 -0
- scc_cli/utils/ttl.py +376 -0
- scc_cli/validate.py +455 -0
- scc_cli-1.5.3.dist-info/METADATA +401 -0
- scc_cli-1.5.3.dist-info/RECORD +153 -0
- scc_cli-1.5.3.dist-info/WHEEL +4 -0
- scc_cli-1.5.3.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|