codeshift 0.4.0__tar.gz → 0.7.0__tar.gz
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.
- {codeshift-0.4.0 → codeshift-0.7.0}/PKG-INFO +4 -1
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/__init__.py +1 -1
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/auth.py +41 -25
- codeshift-0.7.0/codeshift/cli/commands/health.py +244 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/upgrade.py +68 -55
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/main.py +2 -0
- codeshift-0.7.0/codeshift/health/__init__.py +50 -0
- codeshift-0.7.0/codeshift/health/calculator.py +217 -0
- codeshift-0.7.0/codeshift/health/metrics/__init__.py +63 -0
- codeshift-0.7.0/codeshift/health/metrics/documentation.py +209 -0
- codeshift-0.7.0/codeshift/health/metrics/freshness.py +180 -0
- codeshift-0.7.0/codeshift/health/metrics/migration_readiness.py +142 -0
- codeshift-0.7.0/codeshift/health/metrics/security.py +225 -0
- codeshift-0.7.0/codeshift/health/metrics/test_coverage.py +191 -0
- codeshift-0.7.0/codeshift/health/models.py +284 -0
- codeshift-0.7.0/codeshift/health/report.py +310 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/generator.py +6 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/aiohttp.yaml +3 -3
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/httpx.yaml +4 -4
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/pytest.yaml +1 -1
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/models.py +1 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/marshmallow_transformer.py +50 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/pydantic_v1_to_v2.py +191 -22
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/scanner/code_scanner.py +22 -2
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/utils/api_client.py +144 -4
- codeshift-0.7.0/codeshift/utils/credential_store.py +393 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/utils/llm_client.py +111 -9
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/PKG-INFO +4 -1
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/SOURCES.txt +14 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/requires.txt +3 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/pyproject.toml +14 -2
- codeshift-0.7.0/tests/test_health.py +600 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_marshmallow_transforms.py +79 -0
- codeshift-0.7.0/tests/test_pydantic_type_inference.py +235 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/LICENSE +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/README.md +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/analyzer/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/analyzer/risk_assessor.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/apply.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/diff.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/scan.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/commands/upgrade_all.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/package_manager.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/cli/quota.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/cache.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/models.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/parser.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge/sources.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/attrs.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/celery.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/click.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/django.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/fastapi.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/flask.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/marshmallow.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/numpy.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/pandas.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/pydantic.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/requests.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/libraries/sqlalchemy.yaml +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/knowledge_base/loader.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/ast_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/engine.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/llm_migrator.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/aiohttp_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/attrs_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/celery_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/click_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/django_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/fastapi_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/flask_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/httpx_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/numpy_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/pandas_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/pytest_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/requests_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/migrator/transforms/sqlalchemy_transformer.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/scanner/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/scanner/dependency_parser.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/utils/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/utils/cache.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/utils/config.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/validator/__init__.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/validator/syntax_checker.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift/validator/test_runner.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/dependency_links.txt +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/entry_points.txt +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/codeshift.egg-info/top_level.txt +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/setup.cfg +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_aiohttp_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_attrs_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_celery_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_click_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_code_scanner.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_django_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_fastapi_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_flask_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_httpx_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_knowledge_base.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_numpy_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_pandas_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_pydantic_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_pytest_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_requests_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_risk_assessor.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_sqlalchemy_transforms.py +0 -0
- {codeshift-0.4.0 → codeshift-0.7.0}/tests/test_syntax_checker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeshift
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: AI-powered CLI tool that migrates Python code to handle breaking dependency changes
|
|
5
5
|
Author: Ragab Technologies
|
|
6
6
|
License: MIT
|
|
@@ -27,6 +27,9 @@ Requires-Dist: rich>=13.0
|
|
|
27
27
|
Requires-Dist: toml>=0.10
|
|
28
28
|
Requires-Dist: packaging>=23.0
|
|
29
29
|
Requires-Dist: httpx>=0.25
|
|
30
|
+
Requires-Dist: black>=24.10.0
|
|
31
|
+
Requires-Dist: cryptography>=41.0
|
|
32
|
+
Requires-Dist: nox>=2025.11.12
|
|
30
33
|
Provides-Extra: dev
|
|
31
34
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
35
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
@@ -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
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
except
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
22
|
-
from codeshift.migrator.
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
"
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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}")
|
|
@@ -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
|
+
]
|