@@ -255,28 +255,276 @@ await AnsiConsole.Status()
255255
256256 private static async Task < int > ApplyUpgradesAsync ( FshManifest manifest , Settings settings , CancellationToken cancellationToken )
257257 {
258- // TODO: Sprint 3 - Implement upgrade apply
259- // 1. Fetch latest release
260- // 2. Update Directory.Packages.props
261- // 3. For code changes, show diff and ask confirmation
262- // 4. Update manifest with new versions
258+ using var githubService = new GitHubReleaseService ( ) ;
259+
260+ // Fetch latest release
261+ GitHubRelease ? latestRelease = null ;
262+ await AnsiConsole . Status ( )
263+ . Spinner ( Spinner . Known . Dots )
264+ . StartAsync ( "Fetching latest release..." , async ctx =>
265+ {
266+ if ( settings . IncludePrerelease )
267+ {
268+ var releases = await githubService . GetReleasesAsync ( 10 , cancellationToken ) ;
269+ latestRelease = releases . FirstOrDefault ( ) ;
270+ }
271+ else
272+ {
273+ latestRelease = await githubService . GetLatestReleaseAsync ( cancellationToken ) ;
274+ }
275+ } ) ;
276+
277+ if ( latestRelease == null )
278+ {
279+ AnsiConsole . MarkupLine ( "[red]Error:[/] Could not fetch release information from GitHub." ) ;
280+ return 1 ;
281+ }
282+
283+ var latestVersion = latestRelease . Version ;
284+ var comparison = VersionComparer . CompareVersions ( manifest . FshVersion , latestVersion ) ;
285+
286+ if ( comparison >= 0 )
287+ {
288+ AnsiConsole . MarkupLine ( "[green]✓[/] Already up to date!" ) ;
289+ return 0 ;
290+ }
291+
292+ AnsiConsole . MarkupLine ( $ "[dim]Upgrading:[/] [yellow]{ manifest . FshVersion } [/] → [green]{ latestVersion } [/]") ;
293+ AnsiConsole . WriteLine ( ) ;
294+
295+ // Get package diff
296+ var currentPackagesProps = await GetLocalPackagesPropsAsync ( settings . Path ) ;
297+ var latestPackagesProps = await githubService . GetPackagesPropsAsync ( latestRelease . TagName , cancellationToken ) ;
298+
299+ if ( currentPackagesProps == null )
300+ {
301+ AnsiConsole . MarkupLine ( "[red]Error:[/] Could not read Directory.Packages.props" ) ;
302+ return 1 ;
303+ }
304+
305+ if ( latestPackagesProps == null )
306+ {
307+ AnsiConsole . MarkupLine ( "[red]Error:[/] Could not fetch latest Directory.Packages.props from GitHub" ) ;
308+ return 1 ;
309+ }
310+
311+ var currentVersions = VersionComparer . ParsePackagesProps ( currentPackagesProps ) ;
312+ var latestVersions = VersionComparer . ParsePackagesProps ( latestPackagesProps ) ;
313+ var diff = VersionComparer . Compare ( currentVersions , latestVersions ) ;
314+
315+ if ( ! diff . HasChanges )
316+ {
317+ AnsiConsole . MarkupLine ( "[dim]No package changes detected.[/]" ) ;
318+
319+ // Still update manifest version
320+ if ( ! settings . DryRun )
321+ {
322+ await PackageUpdater . UpdateManifestAsync ( settings . Path , latestVersion , cancellationToken ) ;
323+ AnsiConsole . MarkupLine ( "[green]✓[/] Updated manifest version." ) ;
324+ }
325+ return 0 ;
326+ }
263327
264- AnsiConsole . MarkupLine ( "[yellow]⚠ Upgrade apply not yet implemented[/]" ) ;
328+ // Show what will be changed
329+ AnsiConsole . MarkupLine ( "[blue]Changes to apply:[/]" ) ;
265330 AnsiConsole . WriteLine ( ) ;
266- AnsiConsole . MarkupLine ( "[dim]Coming in Sprint 3:[/]" ) ;
267- AnsiConsole . MarkupLine ( "[dim] • Package version updater[/]" ) ;
268- AnsiConsole . MarkupLine ( "[dim] • Safe (non-breaking) auto-apply[/]" ) ;
269- AnsiConsole . MarkupLine ( "[dim] • Interactive diff viewer[/]" ) ;
331+
332+ if ( diff . Updated . Count > 0 )
333+ {
334+ var updateTable = new Table ( )
335+ . Border ( TableBorder . Simple )
336+ . AddColumn ( "Package" )
337+ . AddColumn ( "From" )
338+ . AddColumn ( "To" )
339+ . AddColumn ( "Status" ) ;
340+
341+ foreach ( var update in diff . Updated . OrderBy ( u => u . Package ) )
342+ {
343+ var willSkip = settings . SkipBreaking && update . IsBreaking ;
344+
345+ string status ;
346+ if ( ! update . IsBreaking )
347+ status = "[green]Safe[/]" ;
348+ else if ( willSkip )
349+ status = "[yellow]Skip (breaking)[/]" ;
350+ else
351+ status = "[red]Breaking[/]" ;
352+
353+ var packageName = willSkip ? $ "[strikethrough dim]{ update . Package } [/]" : update . Package ;
354+
355+ updateTable . AddRow (
356+ packageName ,
357+ update . FromVersion ,
358+ $ "[green]{ update . ToVersion } [/]",
359+ status ) ;
360+ }
361+
362+ AnsiConsole . Write ( updateTable ) ;
363+ }
364+
365+ if ( diff . Added . Count > 0 )
366+ {
367+ AnsiConsole . WriteLine ( ) ;
368+ AnsiConsole . MarkupLine ( $ "[green]+[/] { diff . Added . Count } new packages will be added") ;
369+ }
370+
371+ if ( diff . Removed . Count > 0 )
372+ {
373+ AnsiConsole . WriteLine ( ) ;
374+ AnsiConsole . MarkupLine ( $ "[yellow]![/] { diff . Removed . Count } packages are no longer in the latest release (manual review needed)") ;
375+ }
376+
270377 AnsiConsole . WriteLine ( ) ;
271378
379+ // Dry run mode - stop here
272380 if ( settings . DryRun )
273381 {
274- AnsiConsole . MarkupLine ( "[dim]Dry run mode - no changes would be made[/]" ) ;
382+ AnsiConsole . MarkupLine ( "[yellow]Dry run mode[/] - no changes were made." ) ;
383+ return 0 ;
275384 }
276385
277- if ( settings . SkipBreaking )
386+ // Confirm unless forced
387+ if ( ! settings . Force )
278388 {
279- AnsiConsole . MarkupLine ( "[dim]Skip breaking mode - would skip breaking changes[/]" ) ;
389+ var confirm = await AnsiConsole . ConfirmAsync ( "Apply these changes?" , false , cancellationToken ) ;
390+ if ( ! confirm )
391+ {
392+ AnsiConsole . MarkupLine ( "[dim]Cancelled.[/]" ) ;
393+ return 0 ;
394+ }
395+ }
396+
397+ // Create backup
398+ AnsiConsole . MarkupLine ( "[dim]Creating backup...[/]" ) ;
399+ var backupPath = await PackageUpdater . CreateBackupAsync ( settings . Path , cancellationToken ) ;
400+
401+ if ( backupPath == null )
402+ {
403+ AnsiConsole . MarkupLine ( "[yellow]⚠[/] Could not create backup. Continue anyway?" ) ;
404+ if ( ! settings . Force )
405+ {
406+ var continueAnyway = await AnsiConsole . ConfirmAsync ( "Continue without backup?" , false , cancellationToken ) ;
407+ if ( ! continueAnyway )
408+ {
409+ AnsiConsole . MarkupLine ( "[dim]Cancelled.[/]" ) ;
410+ return 0 ;
411+ }
412+ }
413+ }
414+ else
415+ {
416+ AnsiConsole . MarkupLine ( $ "[dim]Backup created:[/] { backupPath } ") ;
417+ }
418+
419+ // Apply updates
420+ var updateOptions = new UpdateOptions
421+ {
422+ DryRun = false ,
423+ SkipBreaking = settings . SkipBreaking ,
424+ Force = settings . Force
425+ } ;
426+
427+ UpdateResult result ;
428+ await AnsiConsole . Status ( )
429+ . Spinner ( Spinner . Known . Dots )
430+ . StartAsync ( "Applying updates..." , async ctx =>
431+ {
432+ result = await PackageUpdater . UpdatePackagesPropsAsync (
433+ settings . Path ,
434+ diff ,
435+ updateOptions ,
436+ cancellationToken ) ;
437+ } ) ;
438+
439+ result = await PackageUpdater . UpdatePackagesPropsAsync (
440+ settings . Path ,
441+ diff ,
442+ updateOptions ,
443+ cancellationToken ) ;
444+
445+ // Show results
446+ AnsiConsole . WriteLine ( ) ;
447+
448+ if ( result . Success )
449+ {
450+ AnsiConsole . MarkupLine ( "[green]✓[/] Packages updated successfully!" ) ;
451+ AnsiConsole . WriteLine ( ) ;
452+
453+ if ( result . Updated . Count > 0 )
454+ {
455+ AnsiConsole . MarkupLine ( $ "[green]Updated:[/] { result . Updated . Count } packages") ;
456+ }
457+
458+ if ( result . Added . Count > 0 )
459+ {
460+ AnsiConsole . MarkupLine ( $ "[green]Added:[/] { result . Added . Count } packages") ;
461+ }
462+
463+ if ( result . Skipped . Count > 0 )
464+ {
465+ AnsiConsole . MarkupLine ( $ "[yellow]Skipped:[/] { result . Skipped . Count } packages (breaking changes)") ;
466+ }
467+
468+ // Update manifest
469+ var manifestUpdated = await PackageUpdater . UpdateManifestAsync ( settings . Path , latestVersion , cancellationToken ) ;
470+ if ( manifestUpdated )
471+ {
472+ AnsiConsole . MarkupLine ( "[green]✓[/] Manifest updated." ) ;
473+ }
474+
475+ // Show warnings
476+ if ( result . Warnings . Count > 0 )
477+ {
478+ AnsiConsole . WriteLine ( ) ;
479+ AnsiConsole . MarkupLine ( "[yellow]Warnings:[/]" ) ;
480+ foreach ( var warning in result . Warnings )
481+ {
482+ AnsiConsole . MarkupLine ( $ " [yellow]![/] { warning } ") ;
483+ }
484+ }
485+
486+ // Next steps
487+ AnsiConsole . WriteLine ( ) ;
488+ AnsiConsole . MarkupLine ( "[dim]Next steps:[/]" ) ;
489+ AnsiConsole . MarkupLine ( " 1. Run [green]dotnet restore[/] to restore packages" ) ;
490+ AnsiConsole . MarkupLine ( " 2. Run [green]dotnet build[/] to verify the upgrade" ) ;
491+ AnsiConsole . MarkupLine ( " 3. Review and test your application" ) ;
492+
493+ if ( backupPath != null )
494+ {
495+ AnsiConsole . WriteLine ( ) ;
496+ AnsiConsole . MarkupLine ( $ "[dim]To rollback:[/] restore from { backupPath } ") ;
497+ }
498+ }
499+ else
500+ {
501+ AnsiConsole . MarkupLine ( "[red]✗[/] Upgrade failed!" ) ;
502+
503+ foreach ( var error in result . Errors )
504+ {
505+ AnsiConsole . MarkupLine ( $ " [red]Error:[/] { error } ") ;
506+ }
507+
508+ // Offer to restore backup
509+ if ( backupPath != null )
510+ {
511+ AnsiConsole . WriteLine ( ) ;
512+ var restore = await AnsiConsole . ConfirmAsync ( "Restore from backup?" , true , cancellationToken ) ;
513+ if ( restore )
514+ {
515+ var restored = await PackageUpdater . RestoreBackupAsync ( backupPath , cancellationToken ) ;
516+ if ( restored )
517+ {
518+ AnsiConsole . MarkupLine ( "[green]✓[/] Restored from backup." ) ;
519+ }
520+ else
521+ {
522+ AnsiConsole . MarkupLine ( $ "[red]✗[/] Could not restore. Manual restore needed from: { backupPath } ") ;
523+ }
524+ }
525+ }
526+
527+ return 1 ;
280528 }
281529
282530 return 0 ;
0 commit comments