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,436 @@
|
|
|
1
|
+
"""Upgrade command for analyzing and preparing migrations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from codeshift.cli.quota import (
|
|
14
|
+
QuotaError,
|
|
15
|
+
check_quota,
|
|
16
|
+
record_usage,
|
|
17
|
+
show_quota_exceeded_message,
|
|
18
|
+
)
|
|
19
|
+
from codeshift.knowledge import (
|
|
20
|
+
Confidence,
|
|
21
|
+
GeneratedKnowledgeBase,
|
|
22
|
+
generate_knowledge_base_sync,
|
|
23
|
+
is_tier_1_library,
|
|
24
|
+
)
|
|
25
|
+
from codeshift.knowledge_base import KnowledgeBaseLoader
|
|
26
|
+
from codeshift.migrator.ast_transforms import TransformChange, TransformResult, TransformStatus
|
|
27
|
+
from codeshift.migrator.transforms.fastapi_transformer import transform_fastapi
|
|
28
|
+
from codeshift.migrator.transforms.pandas_transformer import transform_pandas
|
|
29
|
+
from codeshift.migrator.transforms.pydantic_v1_to_v2 import transform_pydantic_v1_to_v2
|
|
30
|
+
from codeshift.migrator.transforms.requests_transformer import transform_requests
|
|
31
|
+
from codeshift.migrator.transforms.sqlalchemy_transformer import transform_sqlalchemy
|
|
32
|
+
from codeshift.scanner import CodeScanner, DependencyParser
|
|
33
|
+
from codeshift.utils.config import ProjectConfig
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_state(project_path: Path) -> dict[str, Any] | None:
|
|
39
|
+
"""Load the current migration state if it exists."""
|
|
40
|
+
state_file = project_path / ".codeshift" / "state.json"
|
|
41
|
+
if state_file.exists():
|
|
42
|
+
try:
|
|
43
|
+
return cast(dict[str, Any], json.loads(state_file.read_text()))
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_state(project_path: Path, state: dict) -> None:
|
|
50
|
+
"""Save the migration state."""
|
|
51
|
+
state_dir = project_path / ".codeshift"
|
|
52
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
state_file = state_dir / "state.json"
|
|
54
|
+
state_file.write_text(json.dumps(state, indent=2, default=str))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@click.command()
|
|
58
|
+
@click.argument("library")
|
|
59
|
+
@click.option(
|
|
60
|
+
"--target",
|
|
61
|
+
"-t",
|
|
62
|
+
required=True,
|
|
63
|
+
help="Target version to upgrade to (e.g., 2.5.0)",
|
|
64
|
+
)
|
|
65
|
+
@click.option(
|
|
66
|
+
"--path",
|
|
67
|
+
"-p",
|
|
68
|
+
type=click.Path(exists=True),
|
|
69
|
+
default=".",
|
|
70
|
+
help="Path to the project to analyze",
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--file",
|
|
74
|
+
"-f",
|
|
75
|
+
type=click.Path(exists=True),
|
|
76
|
+
help="Analyze a single file instead of the entire project",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--dry-run",
|
|
80
|
+
is_flag=True,
|
|
81
|
+
help="Show what would be changed without saving state",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--verbose",
|
|
85
|
+
"-v",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
help="Show detailed output",
|
|
88
|
+
)
|
|
89
|
+
def upgrade(
|
|
90
|
+
library: str,
|
|
91
|
+
target: str,
|
|
92
|
+
path: str,
|
|
93
|
+
file: str | None,
|
|
94
|
+
dry_run: bool,
|
|
95
|
+
verbose: bool,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Analyze your codebase and propose changes for a library upgrade.
|
|
98
|
+
|
|
99
|
+
\b
|
|
100
|
+
Examples:
|
|
101
|
+
codeshift upgrade pydantic --target 2.5.0
|
|
102
|
+
codeshift upgrade pydantic -t 2.0 --file models.py
|
|
103
|
+
codeshift upgrade pydantic -t 2.0 --dry-run
|
|
104
|
+
"""
|
|
105
|
+
project_path = Path(path).resolve()
|
|
106
|
+
project_config = ProjectConfig.from_pyproject(project_path)
|
|
107
|
+
|
|
108
|
+
# Check quota before starting (allow offline for Tier 1 libraries)
|
|
109
|
+
is_tier1 = is_tier_1_library(library)
|
|
110
|
+
try:
|
|
111
|
+
check_quota("file_migrated", quantity=1, allow_offline=is_tier1)
|
|
112
|
+
except QuotaError as e:
|
|
113
|
+
show_quota_exceeded_message(e)
|
|
114
|
+
raise SystemExit(1) from e
|
|
115
|
+
|
|
116
|
+
# Load knowledge base
|
|
117
|
+
loader = KnowledgeBaseLoader()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
knowledge = loader.load(library)
|
|
121
|
+
except FileNotFoundError as e:
|
|
122
|
+
console.print(f"[red]Error:[/] {e}")
|
|
123
|
+
console.print(f"\nSupported libraries: {', '.join(loader.get_supported_libraries())}")
|
|
124
|
+
raise SystemExit(1) from e
|
|
125
|
+
|
|
126
|
+
# Check if migration is supported
|
|
127
|
+
# For now, we'll allow any version since we're doing a general migration
|
|
128
|
+
console.print(
|
|
129
|
+
Panel(
|
|
130
|
+
f"[bold]Upgrading {knowledge.display_name}[/] to version [cyan]{target}[/]\n\n"
|
|
131
|
+
f"{knowledge.description}\n"
|
|
132
|
+
f"Migration guide: {knowledge.migration_guide_url or 'N/A'}",
|
|
133
|
+
title="PyResolve Migration",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Step 1: Parse dependencies
|
|
138
|
+
with Progress(
|
|
139
|
+
SpinnerColumn(),
|
|
140
|
+
TextColumn("[progress.description]{task.description}"),
|
|
141
|
+
console=console,
|
|
142
|
+
) as progress:
|
|
143
|
+
task = progress.add_task("Checking project dependencies...", total=None)
|
|
144
|
+
|
|
145
|
+
dep_parser = DependencyParser(project_path)
|
|
146
|
+
current_dep = dep_parser.get_dependency(library)
|
|
147
|
+
|
|
148
|
+
current_version = None
|
|
149
|
+
if current_dep:
|
|
150
|
+
console.print(
|
|
151
|
+
f"Found [cyan]{library}[/] in project dependencies: {current_dep.version_spec or 'any version'}"
|
|
152
|
+
)
|
|
153
|
+
# Extract version number from spec (e.g., ">=1.0,<2.0" -> "1.0")
|
|
154
|
+
if current_dep.version_spec:
|
|
155
|
+
import re
|
|
156
|
+
|
|
157
|
+
version_match = re.search(r"(\d+\.\d+(?:\.\d+)?)", current_dep.version_spec)
|
|
158
|
+
if version_match:
|
|
159
|
+
current_version = version_match.group(1)
|
|
160
|
+
else:
|
|
161
|
+
console.print(f"[yellow]Warning:[/] {library} not found in project dependencies")
|
|
162
|
+
|
|
163
|
+
progress.update(task, completed=True)
|
|
164
|
+
|
|
165
|
+
# Step 2: Fetch knowledge sources from GitHub
|
|
166
|
+
generated_kb: GeneratedKnowledgeBase | None = None
|
|
167
|
+
|
|
168
|
+
with Progress(
|
|
169
|
+
SpinnerColumn(),
|
|
170
|
+
TextColumn("[progress.description]{task.description}"),
|
|
171
|
+
console=console,
|
|
172
|
+
) as progress:
|
|
173
|
+
task = progress.add_task("Fetching knowledge sources...", total=None)
|
|
174
|
+
|
|
175
|
+
def progress_callback(msg: str) -> None:
|
|
176
|
+
progress.update(task, description=msg)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
generated_kb = generate_knowledge_base_sync(
|
|
180
|
+
package=library,
|
|
181
|
+
old_version=current_version or "1.0",
|
|
182
|
+
new_version=target,
|
|
183
|
+
progress_callback=progress_callback,
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
if verbose:
|
|
187
|
+
console.print(f"[yellow]Warning:[/] Could not fetch knowledge sources: {e}")
|
|
188
|
+
|
|
189
|
+
progress.update(task, completed=True)
|
|
190
|
+
|
|
191
|
+
# Display detected breaking changes
|
|
192
|
+
if generated_kb and generated_kb.has_changes:
|
|
193
|
+
console.print("\n[bold]Breaking changes detected:[/]\n")
|
|
194
|
+
|
|
195
|
+
# Group by confidence
|
|
196
|
+
high_confidence = generated_kb.get_changes_by_confidence(Confidence.HIGH)
|
|
197
|
+
medium_confidence = [
|
|
198
|
+
c for c in generated_kb.breaking_changes if c.confidence == Confidence.MEDIUM
|
|
199
|
+
]
|
|
200
|
+
low_confidence = [
|
|
201
|
+
c for c in generated_kb.breaking_changes if c.confidence == Confidence.LOW
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
if high_confidence:
|
|
205
|
+
console.print(" [green]HIGH CONFIDENCE:[/]")
|
|
206
|
+
for change in high_confidence:
|
|
207
|
+
if change.new_api:
|
|
208
|
+
console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
|
|
209
|
+
else:
|
|
210
|
+
console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
|
|
211
|
+
|
|
212
|
+
if medium_confidence:
|
|
213
|
+
console.print("\n [yellow]MEDIUM CONFIDENCE:[/]")
|
|
214
|
+
for change in medium_confidence:
|
|
215
|
+
if change.new_api:
|
|
216
|
+
console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
|
|
217
|
+
else:
|
|
218
|
+
console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
|
|
219
|
+
|
|
220
|
+
if low_confidence and verbose:
|
|
221
|
+
console.print("\n [dim]LOW CONFIDENCE:[/]")
|
|
222
|
+
for change in low_confidence:
|
|
223
|
+
if change.new_api:
|
|
224
|
+
console.print(f" [dim]├──[/] {change.old_api} [dim]→[/] {change.new_api}")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f" [dim]├──[/] {change.old_api} [red](removed)[/]")
|
|
227
|
+
|
|
228
|
+
if generated_kb.sources:
|
|
229
|
+
console.print(
|
|
230
|
+
f"\n [dim]Sources: {', '.join(generated_kb.sources[:2])}{'...' if len(generated_kb.sources) > 2 else ''}[/]"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
elif generated_kb:
|
|
234
|
+
console.print("\n[dim]No breaking changes detected from changelog sources.[/]")
|
|
235
|
+
|
|
236
|
+
# Step 3: Scan for library usage
|
|
237
|
+
console.print("") # Add spacing
|
|
238
|
+
with Progress(
|
|
239
|
+
SpinnerColumn(),
|
|
240
|
+
TextColumn("[progress.description]{task.description}"),
|
|
241
|
+
console=console,
|
|
242
|
+
) as progress:
|
|
243
|
+
task = progress.add_task("Scanning for library usage...", total=None)
|
|
244
|
+
|
|
245
|
+
scanner = CodeScanner(library, exclude_patterns=project_config.exclude)
|
|
246
|
+
|
|
247
|
+
if file:
|
|
248
|
+
# Single file mode
|
|
249
|
+
file_path = Path(file).resolve()
|
|
250
|
+
imports, usages = scanner.scan_file(file_path)
|
|
251
|
+
scan_result_files = 1
|
|
252
|
+
scan_result_imports = imports
|
|
253
|
+
scan_result_usages = usages
|
|
254
|
+
scan_result_errors = []
|
|
255
|
+
else:
|
|
256
|
+
# Full project scan
|
|
257
|
+
scan_result = scanner.scan_directory(project_path)
|
|
258
|
+
scan_result_files = scan_result.files_scanned
|
|
259
|
+
scan_result_imports = scan_result.imports
|
|
260
|
+
scan_result_usages = scan_result.usages
|
|
261
|
+
scan_result_errors = scan_result.errors
|
|
262
|
+
|
|
263
|
+
progress.update(task, completed=True)
|
|
264
|
+
|
|
265
|
+
console.print(f"\nScanned [cyan]{scan_result_files}[/] files")
|
|
266
|
+
console.print(f"Found [cyan]{len(scan_result_imports)}[/] imports from {library}")
|
|
267
|
+
console.print(f"Found [cyan]{len(scan_result_usages)}[/] usages of {library} symbols")
|
|
268
|
+
|
|
269
|
+
if scan_result_errors:
|
|
270
|
+
console.print(f"[yellow]Warnings:[/] {len(scan_result_errors)} files could not be parsed")
|
|
271
|
+
if verbose:
|
|
272
|
+
for file_path, error in scan_result_errors:
|
|
273
|
+
console.print(f" - {file_path}: {error}")
|
|
274
|
+
|
|
275
|
+
if not scan_result_imports:
|
|
276
|
+
console.print(f"\n[yellow]No {library} imports found in the codebase.[/]")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Step 4: Apply transforms
|
|
280
|
+
with Progress(
|
|
281
|
+
SpinnerColumn(),
|
|
282
|
+
TextColumn("[progress.description]{task.description}"),
|
|
283
|
+
console=console,
|
|
284
|
+
) as progress:
|
|
285
|
+
task = progress.add_task("Analyzing code and proposing changes...", total=None)
|
|
286
|
+
|
|
287
|
+
# Get unique files with imports
|
|
288
|
+
files_to_transform = set()
|
|
289
|
+
for imp in scan_result_imports:
|
|
290
|
+
files_to_transform.add(imp.file_path)
|
|
291
|
+
|
|
292
|
+
results: list[TransformResult] = []
|
|
293
|
+
|
|
294
|
+
for file_path in files_to_transform:
|
|
295
|
+
try:
|
|
296
|
+
source_code = file_path.read_text()
|
|
297
|
+
|
|
298
|
+
# Select transformer based on library
|
|
299
|
+
transform_func = {
|
|
300
|
+
"pydantic": transform_pydantic_v1_to_v2,
|
|
301
|
+
"fastapi": transform_fastapi,
|
|
302
|
+
"sqlalchemy": transform_sqlalchemy,
|
|
303
|
+
"pandas": transform_pandas,
|
|
304
|
+
"requests": transform_requests,
|
|
305
|
+
}.get(library)
|
|
306
|
+
|
|
307
|
+
if transform_func:
|
|
308
|
+
transformed_code, changes = transform_func(source_code)
|
|
309
|
+
# Create TransformResult from the function output
|
|
310
|
+
result = TransformResult(
|
|
311
|
+
file_path=file_path,
|
|
312
|
+
status=TransformStatus.SUCCESS if changes else TransformStatus.NO_CHANGES,
|
|
313
|
+
original_code=source_code,
|
|
314
|
+
transformed_code=transformed_code,
|
|
315
|
+
changes=[
|
|
316
|
+
TransformChange(
|
|
317
|
+
description=c.description,
|
|
318
|
+
line_number=c.line_number,
|
|
319
|
+
original=c.original,
|
|
320
|
+
replacement=c.replacement,
|
|
321
|
+
transform_name=c.transform_name,
|
|
322
|
+
confidence=getattr(c, "confidence", 1.0),
|
|
323
|
+
)
|
|
324
|
+
for c in changes
|
|
325
|
+
],
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
console.print(f"[yellow]Warning:[/] No transformer available for {library}")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
if result.has_changes:
|
|
332
|
+
results.append(result)
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
console.print(f"[red]Error processing {file_path}:[/] {e}")
|
|
336
|
+
|
|
337
|
+
progress.update(task, completed=True)
|
|
338
|
+
|
|
339
|
+
# Step 5: Show results
|
|
340
|
+
if not results:
|
|
341
|
+
console.print(
|
|
342
|
+
f"\n[green]No changes needed![/] Your code appears to be compatible with {library} v{target}."
|
|
343
|
+
)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
console.print("\n[bold]Proposed Changes[/]")
|
|
347
|
+
|
|
348
|
+
table = Table()
|
|
349
|
+
table.add_column("File", style="cyan")
|
|
350
|
+
table.add_column("Changes", justify="right")
|
|
351
|
+
table.add_column("Status", justify="center")
|
|
352
|
+
|
|
353
|
+
total_changes = 0
|
|
354
|
+
for result in results:
|
|
355
|
+
status_style = {
|
|
356
|
+
TransformStatus.SUCCESS: "[green]Ready[/]",
|
|
357
|
+
TransformStatus.PARTIAL: "[yellow]Partial[/]",
|
|
358
|
+
TransformStatus.FAILED: "[red]Failed[/]",
|
|
359
|
+
TransformStatus.NO_CHANGES: "[dim]No changes[/]",
|
|
360
|
+
}
|
|
361
|
+
# Handle files outside project path
|
|
362
|
+
try:
|
|
363
|
+
display_path = str(result.file_path.relative_to(project_path))
|
|
364
|
+
except ValueError:
|
|
365
|
+
display_path = str(result.file_path)
|
|
366
|
+
table.add_row(
|
|
367
|
+
display_path,
|
|
368
|
+
str(result.change_count),
|
|
369
|
+
status_style.get(result.status, "[dim]Unknown[/]"),
|
|
370
|
+
)
|
|
371
|
+
total_changes += result.change_count
|
|
372
|
+
|
|
373
|
+
console.print(table)
|
|
374
|
+
console.print(f"\nTotal: [cyan]{total_changes}[/] changes across [cyan]{len(results)}[/] files")
|
|
375
|
+
|
|
376
|
+
# Show detailed changes if verbose
|
|
377
|
+
if verbose:
|
|
378
|
+
console.print("\n[bold]Change Details[/]")
|
|
379
|
+
for result in results:
|
|
380
|
+
try:
|
|
381
|
+
display_path = str(result.file_path.relative_to(project_path))
|
|
382
|
+
except ValueError:
|
|
383
|
+
display_path = str(result.file_path)
|
|
384
|
+
console.print(f"\n[cyan]{display_path}[/]:")
|
|
385
|
+
for transform_change in result.changes:
|
|
386
|
+
console.print(f" • {transform_change.description}")
|
|
387
|
+
console.print(f" [red]- {transform_change.original}[/]")
|
|
388
|
+
console.print(f" [green]+ {transform_change.replacement}[/]")
|
|
389
|
+
|
|
390
|
+
# Save state
|
|
391
|
+
if not dry_run:
|
|
392
|
+
state = {
|
|
393
|
+
"library": library,
|
|
394
|
+
"target_version": target,
|
|
395
|
+
"project_path": str(project_path),
|
|
396
|
+
"results": [
|
|
397
|
+
{
|
|
398
|
+
"file_path": str(r.file_path),
|
|
399
|
+
"original_code": r.original_code,
|
|
400
|
+
"transformed_code": r.transformed_code,
|
|
401
|
+
"change_count": r.change_count,
|
|
402
|
+
"status": r.status.value,
|
|
403
|
+
"changes": [
|
|
404
|
+
{
|
|
405
|
+
"description": c.description,
|
|
406
|
+
"line_number": c.line_number,
|
|
407
|
+
"original": c.original,
|
|
408
|
+
"replacement": c.replacement,
|
|
409
|
+
"transform_name": c.transform_name,
|
|
410
|
+
}
|
|
411
|
+
for c in r.changes
|
|
412
|
+
],
|
|
413
|
+
}
|
|
414
|
+
for r in results
|
|
415
|
+
],
|
|
416
|
+
}
|
|
417
|
+
save_state(project_path, state)
|
|
418
|
+
|
|
419
|
+
# Record usage event
|
|
420
|
+
record_usage(
|
|
421
|
+
event_type="scan",
|
|
422
|
+
library=library,
|
|
423
|
+
quantity=1,
|
|
424
|
+
metadata={
|
|
425
|
+
"files_analyzed": len(files_to_transform),
|
|
426
|
+
"changes_proposed": total_changes,
|
|
427
|
+
"target_version": target,
|
|
428
|
+
},
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
console.print("\n[dim]State saved to .codeshift/state.json[/]")
|
|
432
|
+
console.print("\nNext steps:")
|
|
433
|
+
console.print(" [cyan]codeshift diff[/] - View detailed diff of proposed changes")
|
|
434
|
+
console.print(" [cyan]codeshift apply[/] - Apply changes to your files")
|
|
435
|
+
else:
|
|
436
|
+
console.print("\n[dim]Dry run mode - no state saved[/]")
|