Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions src/Tools/CLI/Commands/UpgradeCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using FSH.CLI.Models;
using Spectre.Console;
using Spectre.Console.Cli;

namespace FSH.CLI.Commands;

/// <summary>
/// Check for and apply FSH framework upgrades.
/// </summary>
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
internal sealed class UpgradeCommand : AsyncCommand<UpgradeCommand.Settings>
{
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
internal sealed class Settings : CommandSettings
{
[CommandOption("-p|--path")]
[Description("Path to the FSH project (defaults to current directory)")]
[DefaultValue(".")]
public string Path { get; set; } = ".";

[CommandOption("--check")]
[Description("Check for available upgrades without applying")]
[DefaultValue(false)]
public bool CheckOnly { get; set; }

[CommandOption("--apply")]
[Description("Apply available upgrades")]
[DefaultValue(false)]
public bool Apply { get; set; }

[CommandOption("--skip-breaking")]
[Description("Skip breaking changes during upgrade")]
[DefaultValue(false)]
public bool SkipBreaking { get; set; }

[CommandOption("--force")]
[Description("Force upgrade even if customizations detected")]
[DefaultValue(false)]
public bool Force { get; set; }

[CommandOption("--dry-run")]
[Description("Show what would be changed without making modifications")]
[DefaultValue(false)]
public bool DryRun { get; set; }
}

public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
// Validate project has manifest
var manifest = FshManifest.TryLoad(settings.Path);
if (manifest == null)
{
AnsiConsole.MarkupLine("[red]Error:[/] No FSH project found at this location.");
AnsiConsole.MarkupLine("[dim]This command requires a project created with FSH CLI 10.0.0 or later.[/]");
AnsiConsole.MarkupLine("[dim]The project must have a [yellow].fsh/manifest.json[/] file.[/]");
return 1;
}

// Show current status
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[blue]FSH Upgrade[/]");
AnsiConsole.MarkupLine($"[dim]Project:[/] {Path.GetFullPath(settings.Path)}");
AnsiConsole.MarkupLine($"[dim]Current version:[/] [yellow]{manifest.FshVersion}[/]");
AnsiConsole.WriteLine();

// Determine mode
if (!settings.CheckOnly && !settings.Apply)
{
// Default: show help
ShowUsageHelp();
return 0;
}

if (settings.CheckOnly)
{
return await CheckForUpgradesAsync(manifest, settings, cancellationToken);
}

if (settings.Apply)
{
return await ApplyUpgradesAsync(manifest, settings, cancellationToken);
}

return 0;
}

private static void ShowUsageHelp()
{
AnsiConsole.MarkupLine("[yellow]Usage:[/]");
AnsiConsole.MarkupLine(" [green]fsh upgrade --check[/] Check for available upgrades");
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply[/] Apply available upgrades");
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply --skip-breaking[/] Apply safe updates only");
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply --dry-run[/] Preview changes without applying");
AnsiConsole.WriteLine();
}

private static Task<int> CheckForUpgradesAsync(FshManifest manifest, Settings settings, CancellationToken cancellationToken)

Check warning on line 99 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'cancellationToken'. (https://rules.sonarsource.com/csharp/RSPEC-1172)

Check warning on line 99 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'settings'. (https://rules.sonarsource.com/csharp/RSPEC-1172)

Check warning on line 99 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'manifest'. (https://rules.sonarsource.com/csharp/RSPEC-1172)
{
// TODO: Sprint 2 - Implement upgrade check

Check warning on line 101 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
// 1. Fetch latest release info from GitHub API
// 2. Compare versions
// 3. Show available changes

AnsiConsole.MarkupLine("[yellow]⚠ Upgrade check not yet implemented[/]");
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[dim]Coming in Sprint 2:[/]");
AnsiConsole.MarkupLine("[dim] • GitHub API integration for release fetching[/]");
AnsiConsole.MarkupLine("[dim] • Version comparison logic[/]");
AnsiConsole.MarkupLine("[dim] • Package diff detection[/]");
AnsiConsole.WriteLine();

// Placeholder output showing what it will look like
AnsiConsole.MarkupLine("[blue]Preview of planned output:[/]");
AnsiConsole.WriteLine();

var panel = new Panel(
"""
[green]FSH Upgrade Check[/]

Current: [yellow]10.0.0[/]
Latest: [green]10.1.0[/]

[blue]Changes available:[/]

BuildingBlocks/Web:
[green]+[/] Added RateLimitingMiddleware
[yellow]~[/] Modified ExceptionHandler (non-breaking)

Modules/Identity:
[green]+[/] Added MFA support
[red]![/] Breaking: IUserService signature changed

Directory.Packages.props:
[yellow]~[/] 12 package updates

Run '[green]fsh upgrade --apply[/]' to upgrade.
Run '[green]fsh upgrade --apply --skip-breaking[/]' for safe updates only.
""")
{
Border = BoxBorder.Rounded,
Padding = new Padding(2, 1)
};

AnsiConsole.Write(panel);
AnsiConsole.WriteLine();

return Task.FromResult(0);
}

private static Task<int> ApplyUpgradesAsync(FshManifest manifest, Settings settings, CancellationToken cancellationToken)

Check warning on line 152 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'cancellationToken'. (https://rules.sonarsource.com/csharp/RSPEC-1172)

Check warning on line 152 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unused method parameter 'manifest'. (https://rules.sonarsource.com/csharp/RSPEC-1172)
{
// TODO: Sprint 3 - Implement upgrade apply

Check warning on line 154 in src/Tools/CLI/Commands/UpgradeCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
// 1. Fetch latest release
// 2. Update Directory.Packages.props
// 3. For code changes, show diff and ask confirmation
// 4. Update manifest with new versions

AnsiConsole.MarkupLine("[yellow]⚠ Upgrade apply not yet implemented[/]");
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[dim]Coming in Sprint 3:[/]");
AnsiConsole.MarkupLine("[dim] • Package version updater[/]");
AnsiConsole.MarkupLine("[dim] • Safe (non-breaking) auto-apply[/]");
AnsiConsole.MarkupLine("[dim] • Interactive diff viewer[/]");
AnsiConsole.WriteLine();

if (settings.DryRun)
{
AnsiConsole.MarkupLine("[dim]Dry run mode - no changes would be made[/]");
}

if (settings.SkipBreaking)
{
AnsiConsole.MarkupLine("[dim]Skip breaking mode - would skip breaking changes[/]");
}

return Task.FromResult(0);
}
}
136 changes: 136 additions & 0 deletions src/Tools/CLI/Commands/VersionCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using FSH.CLI.Models;
using Spectre.Console;
using Spectre.Console.Cli;

namespace FSH.CLI.Commands;

/// <summary>
/// Display CLI and project version information.
/// </summary>
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
internal sealed class VersionCommand : AsyncCommand<VersionCommand.Settings>
{
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
internal sealed class Settings : CommandSettings
{
[CommandOption("-p|--path")]
[Description("Path to the FSH project (defaults to current directory)")]
[DefaultValue(".")]
public string Path { get; set; } = ".";

[CommandOption("--json")]
[Description("Output as JSON")]
[DefaultValue(false)]
public bool Json { get; set; }
}

public override Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
{
var cliVersion = GetCliVersion();
var manifest = FshManifest.TryLoad(settings.Path);

if (settings.Json)
{
OutputJson(cliVersion, manifest);
}
else
{
OutputTable(cliVersion, manifest, settings.Path);
}

return Task.FromResult(0);
}

private static void OutputTable(string cliVersion, FshManifest? manifest, string path)
{
AnsiConsole.WriteLine();

var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[blue]Component[/]")
.AddColumn("[blue]Version[/]");

table.AddRow("FSH CLI", $"[green]{cliVersion}[/]");

if (manifest != null)
{
table.AddRow("Project FSH Version", $"[green]{manifest.FshVersion}[/]");
table.AddRow("Project Created", $"[dim]{manifest.CreatedAt:yyyy-MM-dd HH:mm}[/]");
table.AddRow("Project Type", $"[dim]{manifest.Options.Type}[/]");
table.AddRow("Architecture", $"[dim]{manifest.Options.Architecture}[/]");
table.AddRow("Database", $"[dim]{manifest.Options.Database}[/]");

if (manifest.Options.Modules.Count > 0)
{
table.AddRow("Modules", $"[dim]{string.Join(", ", manifest.Options.Modules)}[/]");
}

if (manifest.LastUpgradeAt.HasValue)
{
table.AddRow("Last Upgrade", $"[dim]{manifest.LastUpgradeAt:yyyy-MM-dd HH:mm}[/]");
}

// Show building blocks versions
AnsiConsole.Write(table);

if (manifest.Tracking.BuildingBlocks.Count > 0)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[blue]Building Blocks:[/]");
var bbTable = new Table()
.Border(TableBorder.Simple)
.AddColumn("Package")
.AddColumn("Version");

foreach (var (package, version) in manifest.Tracking.BuildingBlocks)
{
bbTable.AddRow($"[dim]{package}[/]", version);
}
AnsiConsole.Write(bbTable);
}
}
else
{
AnsiConsole.Write(table);
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[dim]No FSH project found at:[/] [yellow]{Path.GetFullPath(path)}[/]");
AnsiConsole.MarkupLine("[dim]Run [green]fsh new[/] to create a new project.[/]");
}

AnsiConsole.WriteLine();
}

private static void OutputJson(string cliVersion, FshManifest? manifest)
{
var output = new
{
cliVersion,
project = manifest != null ? new
{
fshVersion = manifest.FshVersion,
createdAt = manifest.CreatedAt,
type = manifest.Options.Type,
architecture = manifest.Options.Architecture,
database = manifest.Options.Database,
modules = manifest.Options.Modules,
buildingBlocks = manifest.Tracking.BuildingBlocks,
lastUpgradeAt = manifest.LastUpgradeAt
} : null
};

AnsiConsole.WriteLine(System.Text.Json.JsonSerializer.Serialize(output, new System.Text.Json.JsonSerializerOptions

Check warning on line 123 in src/Tools/CLI/Commands/VersionCommand.cs

View workflow job for this annotation

GitHub Actions / Build

Avoid creating a new 'JsonSerializerOptions' instance for every serialization operation. Cache and reuse instances instead. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1869)
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
}));
}

private static string GetCliVersion()
{
var assembly = typeof(VersionCommand).Assembly;
var version = assembly.GetName().Version;
return version?.ToString(3) ?? "1.0.0";
}
}
Loading