Version 1.0

This commit is contained in:
Canon
2026-02-04 09:00:07 +08:00
commit 4c8b6cdd9f
25 changed files with 2407 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
using FCPModUpdater.Commands.Settings;
using FCPModUpdater.Services;
using FCPModUpdater.UI;
using Spectre.Console;
using Spectre.Console.Cli;
namespace FCPModUpdater.Commands;
public class ScanCommand : AsyncCommand<ModPathSettings>
{
public override async Task<int> ExecuteAsync(CommandContext context, ModPathSettings settings,
CancellationToken cancellationToken)
{
try
{
// Resolve mods directory
var modsDirectory = ResolveModsDirectory(settings);
if (modsDirectory == null)
{
return 1;
}
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
AnsiConsole.WriteLine();
// Check for git
var gitService = new GitService();
if (!await gitService.IsGitInstalledAsync(cancellationToken))
{
AnsiConsole.MarkupLine("[yellow]Warning: Git is not installed or not in PATH.[/]");
AnsiConsole.MarkupLine("[yellow]Git-based features will be limited.[/]");
AnsiConsole.MarkupLine("[grey]Install git from: https://git-scm.com/downloads[/]");
AnsiConsole.WriteLine();
}
// Initialize services
var gitHubApiService = new GitHubApiService();
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
// Run interactive menu
var menu = new InteractiveMenu(
gitService,
gitHubApiService,
modDiscoveryService,
modsDirectory);
await menu.RunAsync(cancellationToken);
return 0;
}
catch (OperationCanceledException)
{
AnsiConsole.MarkupLine("[grey]Operation cancelled.[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex);
return 1;
}
}
private static string? ResolveModsDirectory(ModPathSettings settings)
{
if (settings.ModDirectory != null)
{
return settings.ModDirectory.FullName;
}
// Auto-discover
var pathDiscovery = new PathDiscoveryService();
var paths = pathDiscovery.DiscoverModPaths();
if (paths.Count == 0)
{
AnsiConsole.MarkupLine("[red]Error: Could not find RimWorld Mods folder.[/]");
AnsiConsole.MarkupLine("[grey]Please specify the path using --directory[/]");
return null;
}
if (paths.Count == 1)
{
return paths[0];
}
// Multiple paths found - let user choose
return AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[bold]Multiple RimWorld installations found. Select one:[/]")
.PageSize(10)
.AddChoices(paths));
}
}
+24
View File
@@ -0,0 +1,24 @@
using System.ComponentModel;
using JetBrains.Annotations;
using Spectre.Console;
using Spectre.Console.Cli;
namespace FCPModUpdater.Commands.Settings;
[UsedImplicitly]
public class ModPathSettings : CommandSettings
{
[CommandOption("-d|--directory")]
[Description("Path to the RimWorld/Mods folder. If not specified, will attempt to auto-discover.")]
public DirectoryInfo? ModDirectory { get; init; }
public override ValidationResult Validate()
{
if (ModDirectory != null && !ModDirectory.Exists)
{
return ValidationResult.Error($"Directory does not exist: {ModDirectory.FullName}");
}
return ValidationResult.Success();
}
}
+128
View File
@@ -0,0 +1,128 @@
using FCPModUpdater.Commands.Settings;
using FCPModUpdater.Models;
using FCPModUpdater.Services;
using FCPModUpdater.UI;
using Spectre.Console;
using Spectre.Console.Cli;
namespace FCPModUpdater.Commands;
public class UpdateCommand : AsyncCommand<ModPathSettings>
{
public override async Task<int> ExecuteAsync(CommandContext context, ModPathSettings settings,
CancellationToken cancellationToken)
{
try
{
// Resolve mods directory
var modsDirectory = ResolveModsDirectory(settings);
if (modsDirectory == null)
{
return 1;
}
AnsiConsole.MarkupLine($"[grey]Using mods directory: {modsDirectory}[/]");
AnsiConsole.WriteLine();
// Check for git
var gitService = new GitService();
if (!await gitService.IsGitInstalledAsync(cancellationToken))
{
AnsiConsole.MarkupLine("[red]Error: Git is not installed or not in PATH.[/]");
AnsiConsole.MarkupLine("[grey]Install git from: https://git-scm.com/downloads[/]");
return 1;
}
// Initialize services
var gitHubApiService = new GitHubApiService();
var modDiscoveryService = new ModDiscoveryService(gitService, gitHubApiService);
// Discover mods
var mods = await ProgressReporter.WithStatusAsync(
"Scanning mods directory...",
async () => await modDiscoveryService.DiscoverModsAsync(modsDirectory, ct: cancellationToken));
// Find updateable mods
var updateableMods = mods
.Where(m => m.Source == ModSource.Git && m.Status == ModStatus.Behind)
.ToList();
if (updateableMods.Count == 0)
{
AnsiConsole.MarkupLine("[green]All FCP mods are up to date![/]");
return 0;
}
AnsiConsole.MarkupLine($"[yellow]Found {updateableMods.Count} mod(s) with updates available:[/]");
foreach (var mod in updateableMods)
{
AnsiConsole.MarkupLine($" • {mod.Name} [grey]({mod.CommitsBehind} commits behind)[/]");
}
AnsiConsole.WriteLine();
// Update all
var results = await ProgressReporter.WithBatchProgressAsync(
"Updating mods",
updateableMods,
m => m.Name,
async (mod, progress) =>
{
progress.Report(25);
var fetchOk = await gitService.FetchAsync(mod.Path, ct: cancellationToken);
if (!fetchOk)
return (false, "Fetch failed");
progress.Report(50);
var pullOk = await gitService.PullAsync(mod.Path, ct: cancellationToken);
progress.Report(100);
return (pullOk, pullOk ? null : "Pull failed");
});
ModTableRenderer.RenderUpdateSummary(results);
var failCount = results.Count(r => !r.Success);
return failCount > 0 ? 1 : 0;
}
catch (OperationCanceledException)
{
AnsiConsole.MarkupLine("[grey]Operation cancelled.[/]");
return 1;
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex);
return 1;
}
}
private static string? ResolveModsDirectory(ModPathSettings settings)
{
if (settings.ModDirectory != null)
{
return settings.ModDirectory.FullName;
}
// Auto-discover
var pathDiscovery = new PathDiscoveryService();
var paths = pathDiscovery.DiscoverModPaths();
if (paths.Count == 0)
{
AnsiConsole.MarkupLine("[red]Error: Could not find RimWorld Mods folder.[/]");
AnsiConsole.MarkupLine("[grey]Please specify the path using --directory[/]");
return null;
}
if (paths.Count == 1)
{
return paths[0];
}
// Multiple paths found - use first one for non-interactive mode
AnsiConsole.MarkupLine($"[yellow]Multiple installations found, using: {paths[0]}[/]");
AnsiConsole.MarkupLine("[grey]Use --directory to specify a different path[/]");
return paths[0];
}
}