codeshift 0.2.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.
- codeshift/__init__.py +8 -0
- codeshift/analyzer/__init__.py +5 -0
- codeshift/analyzer/risk_assessor.py +388 -0
- codeshift/api/__init__.py +1 -0
- codeshift/api/auth.py +182 -0
- codeshift/api/config.py +73 -0
- codeshift/api/database.py +215 -0
- codeshift/api/main.py +103 -0
- codeshift/api/models/__init__.py +55 -0
- codeshift/api/models/auth.py +108 -0
- codeshift/api/models/billing.py +92 -0
- codeshift/api/models/migrate.py +42 -0
- codeshift/api/models/usage.py +116 -0
- codeshift/api/routers/__init__.py +5 -0
- codeshift/api/routers/auth.py +440 -0
- codeshift/api/routers/billing.py +395 -0
- codeshift/api/routers/migrate.py +304 -0
- codeshift/api/routers/usage.py +291 -0
- codeshift/api/routers/webhooks.py +289 -0
- codeshift/cli/__init__.py +5 -0
- codeshift/cli/commands/__init__.py +7 -0
- codeshift/cli/commands/apply.py +352 -0
- codeshift/cli/commands/auth.py +842 -0
- codeshift/cli/commands/diff.py +221 -0
- codeshift/cli/commands/scan.py +368 -0
- codeshift/cli/commands/upgrade.py +436 -0
- codeshift/cli/commands/upgrade_all.py +518 -0
- codeshift/cli/main.py +221 -0
- codeshift/cli/quota.py +210 -0
- codeshift/knowledge/__init__.py +50 -0
- codeshift/knowledge/cache.py +167 -0
- codeshift/knowledge/generator.py +231 -0
- codeshift/knowledge/models.py +151 -0
- codeshift/knowledge/parser.py +270 -0
- codeshift/knowledge/sources.py +388 -0
- codeshift/knowledge_base/__init__.py +17 -0
- codeshift/knowledge_base/loader.py +102 -0
- codeshift/knowledge_base/models.py +110 -0
- codeshift/migrator/__init__.py +23 -0
- codeshift/migrator/ast_transforms.py +256 -0
- codeshift/migrator/engine.py +395 -0
- codeshift/migrator/llm_migrator.py +320 -0
- codeshift/migrator/transforms/__init__.py +19 -0
- codeshift/migrator/transforms/fastapi_transformer.py +174 -0
- codeshift/migrator/transforms/pandas_transformer.py +236 -0
- codeshift/migrator/transforms/pydantic_v1_to_v2.py +637 -0
- codeshift/migrator/transforms/requests_transformer.py +218 -0
- codeshift/migrator/transforms/sqlalchemy_transformer.py +175 -0
- codeshift/scanner/__init__.py +6 -0
- codeshift/scanner/code_scanner.py +352 -0
- codeshift/scanner/dependency_parser.py +473 -0
- codeshift/utils/__init__.py +5 -0
- codeshift/utils/api_client.py +266 -0
- codeshift/utils/cache.py +318 -0
- codeshift/utils/config.py +71 -0
- codeshift/utils/llm_client.py +221 -0
- codeshift/validator/__init__.py +6 -0
- codeshift/validator/syntax_checker.py +183 -0
- codeshift/validator/test_runner.py +224 -0
- codeshift-0.2.0.dist-info/METADATA +326 -0
- codeshift-0.2.0.dist-info/RECORD +65 -0
- codeshift-0.2.0.dist-info/WHEEL +5 -0
- codeshift-0.2.0.dist-info/entry_points.txt +2 -0
- codeshift-0.2.0.dist-info/licenses/LICENSE +21 -0
- codeshift-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Diff command for viewing proposed changes."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
from codeshift.cli.commands.upgrade import load_state
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option(
|
|
18
|
+
"--path",
|
|
19
|
+
"-p",
|
|
20
|
+
type=click.Path(exists=True),
|
|
21
|
+
default=".",
|
|
22
|
+
help="Path to the project",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--file",
|
|
26
|
+
"-f",
|
|
27
|
+
type=str,
|
|
28
|
+
help="Show diff for a specific file only",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--no-color",
|
|
32
|
+
is_flag=True,
|
|
33
|
+
help="Disable colored output",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--context",
|
|
37
|
+
"-c",
|
|
38
|
+
type=int,
|
|
39
|
+
default=3,
|
|
40
|
+
help="Number of context lines in diff (default: 3)",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--summary",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
help="Show only a summary of changes without full diff",
|
|
46
|
+
)
|
|
47
|
+
def diff(
|
|
48
|
+
path: str,
|
|
49
|
+
file: str | None,
|
|
50
|
+
no_color: bool,
|
|
51
|
+
context: int,
|
|
52
|
+
summary: bool,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""View detailed diff of proposed changes.
|
|
55
|
+
|
|
56
|
+
\b
|
|
57
|
+
Examples:
|
|
58
|
+
codeshift diff
|
|
59
|
+
codeshift diff --file models.py
|
|
60
|
+
codeshift diff --summary
|
|
61
|
+
"""
|
|
62
|
+
project_path = Path(path).resolve()
|
|
63
|
+
state = load_state(project_path)
|
|
64
|
+
|
|
65
|
+
if state is None:
|
|
66
|
+
console.print(
|
|
67
|
+
Panel(
|
|
68
|
+
"[yellow]No pending migration found.[/]\n\n"
|
|
69
|
+
"Run [cyan]codeshift upgrade <library> --target <version>[/] first.",
|
|
70
|
+
title="No Changes",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
library = state.get("library", "unknown")
|
|
76
|
+
target_version = state.get("target_version", "unknown")
|
|
77
|
+
results = state.get("results", [])
|
|
78
|
+
|
|
79
|
+
if not results:
|
|
80
|
+
console.print("[yellow]No changes pending.[/]")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
console.print(
|
|
84
|
+
Panel(
|
|
85
|
+
f"[bold]Migration: {library}[/] → v{target_version}\n"
|
|
86
|
+
f"Files: {len(results)} | Total changes: {sum(r.get('change_count', 0) for r in results)}",
|
|
87
|
+
title="Proposed Changes",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for result in results:
|
|
92
|
+
file_path = Path(result["file_path"])
|
|
93
|
+
relative_path = (
|
|
94
|
+
file_path.relative_to(project_path)
|
|
95
|
+
if file_path.is_relative_to(project_path)
|
|
96
|
+
else file_path
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Filter by file if specified
|
|
100
|
+
if file and str(relative_path) != file and file_path.name != file:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
original = result.get("original_code", "")
|
|
104
|
+
transformed = result.get("transformed_code", "")
|
|
105
|
+
changes = result.get("changes", [])
|
|
106
|
+
change_count = result.get("change_count", 0)
|
|
107
|
+
|
|
108
|
+
console.print(f"\n[bold cyan]{relative_path}[/] ({change_count} changes)")
|
|
109
|
+
console.print("─" * 60)
|
|
110
|
+
|
|
111
|
+
if summary:
|
|
112
|
+
# Just show change summaries
|
|
113
|
+
for change in changes:
|
|
114
|
+
console.print(f" • {change['description']}")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Generate unified diff
|
|
118
|
+
original_lines = original.splitlines(keepends=True)
|
|
119
|
+
transformed_lines = transformed.splitlines(keepends=True)
|
|
120
|
+
|
|
121
|
+
diff_lines = list(
|
|
122
|
+
difflib.unified_diff(
|
|
123
|
+
original_lines,
|
|
124
|
+
transformed_lines,
|
|
125
|
+
fromfile=f"a/{relative_path}",
|
|
126
|
+
tofile=f"b/{relative_path}",
|
|
127
|
+
n=context,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not diff_lines:
|
|
132
|
+
console.print(" [dim]No textual differences[/]")
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Display diff with syntax highlighting
|
|
136
|
+
diff_text = "".join(diff_lines)
|
|
137
|
+
|
|
138
|
+
if no_color:
|
|
139
|
+
console.print(diff_text)
|
|
140
|
+
else:
|
|
141
|
+
# Color the diff manually for better visibility
|
|
142
|
+
for line in diff_lines:
|
|
143
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
144
|
+
console.print(f"[bold]{line.rstrip()}[/]")
|
|
145
|
+
elif line.startswith("@@"):
|
|
146
|
+
console.print(f"[cyan]{line.rstrip()}[/]")
|
|
147
|
+
elif line.startswith("+"):
|
|
148
|
+
console.print(f"[green]{line.rstrip()}[/]")
|
|
149
|
+
elif line.startswith("-"):
|
|
150
|
+
console.print(f"[red]{line.rstrip()}[/]")
|
|
151
|
+
else:
|
|
152
|
+
console.print(line.rstrip())
|
|
153
|
+
|
|
154
|
+
# Show change descriptions
|
|
155
|
+
console.print("\n[bold]Changes:[/]")
|
|
156
|
+
for change in changes:
|
|
157
|
+
console.print(f" • {change['description']}")
|
|
158
|
+
|
|
159
|
+
# Show next steps
|
|
160
|
+
console.print("\n" + "─" * 60)
|
|
161
|
+
console.print("Next steps:")
|
|
162
|
+
console.print(" [cyan]codeshift apply[/] - Apply all changes")
|
|
163
|
+
console.print(" [cyan]codeshift apply --file X[/] - Apply changes to specific file")
|
|
164
|
+
console.print(" [cyan]codeshift apply --backup[/] - Apply with backup files")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@click.command(name="show")
|
|
168
|
+
@click.argument("file_path", type=str)
|
|
169
|
+
@click.option(
|
|
170
|
+
"--path",
|
|
171
|
+
"-p",
|
|
172
|
+
type=click.Path(exists=True),
|
|
173
|
+
default=".",
|
|
174
|
+
help="Path to the project",
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--original",
|
|
178
|
+
is_flag=True,
|
|
179
|
+
help="Show original code instead of transformed",
|
|
180
|
+
)
|
|
181
|
+
def show_file(file_path: str, path: str, original: bool) -> None:
|
|
182
|
+
"""Show the full transformed (or original) code for a file.
|
|
183
|
+
|
|
184
|
+
\b
|
|
185
|
+
Examples:
|
|
186
|
+
codeshift show models.py
|
|
187
|
+
codeshift show models.py --original
|
|
188
|
+
"""
|
|
189
|
+
project_path = Path(path).resolve()
|
|
190
|
+
state = load_state(project_path)
|
|
191
|
+
|
|
192
|
+
if state is None:
|
|
193
|
+
console.print("[yellow]No pending migration found.[/]")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
results = state.get("results", [])
|
|
197
|
+
|
|
198
|
+
for result in results:
|
|
199
|
+
result_file = Path(result["file_path"])
|
|
200
|
+
relative_path = (
|
|
201
|
+
result_file.relative_to(project_path)
|
|
202
|
+
if result_file.is_relative_to(project_path)
|
|
203
|
+
else result_file
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if str(relative_path) == file_path or result_file.name == file_path:
|
|
207
|
+
code = result.get("original_code" if original else "transformed_code", "")
|
|
208
|
+
label = "Original" if original else "Transformed"
|
|
209
|
+
|
|
210
|
+
console.print(
|
|
211
|
+
Panel(
|
|
212
|
+
f"[bold]{label} code for {relative_path}[/]",
|
|
213
|
+
title="File Content",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
|
|
218
|
+
console.print(syntax)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
console.print(f"[yellow]File not found in pending changes: {file_path}[/]")
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Scan command for discovering all possible migrations in a project."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import httpx
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from codeshift.knowledge import (
|
|
15
|
+
generate_knowledge_base_sync,
|
|
16
|
+
is_tier_1_library,
|
|
17
|
+
)
|
|
18
|
+
from codeshift.scanner import DependencyParser
|
|
19
|
+
from codeshift.utils.config import ProjectConfig
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_latest_version(package: str) -> str | None:
|
|
25
|
+
"""Fetch the latest version of a package from PyPI.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
package: Package name.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Latest version string or None.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
response = httpx.get(f"https://pypi.org/pypi/{package}/json", timeout=10.0)
|
|
35
|
+
if response.status_code == 200:
|
|
36
|
+
return cast(str | None, response.json().get("info", {}).get("version"))
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_version(version_spec: str) -> str | None:
|
|
43
|
+
"""Extract a version number from a version spec.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
version_spec: Version specification (e.g., ">=1.0,<2.0").
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Extracted version or None.
|
|
50
|
+
"""
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", version_spec)
|
|
54
|
+
if match:
|
|
55
|
+
return match.group(1)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compare_versions(current: str, latest: str) -> bool:
|
|
60
|
+
"""Check if latest version is newer than current.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
current: Current version.
|
|
64
|
+
latest: Latest version.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if latest > current.
|
|
68
|
+
"""
|
|
69
|
+
from packaging.version import Version
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
return bool(Version(latest) > Version(current))
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_major_upgrade(current: str, latest: str) -> bool:
|
|
78
|
+
"""Check if this is a major version upgrade.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
current: Current version.
|
|
82
|
+
latest: Latest version.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
True if major version changed.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
current_major = int(current.split(".")[0])
|
|
89
|
+
latest_major = int(latest.split(".")[0])
|
|
90
|
+
return latest_major > current_major
|
|
91
|
+
except Exception:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@click.command()
|
|
96
|
+
@click.option(
|
|
97
|
+
"--path",
|
|
98
|
+
"-p",
|
|
99
|
+
type=click.Path(exists=True),
|
|
100
|
+
default=".",
|
|
101
|
+
help="Path to the project to scan",
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"--fetch-changes",
|
|
105
|
+
is_flag=True,
|
|
106
|
+
help="Fetch changelogs and detect breaking changes (slower but more detailed)",
|
|
107
|
+
)
|
|
108
|
+
@click.option(
|
|
109
|
+
"--major-only",
|
|
110
|
+
is_flag=True,
|
|
111
|
+
help="Only show major version upgrades",
|
|
112
|
+
)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--json-output",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
help="Output results as JSON",
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"--verbose",
|
|
120
|
+
"-v",
|
|
121
|
+
is_flag=True,
|
|
122
|
+
help="Show detailed output",
|
|
123
|
+
)
|
|
124
|
+
def scan(
|
|
125
|
+
path: str,
|
|
126
|
+
fetch_changes: bool,
|
|
127
|
+
major_only: bool,
|
|
128
|
+
json_output: bool,
|
|
129
|
+
verbose: bool,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Scan your project for possible dependency migrations.
|
|
132
|
+
|
|
133
|
+
This command analyzes your project dependencies, checks for newer versions,
|
|
134
|
+
and suggests which libraries could be upgraded.
|
|
135
|
+
|
|
136
|
+
\b
|
|
137
|
+
Examples:
|
|
138
|
+
codeshift scan
|
|
139
|
+
codeshift scan --fetch-changes
|
|
140
|
+
codeshift scan --major-only
|
|
141
|
+
codeshift scan --json-output
|
|
142
|
+
"""
|
|
143
|
+
project_path = Path(path).resolve()
|
|
144
|
+
# Load project config (currently unused, reserved for future use)
|
|
145
|
+
_ = ProjectConfig.from_pyproject(project_path)
|
|
146
|
+
|
|
147
|
+
if not json_output:
|
|
148
|
+
console.print(
|
|
149
|
+
Panel(
|
|
150
|
+
"[bold]Scanning project for possible migrations[/]\n\n" f"Path: {project_path}",
|
|
151
|
+
title="PyResolve Scan",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Parse dependencies
|
|
156
|
+
dep_parser = DependencyParser(project_path)
|
|
157
|
+
dependencies = dep_parser.parse_all()
|
|
158
|
+
|
|
159
|
+
if not dependencies:
|
|
160
|
+
if not json_output:
|
|
161
|
+
console.print("[yellow]No dependencies found in project.[/]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if not json_output:
|
|
165
|
+
console.print(f"\nFound [cyan]{len(dependencies)}[/] dependencies")
|
|
166
|
+
|
|
167
|
+
# Check for updates
|
|
168
|
+
outdated = []
|
|
169
|
+
|
|
170
|
+
with Progress(
|
|
171
|
+
SpinnerColumn(),
|
|
172
|
+
TextColumn("[progress.description]{task.description}"),
|
|
173
|
+
BarColumn(),
|
|
174
|
+
TaskProgressColumn(),
|
|
175
|
+
console=console,
|
|
176
|
+
disable=json_output,
|
|
177
|
+
) as progress:
|
|
178
|
+
task = progress.add_task("Checking for updates...", total=len(dependencies))
|
|
179
|
+
|
|
180
|
+
for dep in dependencies:
|
|
181
|
+
progress.update(task, description=f"Checking {dep.name}...")
|
|
182
|
+
|
|
183
|
+
current_version = parse_version(dep.version_spec) if dep.version_spec else None
|
|
184
|
+
latest_version = get_latest_version(dep.name)
|
|
185
|
+
|
|
186
|
+
if latest_version and current_version:
|
|
187
|
+
if compare_versions(current_version, latest_version):
|
|
188
|
+
is_major = is_major_upgrade(current_version, latest_version)
|
|
189
|
+
|
|
190
|
+
if major_only and not is_major:
|
|
191
|
+
progress.advance(task)
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
outdated.append(
|
|
195
|
+
{
|
|
196
|
+
"name": dep.name,
|
|
197
|
+
"current": current_version,
|
|
198
|
+
"latest": latest_version,
|
|
199
|
+
"is_major": is_major,
|
|
200
|
+
"is_tier1": is_tier_1_library(dep.name),
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
progress.advance(task)
|
|
205
|
+
|
|
206
|
+
if not outdated:
|
|
207
|
+
if not json_output:
|
|
208
|
+
console.print("\n[green]All dependencies are up to date![/]")
|
|
209
|
+
else:
|
|
210
|
+
print(json.dumps({"outdated": [], "migrations": []}))
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Fetch breaking changes if requested
|
|
214
|
+
migrations = []
|
|
215
|
+
|
|
216
|
+
if fetch_changes:
|
|
217
|
+
if not json_output:
|
|
218
|
+
console.print(
|
|
219
|
+
f"\n[bold]Fetching changelogs for {len(outdated)} outdated packages...[/]\n"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
with Progress(
|
|
223
|
+
SpinnerColumn(),
|
|
224
|
+
TextColumn("[progress.description]{task.description}"),
|
|
225
|
+
BarColumn(),
|
|
226
|
+
TaskProgressColumn(),
|
|
227
|
+
console=console,
|
|
228
|
+
disable=json_output,
|
|
229
|
+
) as progress:
|
|
230
|
+
task = progress.add_task("Fetching changelogs...", total=len(outdated))
|
|
231
|
+
|
|
232
|
+
for pkg in outdated:
|
|
233
|
+
progress.update(task, description=f"Analyzing {pkg['name']}...")
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
kb = generate_knowledge_base_sync(
|
|
237
|
+
package=str(pkg["name"]),
|
|
238
|
+
old_version=str(pkg["current"]),
|
|
239
|
+
new_version=str(pkg["latest"]),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
pkg["breaking_changes"] = len(kb.breaking_changes)
|
|
243
|
+
pkg["confidence"] = kb.overall_confidence.value
|
|
244
|
+
pkg["changes"] = [
|
|
245
|
+
{
|
|
246
|
+
"old_api": c.old_api,
|
|
247
|
+
"new_api": c.new_api,
|
|
248
|
+
"description": c.description,
|
|
249
|
+
"confidence": c.confidence.value,
|
|
250
|
+
}
|
|
251
|
+
for c in kb.breaking_changes[:5] # Limit to 5 changes
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
if kb.has_changes or pkg["is_tier1"]:
|
|
255
|
+
migrations.append(pkg)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
if verbose and not json_output:
|
|
259
|
+
console.print(f" [dim]Could not fetch changelog for {pkg['name']}: {e}[/]")
|
|
260
|
+
pkg["breaking_changes"] = None
|
|
261
|
+
pkg["confidence"] = "unknown"
|
|
262
|
+
|
|
263
|
+
progress.advance(task)
|
|
264
|
+
else:
|
|
265
|
+
# Without fetch_changes, suggest all tier1 and major upgrades
|
|
266
|
+
for pkg in outdated:
|
|
267
|
+
if pkg["is_tier1"] or pkg["is_major"]:
|
|
268
|
+
migrations.append(pkg)
|
|
269
|
+
|
|
270
|
+
# Output results
|
|
271
|
+
if json_output:
|
|
272
|
+
print(
|
|
273
|
+
json.dumps(
|
|
274
|
+
{
|
|
275
|
+
"outdated": outdated,
|
|
276
|
+
"migrations": migrations,
|
|
277
|
+
},
|
|
278
|
+
indent=2,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# Display results
|
|
284
|
+
console.print(f"\n[bold]Outdated Dependencies ({len(outdated)})[/]\n")
|
|
285
|
+
|
|
286
|
+
table = Table()
|
|
287
|
+
table.add_column("Package", style="cyan")
|
|
288
|
+
table.add_column("Current", justify="right")
|
|
289
|
+
table.add_column("Latest", justify="right")
|
|
290
|
+
table.add_column("Type", justify="center")
|
|
291
|
+
table.add_column("Tier", justify="center")
|
|
292
|
+
|
|
293
|
+
if fetch_changes:
|
|
294
|
+
table.add_column("Breaking Changes", justify="right")
|
|
295
|
+
table.add_column("Confidence", justify="center")
|
|
296
|
+
|
|
297
|
+
for pkg in outdated:
|
|
298
|
+
type_str = "[red]Major[/]" if pkg["is_major"] else "[yellow]Minor/Patch[/]"
|
|
299
|
+
tier_str = "[green]Tier 1[/]" if pkg["is_tier1"] else "[dim]Tier 2/3[/]"
|
|
300
|
+
|
|
301
|
+
row = [
|
|
302
|
+
pkg["name"],
|
|
303
|
+
pkg["current"],
|
|
304
|
+
pkg["latest"],
|
|
305
|
+
type_str,
|
|
306
|
+
tier_str,
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
if fetch_changes:
|
|
310
|
+
changes = pkg.get("breaking_changes")
|
|
311
|
+
if changes is not None:
|
|
312
|
+
row.append(str(changes))
|
|
313
|
+
confidence = str(pkg.get("confidence", "unknown"))
|
|
314
|
+
conf_style = {
|
|
315
|
+
"high": "[green]HIGH[/]",
|
|
316
|
+
"medium": "[yellow]MEDIUM[/]",
|
|
317
|
+
"low": "[dim]LOW[/]",
|
|
318
|
+
}.get(confidence, "[dim]?[/]")
|
|
319
|
+
row.append(conf_style)
|
|
320
|
+
else:
|
|
321
|
+
row.append("[dim]?[/]")
|
|
322
|
+
row.append("[dim]?[/]")
|
|
323
|
+
|
|
324
|
+
table.add_row(*[str(item) for item in row])
|
|
325
|
+
|
|
326
|
+
console.print(table)
|
|
327
|
+
|
|
328
|
+
# Show suggested migrations
|
|
329
|
+
if migrations:
|
|
330
|
+
console.print(f"\n[bold]Suggested Migrations ({len(migrations)})[/]\n")
|
|
331
|
+
|
|
332
|
+
for pkg in migrations:
|
|
333
|
+
tier_label = (
|
|
334
|
+
"[green](Tier 1 - deterministic)[/]"
|
|
335
|
+
if pkg["is_tier1"]
|
|
336
|
+
else "[yellow](Tier 2/3 - LLM-assisted)[/]"
|
|
337
|
+
)
|
|
338
|
+
console.print(
|
|
339
|
+
f" [cyan]{pkg['name']}[/] {pkg['current']} → {pkg['latest']} {tier_label}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if fetch_changes and pkg.get("changes"):
|
|
343
|
+
console.print(" Breaking changes:")
|
|
344
|
+
changes_list = cast(list[dict[str, Any]], pkg["changes"])
|
|
345
|
+
for change in changes_list[:3]:
|
|
346
|
+
if change["new_api"]:
|
|
347
|
+
console.print(
|
|
348
|
+
f" [dim]├──[/] {change['old_api']} → {change['new_api']}"
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
console.print(f" [dim]├──[/] {change['old_api']} [red](removed)[/]")
|
|
352
|
+
pkg_changes = cast(list[Any], pkg.get("changes", []))
|
|
353
|
+
if len(pkg_changes) > 3:
|
|
354
|
+
console.print(f" [dim]└── ... and {len(pkg_changes) - 3} more[/]")
|
|
355
|
+
|
|
356
|
+
console.print()
|
|
357
|
+
|
|
358
|
+
console.print("[bold]To migrate a package, run:[/]")
|
|
359
|
+
console.print(" [cyan]codeshift upgrade <package> --target <version>[/]\n")
|
|
360
|
+
|
|
361
|
+
# Show quick commands
|
|
362
|
+
console.print("[bold]Quick commands:[/]")
|
|
363
|
+
for pkg in migrations[:3]:
|
|
364
|
+
console.print(f" [dim]codeshift upgrade {pkg['name']} --target {pkg['latest']}[/]")
|
|
365
|
+
else:
|
|
366
|
+
console.print(
|
|
367
|
+
"\n[dim]No migrations suggested. Use --fetch-changes for detailed analysis.[/]"
|
|
368
|
+
)
|