Skip to content

Commit 2f29689

Browse files
author
jarvis
committed
feat(cli): add upgrade system foundation (Sprint 1)
- Add FshManifest model for tracking project configuration and versions - Generate .fsh/manifest.json during project scaffolding - Add 'fsh version' command to show CLI and project versions - Add 'fsh upgrade' command skeleton with --check and --apply flags Sprint 1 deliverables: - [x] Manifest generation in SolutionGenerator - [x] FshManifest model with JSON serialization - [x] VersionCommand with table and JSON output - [x] UpgradeCommand skeleton with planned functionality preview The manifest tracks: - FSH framework version used - CLI version that created the project - Project options (type, architecture, database, modules) - Building blocks versions for upgrade detection Part of the CLI upgrade system - Sprint 2 will add GitHub API integration for checking available upgrades.
1 parent b95efe9 commit 2f29689

5 files changed

Lines changed: 529 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
3+
using FSH.CLI.Models;
4+
using Spectre.Console;
5+
using Spectre.Console.Cli;
6+
7+
namespace FSH.CLI.Commands;
8+
9+
/// <summary>
10+
/// Check for and apply FSH framework upgrades.
11+
/// </summary>
12+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
13+
internal sealed class UpgradeCommand : AsyncCommand<UpgradeCommand.Settings>
14+
{
15+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
16+
internal sealed class Settings : CommandSettings
17+
{
18+
[CommandOption("-p|--path")]
19+
[Description("Path to the FSH project (defaults to current directory)")]
20+
[DefaultValue(".")]
21+
public string Path { get; set; } = ".";
22+
23+
[CommandOption("--check")]
24+
[Description("Check for available upgrades without applying")]
25+
[DefaultValue(false)]
26+
public bool CheckOnly { get; set; }
27+
28+
[CommandOption("--apply")]
29+
[Description("Apply available upgrades")]
30+
[DefaultValue(false)]
31+
public bool Apply { get; set; }
32+
33+
[CommandOption("--skip-breaking")]
34+
[Description("Skip breaking changes during upgrade")]
35+
[DefaultValue(false)]
36+
public bool SkipBreaking { get; set; }
37+
38+
[CommandOption("--force")]
39+
[Description("Force upgrade even if customizations detected")]
40+
[DefaultValue(false)]
41+
public bool Force { get; set; }
42+
43+
[CommandOption("--dry-run")]
44+
[Description("Show what would be changed without making modifications")]
45+
[DefaultValue(false)]
46+
public bool DryRun { get; set; }
47+
}
48+
49+
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken)
50+
{
51+
// Validate project has manifest
52+
var manifest = FshManifest.TryLoad(settings.Path);
53+
if (manifest == null)
54+
{
55+
AnsiConsole.MarkupLine("[red]Error:[/] No FSH project found at this location.");
56+
AnsiConsole.MarkupLine("[dim]This command requires a project created with FSH CLI 10.0.0 or later.[/]");
57+
AnsiConsole.MarkupLine("[dim]The project must have a [yellow].fsh/manifest.json[/] file.[/]");
58+
return 1;
59+
}
60+
61+
// Show current status
62+
AnsiConsole.WriteLine();
63+
AnsiConsole.MarkupLine($"[blue]FSH Upgrade[/]");
64+
AnsiConsole.MarkupLine($"[dim]Project:[/] {Path.GetFullPath(settings.Path)}");
65+
AnsiConsole.MarkupLine($"[dim]Current version:[/] [yellow]{manifest.FshVersion}[/]");
66+
AnsiConsole.WriteLine();
67+
68+
// Determine mode
69+
if (!settings.CheckOnly && !settings.Apply)
70+
{
71+
// Default: show help
72+
ShowUsageHelp();
73+
return 0;
74+
}
75+
76+
if (settings.CheckOnly)
77+
{
78+
return await CheckForUpgradesAsync(manifest, settings, cancellationToken);
79+
}
80+
81+
if (settings.Apply)
82+
{
83+
return await ApplyUpgradesAsync(manifest, settings, cancellationToken);
84+
}
85+
86+
return 0;
87+
}
88+
89+
private static void ShowUsageHelp()
90+
{
91+
AnsiConsole.MarkupLine("[yellow]Usage:[/]");
92+
AnsiConsole.MarkupLine(" [green]fsh upgrade --check[/] Check for available upgrades");
93+
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply[/] Apply available upgrades");
94+
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply --skip-breaking[/] Apply safe updates only");
95+
AnsiConsole.MarkupLine(" [green]fsh upgrade --apply --dry-run[/] Preview changes without applying");
96+
AnsiConsole.WriteLine();
97+
}
98+
99+
private static Task<int> CheckForUpgradesAsync(FshManifest manifest, Settings settings, CancellationToken cancellationToken)
100+
{
101+
// TODO: Sprint 2 - Implement upgrade check
102+
// 1. Fetch latest release info from GitHub API
103+
// 2. Compare versions
104+
// 3. Show available changes
105+
106+
AnsiConsole.MarkupLine("[yellow]⚠ Upgrade check not yet implemented[/]");
107+
AnsiConsole.WriteLine();
108+
AnsiConsole.MarkupLine("[dim]Coming in Sprint 2:[/]");
109+
AnsiConsole.MarkupLine("[dim] • GitHub API integration for release fetching[/]");
110+
AnsiConsole.MarkupLine("[dim] • Version comparison logic[/]");
111+
AnsiConsole.MarkupLine("[dim] • Package diff detection[/]");
112+
AnsiConsole.WriteLine();
113+
114+
// Placeholder output showing what it will look like
115+
AnsiConsole.MarkupLine("[blue]Preview of planned output:[/]");
116+
AnsiConsole.WriteLine();
117+
118+
var panel = new Panel(
119+
"""
120+
[green]FSH Upgrade Check[/]
121+
122+
Current: [yellow]10.0.0[/]
123+
Latest: [green]10.1.0[/]
124+
125+
[blue]Changes available:[/]
126+
127+
BuildingBlocks/Web:
128+
[green]+[/] Added RateLimitingMiddleware
129+
[yellow]~[/] Modified ExceptionHandler (non-breaking)
130+
131+
Modules/Identity:
132+
[green]+[/] Added MFA support
133+
[red]![/] Breaking: IUserService signature changed
134+
135+
Directory.Packages.props:
136+
[yellow]~[/] 12 package updates
137+
138+
Run '[green]fsh upgrade --apply[/]' to upgrade.
139+
Run '[green]fsh upgrade --apply --skip-breaking[/]' for safe updates only.
140+
""")
141+
{
142+
Border = BoxBorder.Rounded,
143+
Padding = new Padding(2, 1)
144+
};
145+
146+
AnsiConsole.Write(panel);
147+
AnsiConsole.WriteLine();
148+
149+
return Task.FromResult(0);
150+
}
151+
152+
private static Task<int> ApplyUpgradesAsync(FshManifest manifest, Settings settings, CancellationToken cancellationToken)
153+
{
154+
// TODO: Sprint 3 - Implement upgrade apply
155+
// 1. Fetch latest release
156+
// 2. Update Directory.Packages.props
157+
// 3. For code changes, show diff and ask confirmation
158+
// 4. Update manifest with new versions
159+
160+
AnsiConsole.MarkupLine("[yellow]⚠ Upgrade apply not yet implemented[/]");
161+
AnsiConsole.WriteLine();
162+
AnsiConsole.MarkupLine("[dim]Coming in Sprint 3:[/]");
163+
AnsiConsole.MarkupLine("[dim] • Package version updater[/]");
164+
AnsiConsole.MarkupLine("[dim] • Safe (non-breaking) auto-apply[/]");
165+
AnsiConsole.MarkupLine("[dim] • Interactive diff viewer[/]");
166+
AnsiConsole.WriteLine();
167+
168+
if (settings.DryRun)
169+
{
170+
AnsiConsole.MarkupLine("[dim]Dry run mode - no changes would be made[/]");
171+
}
172+
173+
if (settings.SkipBreaking)
174+
{
175+
AnsiConsole.MarkupLine("[dim]Skip breaking mode - would skip breaking changes[/]");
176+
}
177+
178+
return Task.FromResult(0);
179+
}
180+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
3+
using FSH.CLI.Models;
4+
using Spectre.Console;
5+
using Spectre.Console.Cli;
6+
7+
namespace FSH.CLI.Commands;
8+
9+
/// <summary>
10+
/// Display CLI and project version information.
11+
/// </summary>
12+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
13+
internal sealed class VersionCommand : Command<VersionCommand.Settings>
14+
{
15+
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by Spectre.Console.Cli via reflection")]
16+
internal sealed class Settings : CommandSettings
17+
{
18+
[CommandOption("-p|--path")]
19+
[Description("Path to the FSH project (defaults to current directory)")]
20+
[DefaultValue(".")]
21+
public string Path { get; set; } = ".";
22+
23+
[CommandOption("--json")]
24+
[Description("Output as JSON")]
25+
[DefaultValue(false)]
26+
public bool Json { get; set; }
27+
}
28+
29+
public override int Execute(CommandContext context, Settings settings)
30+
{
31+
var cliVersion = GetCliVersion();
32+
var manifest = FshManifest.TryLoad(settings.Path);
33+
34+
if (settings.Json)
35+
{
36+
OutputJson(cliVersion, manifest);
37+
}
38+
else
39+
{
40+
OutputTable(cliVersion, manifest, settings.Path);
41+
}
42+
43+
return 0;
44+
}
45+
46+
private static void OutputTable(string cliVersion, FshManifest? manifest, string path)
47+
{
48+
AnsiConsole.WriteLine();
49+
50+
var table = new Table()
51+
.Border(TableBorder.Rounded)
52+
.AddColumn("[blue]Component[/]")
53+
.AddColumn("[blue]Version[/]");
54+
55+
table.AddRow("FSH CLI", $"[green]{cliVersion}[/]");
56+
57+
if (manifest != null)
58+
{
59+
table.AddRow("Project FSH Version", $"[green]{manifest.FshVersion}[/]");
60+
table.AddRow("Project Created", $"[dim]{manifest.CreatedAt:yyyy-MM-dd HH:mm}[/]");
61+
table.AddRow("Project Type", $"[dim]{manifest.Options.Type}[/]");
62+
table.AddRow("Architecture", $"[dim]{manifest.Options.Architecture}[/]");
63+
table.AddRow("Database", $"[dim]{manifest.Options.Database}[/]");
64+
65+
if (manifest.Options.Modules.Count > 0)
66+
{
67+
table.AddRow("Modules", $"[dim]{string.Join(", ", manifest.Options.Modules)}[/]");
68+
}
69+
70+
if (manifest.LastUpgradeAt.HasValue)
71+
{
72+
table.AddRow("Last Upgrade", $"[dim]{manifest.LastUpgradeAt:yyyy-MM-dd HH:mm}[/]");
73+
}
74+
75+
// Show building blocks versions
76+
AnsiConsole.Write(table);
77+
78+
if (manifest.Tracking.BuildingBlocks.Count > 0)
79+
{
80+
AnsiConsole.WriteLine();
81+
AnsiConsole.MarkupLine("[blue]Building Blocks:[/]");
82+
var bbTable = new Table()
83+
.Border(TableBorder.Simple)
84+
.AddColumn("Package")
85+
.AddColumn("Version");
86+
87+
foreach (var (package, version) in manifest.Tracking.BuildingBlocks)
88+
{
89+
bbTable.AddRow($"[dim]{package}[/]", version);
90+
}
91+
AnsiConsole.Write(bbTable);
92+
}
93+
}
94+
else
95+
{
96+
AnsiConsole.Write(table);
97+
AnsiConsole.WriteLine();
98+
AnsiConsole.MarkupLine($"[dim]No FSH project found at:[/] [yellow]{Path.GetFullPath(path)}[/]");
99+
AnsiConsole.MarkupLine("[dim]Run [green]fsh new[/] to create a new project.[/]");
100+
}
101+
102+
AnsiConsole.WriteLine();
103+
}
104+
105+
private static void OutputJson(string cliVersion, FshManifest? manifest)
106+
{
107+
var output = new
108+
{
109+
cliVersion,
110+
project = manifest != null ? new
111+
{
112+
fshVersion = manifest.FshVersion,
113+
createdAt = manifest.CreatedAt,
114+
type = manifest.Options.Type,
115+
architecture = manifest.Options.Architecture,
116+
database = manifest.Options.Database,
117+
modules = manifest.Options.Modules,
118+
buildingBlocks = manifest.Tracking.BuildingBlocks,
119+
lastUpgradeAt = manifest.LastUpgradeAt
120+
} : null
121+
};
122+
123+
AnsiConsole.WriteLine(System.Text.Json.JsonSerializer.Serialize(output, new System.Text.Json.JsonSerializerOptions
124+
{
125+
WriteIndented = true,
126+
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
127+
}));
128+
}
129+
130+
private static string GetCliVersion()
131+
{
132+
var assembly = typeof(VersionCommand).Assembly;
133+
var version = assembly.GetName().Version;
134+
return version?.ToString(3) ?? "1.0.0";
135+
}
136+
}

0 commit comments

Comments
 (0)