codeshift 0.4.0__py3-none-any.whl → 0.7.0__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.
Files changed (32) hide show
  1. codeshift/__init__.py +1 -1
  2. codeshift/cli/commands/auth.py +41 -25
  3. codeshift/cli/commands/health.py +244 -0
  4. codeshift/cli/commands/upgrade.py +68 -55
  5. codeshift/cli/main.py +2 -0
  6. codeshift/health/__init__.py +50 -0
  7. codeshift/health/calculator.py +217 -0
  8. codeshift/health/metrics/__init__.py +63 -0
  9. codeshift/health/metrics/documentation.py +209 -0
  10. codeshift/health/metrics/freshness.py +180 -0
  11. codeshift/health/metrics/migration_readiness.py +142 -0
  12. codeshift/health/metrics/security.py +225 -0
  13. codeshift/health/metrics/test_coverage.py +191 -0
  14. codeshift/health/models.py +284 -0
  15. codeshift/health/report.py +310 -0
  16. codeshift/knowledge/generator.py +6 -0
  17. codeshift/knowledge_base/libraries/aiohttp.yaml +3 -3
  18. codeshift/knowledge_base/libraries/httpx.yaml +4 -4
  19. codeshift/knowledge_base/libraries/pytest.yaml +1 -1
  20. codeshift/knowledge_base/models.py +1 -0
  21. codeshift/migrator/transforms/marshmallow_transformer.py +50 -0
  22. codeshift/migrator/transforms/pydantic_v1_to_v2.py +191 -22
  23. codeshift/scanner/code_scanner.py +22 -2
  24. codeshift/utils/api_client.py +144 -4
  25. codeshift/utils/credential_store.py +393 -0
  26. codeshift/utils/llm_client.py +111 -9
  27. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/METADATA +4 -1
  28. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/RECORD +32 -20
  29. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/WHEEL +0 -0
  30. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/entry_points.txt +0 -0
  31. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/licenses/LICENSE +0 -0
  32. {codeshift-0.4.0.dist-info → codeshift-0.7.0.dist-info}/top_level.txt +0 -0
codeshift/__init__.py CHANGED
@@ -4,5 +4,5 @@ Codeshift - AI-powered CLI tool for migrating Python code to handle breaking dep
4
4
  Don't just flag the update. Fix the break.
5
5
  """
6
6
 
7
- __version__ = "0.2.0"
7
+ __version__ = "0.7.0"
8
8
  __author__ = "Codeshift Team"
@@ -1,11 +1,10 @@
1
1
  """Authentication commands for Codeshift CLI."""
2
2
 
3
- import json
4
3
  import os
5
4
  import time
6
5
  import webbrowser
7
6
  from pathlib import Path
8
- from typing import Any, cast
7
+ from typing import Any
9
8
 
10
9
  import click
11
10
  import httpx
@@ -15,11 +14,16 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
15
14
  from rich.prompt import Confirm, Prompt
16
15
  from rich.table import Table
17
16
 
17
+ from codeshift.utils.credential_store import (
18
+ CredentialDecryptionError,
19
+ get_credential_store,
20
+ )
21
+
18
22
  console = Console()
19
23
 
20
- # Config directory for storing credentials
24
+ # Config directory for storing credentials (kept for backward compatibility reference)
21
25
  CONFIG_DIR = Path.home() / ".config" / "codeshift"
22
- CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
26
+ CREDENTIALS_FILE = CONFIG_DIR / "credentials.json" # Legacy path
23
27
 
24
28
 
25
29
  def get_api_url() -> str:
@@ -28,28 +32,38 @@ def get_api_url() -> str:
28
32
 
29
33
 
30
34
  def load_credentials() -> dict[str, Any] | None:
31
- """Load saved credentials from disk."""
32
- if not CREDENTIALS_FILE.exists():
33
- return None
35
+ """Load saved credentials from secure storage.
36
+
37
+ Automatically handles migration from plaintext to encrypted storage.
38
+
39
+ Returns:
40
+ Dictionary of credentials, or None if not found.
41
+ """
42
+ store = get_credential_store()
34
43
  try:
35
- return cast(dict[str, Any], json.loads(CREDENTIALS_FILE.read_text()))
36
- except (OSError, json.JSONDecodeError):
44
+ return store.load()
45
+ except CredentialDecryptionError as e:
46
+ console.print(
47
+ Panel(
48
+ f"[red]Could not decrypt credentials:[/] {e}\n\n"
49
+ "This may happen if credentials were created on a different machine.\n"
50
+ "Please run [cyan]codeshift login[/] to re-authenticate.",
51
+ title="Credential Error",
52
+ )
53
+ )
37
54
  return None
38
55
 
39
56
 
40
57
  def save_credentials(credentials: dict) -> None:
41
- """Save credentials to disk."""
42
- CONFIG_DIR.mkdir(parents=True, exist_ok=True)
43
-
44
- # Set restrictive permissions
45
- CREDENTIALS_FILE.write_text(json.dumps(credentials, indent=2))
46
- os.chmod(CREDENTIALS_FILE, 0o600)
58
+ """Save credentials to secure encrypted storage."""
59
+ store = get_credential_store()
60
+ store.save(credentials)
47
61
 
48
62
 
49
63
  def delete_credentials() -> None:
50
- """Delete saved credentials."""
51
- if CREDENTIALS_FILE.exists():
52
- CREDENTIALS_FILE.unlink()
64
+ """Delete saved credentials securely."""
65
+ store = get_credential_store()
66
+ store.delete()
53
67
 
54
68
 
55
69
  def get_api_key() -> str | None:
@@ -107,7 +121,8 @@ def login(
107
121
  2. API key: codeshift login -k pyr_xxxxx
108
122
  3. Device flow: codeshift login --device
109
123
 
110
- Your credentials are stored in ~/.config/codeshift/credentials.json
124
+ Your credentials are stored securely in ~/.config/codeshift/credentials.enc
125
+ using AES encryption.
111
126
 
112
127
  Don't have an account? Run: codeshift register
113
128
  """
@@ -154,7 +169,8 @@ def register(
154
169
  Example:
155
170
  codeshift register -e user@example.com -p yourpassword
156
171
 
157
- Your credentials are stored in ~/.config/codeshift/credentials.json
172
+ Your credentials are stored securely in ~/.config/codeshift/credentials.enc
173
+ using AES encryption.
158
174
  """
159
175
  # Check if already logged in
160
176
  existing = load_credentials()
@@ -211,7 +227,7 @@ def _register_account(email: str, password: str, full_name: str | None) -> None:
211
227
  if response.status_code == 200:
212
228
  data = response.json()
213
229
 
214
- # Save credentials
230
+ # Save credentials securely
215
231
  save_credentials(
216
232
  {
217
233
  "api_key": data["api_key"],
@@ -275,7 +291,7 @@ def _login_with_api_key(api_key: str) -> None:
275
291
  if response.status_code == 200:
276
292
  user = response.json()
277
293
 
278
- # Save credentials
294
+ # Save credentials securely
279
295
  save_credentials(
280
296
  {
281
297
  "api_key": api_key,
@@ -327,7 +343,7 @@ def _login_with_password(email: str, password: str) -> None:
327
343
  if response.status_code == 200:
328
344
  data = response.json()
329
345
 
330
- # Save credentials
346
+ # Save credentials securely
331
347
  save_credentials(
332
348
  {
333
349
  "api_key": data["api_key"],
@@ -422,7 +438,7 @@ def _login_with_device_code() -> None:
422
438
  if response.status_code == 200:
423
439
  data = response.json()
424
440
 
425
- # Save credentials
441
+ # Save credentials securely
426
442
  save_credentials(
427
443
  {
428
444
  "api_key": data["api_key"],
@@ -487,7 +503,7 @@ def logout() -> None:
487
503
  except httpx.RequestError:
488
504
  pass
489
505
 
490
- # Delete local credentials
506
+ # Delete local credentials securely
491
507
  delete_credentials()
492
508
 
493
509
  console.print("[green]Successfully logged out[/]")
@@ -0,0 +1,244 @@
1
+ """CLI command for codebase health scoring."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from codeshift.health.calculator import HealthCalculator
12
+ from codeshift.health.models import HealthGrade, HealthScore, MetricCategory
13
+ from codeshift.health.report import generate_json_report, save_html_report, save_json_report
14
+
15
+ console = Console()
16
+
17
+
18
+ @click.command()
19
+ @click.option(
20
+ "--path",
21
+ "-p",
22
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
23
+ default=".",
24
+ help="Path to the project (default: current directory)",
25
+ )
26
+ @click.option(
27
+ "--report",
28
+ "-r",
29
+ type=click.Choice(["json", "html"]),
30
+ help="Generate a detailed report in the specified format",
31
+ )
32
+ @click.option(
33
+ "--output",
34
+ "-o",
35
+ type=click.Path(),
36
+ help="Output file path for the report (default: health_report.<format>)",
37
+ )
38
+ @click.option(
39
+ "--ci",
40
+ is_flag=True,
41
+ help="CI mode: exit with non-zero status if score is below threshold",
42
+ )
43
+ @click.option(
44
+ "--threshold",
45
+ type=int,
46
+ default=70,
47
+ help="Minimum score for CI mode (default: 70)",
48
+ )
49
+ @click.option(
50
+ "--verbose",
51
+ "-v",
52
+ is_flag=True,
53
+ help="Show detailed output including all dependencies",
54
+ )
55
+ def health(
56
+ path: str,
57
+ report: str | None,
58
+ output: str | None,
59
+ ci: bool,
60
+ threshold: int,
61
+ verbose: bool,
62
+ ) -> None:
63
+ """Analyze codebase health and generate a score.
64
+
65
+ Evaluates your project across five dimensions:
66
+ - Dependency Freshness (30%): How up-to-date are your dependencies?
67
+ - Security (25%): Known vulnerabilities in dependencies
68
+ - Migration Readiness (20%): Tier 1/2 support coverage
69
+ - Test Coverage (15%): Percentage of code covered by tests
70
+ - Documentation (10%): Type hints and docstrings
71
+
72
+ \b
73
+ Examples:
74
+ codeshift health # Show health summary
75
+ codeshift health --report html # Generate HTML report
76
+ codeshift health --report json -o report.json
77
+ codeshift health --ci --threshold 70 # CI mode
78
+
79
+ """
80
+ project_path = Path(path).resolve()
81
+
82
+ with console.status("[bold blue]Analyzing codebase health..."):
83
+ calculator = HealthCalculator()
84
+ score = calculator.calculate(project_path)
85
+
86
+ # Handle report generation
87
+ if report:
88
+ output_path = Path(output) if output else Path(f"health_report.{report}")
89
+
90
+ if report == "json":
91
+ save_json_report(score, output_path)
92
+ console.print(f"[green]JSON report saved to:[/] {output_path}")
93
+ elif report == "html":
94
+ save_html_report(score, output_path)
95
+ console.print(f"[green]HTML report saved to:[/] {output_path}")
96
+
97
+ # In CI mode with report, also output JSON to stdout
98
+ if ci:
99
+ console.print(generate_json_report(score))
100
+ else:
101
+ # Display rich table output
102
+ _display_health_summary(score, verbose)
103
+
104
+ # CI mode exit code handling
105
+ if ci:
106
+ if score.overall_score < threshold:
107
+ console.print(
108
+ f"\n[red]CI Check Failed:[/] Score {score.overall_score:.1f} is below threshold {threshold}"
109
+ )
110
+ sys.exit(1)
111
+ else:
112
+ console.print(
113
+ f"\n[green]CI Check Passed:[/] Score {score.overall_score:.1f} meets threshold {threshold}"
114
+ )
115
+ sys.exit(0)
116
+
117
+
118
+ def _display_health_summary(score: HealthScore, verbose: bool) -> None:
119
+ """Display the health score summary in the terminal.
120
+
121
+ Args:
122
+ score: HealthScore object
123
+ verbose: Whether to show detailed output
124
+ """
125
+ # Grade panel
126
+ grade_style = _get_grade_style(score.grade)
127
+ console.print(
128
+ Panel(
129
+ f"[{grade_style}]Grade {score.grade.value}[/] - {score.overall_score:.1f}/100",
130
+ title="[bold]Codebase Health Score[/]",
131
+ subtitle=str(score.project_path),
132
+ )
133
+ )
134
+
135
+ # Metrics table
136
+ table = Table(title="Metrics Breakdown", show_header=True)
137
+ table.add_column("Category", style="cyan")
138
+ table.add_column("Score", justify="right")
139
+ table.add_column("Weight", justify="right", style="dim")
140
+ table.add_column("Details")
141
+
142
+ # Sort by score (lowest first) to highlight problem areas
143
+ sorted_metrics = sorted(score.metrics, key=lambda m: m.score)
144
+
145
+ for metric in sorted_metrics:
146
+ score_style = _get_score_style(metric.score)
147
+ weight_pct = f"{metric.weight * 100:.0f}%"
148
+ table.add_row(
149
+ _format_category(metric.category),
150
+ f"[{score_style}]{metric.score:.1f}[/]",
151
+ weight_pct,
152
+ metric.description,
153
+ )
154
+
155
+ console.print(table)
156
+
157
+ # Recommendations
158
+ if score.top_recommendations:
159
+ console.print("\n[bold]Top Recommendations:[/]")
160
+ for i, rec in enumerate(score.top_recommendations, 1):
161
+ console.print(f" {i}. {rec}")
162
+
163
+ # Verbose: show dependencies
164
+ if verbose and score.dependencies:
165
+ console.print()
166
+ deps_table = Table(title="Dependencies", show_header=True)
167
+ deps_table.add_column("Package", style="cyan")
168
+ deps_table.add_column("Current")
169
+ deps_table.add_column("Latest")
170
+ deps_table.add_column("Status")
171
+ deps_table.add_column("Migration")
172
+ deps_table.add_column("Vulns", justify="right")
173
+
174
+ for dep in score.dependencies:
175
+ status = "[green]✓[/]" if not dep.is_outdated else "[yellow]↑[/]"
176
+ tier = (
177
+ "[green]Tier 1[/]"
178
+ if dep.has_tier1_support
179
+ else ("[cyan]Tier 2[/]" if dep.has_tier2_support else "[dim]-[/]")
180
+ )
181
+ vuln_count = len(dep.vulnerabilities)
182
+ vuln_style = "green" if vuln_count == 0 else "red"
183
+
184
+ deps_table.add_row(
185
+ dep.name,
186
+ dep.current_version or "?",
187
+ dep.latest_version or "?",
188
+ status,
189
+ tier,
190
+ f"[{vuln_style}]{vuln_count}[/]",
191
+ )
192
+
193
+ console.print(deps_table)
194
+
195
+ # Show vulnerabilities summary if any
196
+ if score.vulnerabilities:
197
+ console.print()
198
+ console.print(
199
+ f"[bold red]Security Alert:[/] {len(score.vulnerabilities)} vulnerabilities found"
200
+ )
201
+ for vuln in score.vulnerabilities[:3]:
202
+ console.print(
203
+ f" - [{vuln.severity.value.upper()}] {vuln.package}: {vuln.vulnerability_id}"
204
+ )
205
+ if len(score.vulnerabilities) > 3:
206
+ console.print(f" ... and {len(score.vulnerabilities) - 3} more")
207
+
208
+
209
+ def _get_grade_style(grade: HealthGrade) -> str:
210
+ """Get Rich style for a grade."""
211
+ styles = {
212
+ HealthGrade.A: "bold green",
213
+ HealthGrade.B: "bold cyan",
214
+ HealthGrade.C: "bold yellow",
215
+ HealthGrade.D: "bold orange1",
216
+ HealthGrade.F: "bold red",
217
+ }
218
+ return styles.get(grade, "white")
219
+
220
+
221
+ def _get_score_style(score: float) -> str:
222
+ """Get Rich style for a numeric score."""
223
+ if score >= 90:
224
+ return "green"
225
+ elif score >= 80:
226
+ return "cyan"
227
+ elif score >= 70:
228
+ return "yellow"
229
+ elif score >= 60:
230
+ return "orange1"
231
+ else:
232
+ return "red"
233
+
234
+
235
+ def _format_category(category: MetricCategory) -> str:
236
+ """Format category for display."""
237
+ names = {
238
+ MetricCategory.FRESHNESS: "Freshness",
239
+ MetricCategory.SECURITY: "Security",
240
+ MetricCategory.MIGRATION_READINESS: "Migration Ready",
241
+ MetricCategory.TEST_COVERAGE: "Test Coverage",
242
+ MetricCategory.DOCUMENTATION: "Documentation",
243
+ }
244
+ return names.get(category, category.value)
@@ -18,12 +18,8 @@ from codeshift.knowledge import (
18
18
  is_tier_1_library,
19
19
  )
20
20
  from codeshift.knowledge_base import KnowledgeBaseLoader
21
- from codeshift.migrator.ast_transforms import TransformChange, TransformResult, TransformStatus
22
- from codeshift.migrator.transforms.fastapi_transformer import transform_fastapi
23
- from codeshift.migrator.transforms.pandas_transformer import transform_pandas
24
- from codeshift.migrator.transforms.pydantic_v1_to_v2 import transform_pydantic_v1_to_v2
25
- from codeshift.migrator.transforms.requests_transformer import transform_requests
26
- from codeshift.migrator.transforms.sqlalchemy_transformer import transform_sqlalchemy
21
+ from codeshift.knowledge_base.models import LibraryKnowledge
22
+ from codeshift.migrator.ast_transforms import TransformResult, TransformStatus
27
23
  from codeshift.scanner import CodeScanner, DependencyParser
28
24
  from codeshift.utils.config import ProjectConfig
29
25
 
@@ -81,6 +77,11 @@ def save_state(project_path: Path, state: dict) -> None:
81
77
  is_flag=True,
82
78
  help="Show detailed output",
83
79
  )
80
+ @click.option(
81
+ "--force-llm",
82
+ is_flag=True,
83
+ help="Force LLM migration even for libraries with AST transforms",
84
+ )
84
85
  def upgrade(
85
86
  library: str,
86
87
  target: str,
@@ -88,6 +89,7 @@ def upgrade(
88
89
  file: str | None,
89
90
  dry_run: bool,
90
91
  verbose: bool,
92
+ force_llm: bool,
91
93
  ) -> None:
92
94
  """Analyze your codebase and propose changes for a library upgrade.
93
95
 
@@ -100,34 +102,44 @@ def upgrade(
100
102
  project_path = Path(path).resolve()
101
103
  project_config = ProjectConfig.from_pyproject(project_path)
102
104
 
103
- # Check quota before starting (allow offline for Tier 1 libraries)
105
+ # Check quota before starting (allow offline for Tier 1 libraries unless force-llm)
104
106
  is_tier1 = is_tier_1_library(library)
105
107
  try:
106
- check_quota("file_migrated", quantity=1, allow_offline=is_tier1)
108
+ check_quota("file_migrated", quantity=1, allow_offline=is_tier1 and not force_llm)
107
109
  except QuotaError as e:
108
110
  show_quota_exceeded_message(e)
109
111
  raise SystemExit(1) from e
110
112
 
111
- # Load knowledge base
113
+ # Load knowledge base (optional - YAML may not exist for all libraries)
112
114
  loader = KnowledgeBaseLoader()
115
+ knowledge: LibraryKnowledge | None = None
113
116
 
114
117
  try:
115
118
  knowledge = loader.load(library)
116
- except FileNotFoundError as e:
117
- console.print(f"[red]Error:[/] {e}")
118
- console.print(f"\nSupported libraries: {', '.join(loader.get_supported_libraries())}")
119
- raise SystemExit(1) from e
119
+ except FileNotFoundError:
120
+ if verbose:
121
+ console.print(
122
+ f"[dim]No knowledge base YAML for {library} - using generated knowledge[/]"
123
+ )
120
124
 
121
- # Check if migration is supported
122
- # For now, we'll allow any version since we're doing a general migration
123
- console.print(
124
- Panel(
125
- f"[bold]Upgrading {knowledge.display_name}[/] to version [cyan]{target}[/]\n\n"
126
- f"{knowledge.description}\n"
127
- f"Migration guide: {knowledge.migration_guide_url or 'N/A'}",
128
- title="Codeshift Migration",
125
+ # Display migration info with fallback for missing YAML
126
+ if knowledge:
127
+ console.print(
128
+ Panel(
129
+ f"[bold]Upgrading {knowledge.display_name}[/] to version [cyan]{target}[/]\n\n"
130
+ f"{knowledge.description}\n"
131
+ f"Migration guide: {knowledge.migration_guide_url or 'N/A'}",
132
+ title="Codeshift Migration",
133
+ )
134
+ )
135
+ else:
136
+ console.print(
137
+ Panel(
138
+ f"[bold]Upgrading {library}[/] to version [cyan]{target}[/]\n\n"
139
+ "Using AI-powered migration (no static knowledge base available)",
140
+ title="Codeshift Migration",
141
+ )
129
142
  )
130
- )
131
143
 
132
144
  # Step 1: Parse dependencies
133
145
  with Progress(
@@ -271,7 +283,25 @@ def upgrade(
271
283
  console.print(f"\n[yellow]No {library} imports found in the codebase.[/]")
272
284
  return
273
285
 
274
- # Step 4: Apply transforms
286
+ # Step 4: Apply transforms using MigrationEngine
287
+ # Import here to avoid circular dependency (upgrade.py -> migrator -> llm_migrator -> api_client -> auth -> cli -> upgrade.py)
288
+ from codeshift.migrator import get_migration_engine
289
+
290
+ engine = get_migration_engine()
291
+
292
+ # Check auth for non-Tier1 libraries or force-llm mode
293
+ llm_required = force_llm or not is_tier1
294
+ if llm_required and not engine.llm_migrator.is_available:
295
+ console.print(
296
+ Panel(
297
+ f"[yellow]LLM migration required for {library}[/]\n\n"
298
+ "Run [cyan]codeshift login[/] and upgrade to Pro tier for LLM features.",
299
+ title="Authentication Required",
300
+ )
301
+ )
302
+ if not is_tier1:
303
+ raise SystemExit(1)
304
+
275
305
  with Progress(
276
306
  SpinnerColumn(),
277
307
  TextColumn("[progress.description]{task.description}"),
@@ -286,45 +316,28 @@ def upgrade(
286
316
 
287
317
  results: list[TransformResult] = []
288
318
 
319
+ def migration_progress(msg: str) -> None:
320
+ progress.update(task, description=msg)
321
+
289
322
  for file_path in files_to_transform:
290
323
  try:
291
324
  source_code = file_path.read_text()
292
325
 
293
- # Select transformer based on library
294
- transform_func = {
295
- "pydantic": transform_pydantic_v1_to_v2,
296
- "fastapi": transform_fastapi,
297
- "sqlalchemy": transform_sqlalchemy,
298
- "pandas": transform_pandas,
299
- "requests": transform_requests,
300
- }.get(library)
301
-
302
- if transform_func:
303
- transformed_code, changes = transform_func(source_code)
304
- # Create TransformResult from the function output
305
- result = TransformResult(
306
- file_path=file_path,
307
- status=TransformStatus.SUCCESS if changes else TransformStatus.NO_CHANGES,
308
- original_code=source_code,
309
- transformed_code=transformed_code,
310
- changes=[
311
- TransformChange(
312
- description=c.description,
313
- line_number=c.line_number,
314
- original=c.original,
315
- replacement=c.replacement,
316
- transform_name=c.transform_name,
317
- confidence=getattr(c, "confidence", 1.0),
318
- )
319
- for c in changes
320
- ],
321
- )
322
- else:
323
- console.print(f"[yellow]Warning:[/] No transformer available for {library}")
324
- continue
326
+ result = engine.run_migration(
327
+ code=source_code,
328
+ file_path=file_path,
329
+ library=library,
330
+ old_version=current_version or "1.0",
331
+ new_version=target,
332
+ knowledge_base=generated_kb,
333
+ progress_callback=migration_progress if verbose else None,
334
+ )
325
335
 
326
336
  if result.has_changes:
327
337
  results.append(result)
338
+ elif result.errors:
339
+ for error in result.errors:
340
+ console.print(f"[yellow]Warning ({file_path.name}):[/] {error}")
328
341
 
329
342
  except Exception as e:
330
343
  console.print(f"[red]Error processing {file_path}:[/] {e}")
codeshift/cli/main.py CHANGED
@@ -15,6 +15,7 @@ from codeshift.cli.commands.auth import (
15
15
  whoami,
16
16
  )
17
17
  from codeshift.cli.commands.diff import diff
18
+ from codeshift.cli.commands.health import health
18
19
  from codeshift.cli.commands.scan import scan
19
20
  from codeshift.cli.commands.upgrade import upgrade
20
21
  from codeshift.cli.commands.upgrade_all import upgrade_all
@@ -46,6 +47,7 @@ cli.add_command(upgrade)
46
47
  cli.add_command(upgrade_all)
47
48
  cli.add_command(diff)
48
49
  cli.add_command(apply)
50
+ cli.add_command(health)
49
51
 
50
52
  # Auth commands
51
53
  cli.add_command(register)
@@ -0,0 +1,50 @@
1
+ """Codebase health scoring module.
2
+
3
+ This module provides health scoring capabilities for Python projects,
4
+ analyzing dependency freshness, security, migration readiness, test
5
+ coverage, and documentation quality.
6
+
7
+ Example:
8
+ >>> from codeshift.health import HealthCalculator
9
+ >>> calculator = HealthCalculator()
10
+ >>> score = calculator.calculate(Path("."))
11
+ >>> print(score.summary)
12
+ 🟢 Grade A (92.5/100)
13
+ """
14
+
15
+ from codeshift.health.calculator import HealthCalculator
16
+ from codeshift.health.models import (
17
+ DependencyHealth,
18
+ HealthGrade,
19
+ HealthReport,
20
+ HealthScore,
21
+ MetricCategory,
22
+ MetricResult,
23
+ SecurityVulnerability,
24
+ VulnerabilitySeverity,
25
+ )
26
+ from codeshift.health.report import (
27
+ generate_html_report,
28
+ generate_json_report,
29
+ save_html_report,
30
+ save_json_report,
31
+ )
32
+
33
+ __all__ = [
34
+ # Main calculator
35
+ "HealthCalculator",
36
+ # Models
37
+ "DependencyHealth",
38
+ "HealthGrade",
39
+ "HealthReport",
40
+ "HealthScore",
41
+ "MetricCategory",
42
+ "MetricResult",
43
+ "SecurityVulnerability",
44
+ "VulnerabilitySeverity",
45
+ # Report functions
46
+ "generate_html_report",
47
+ "generate_json_report",
48
+ "save_html_report",
49
+ "save_json_report",
50
+ ]