diff --git a/Launcher.Next.Core/Contracts.cs b/Launcher.Next.Core/Contracts.cs index 655c4f6..00668d3 100644 --- a/Launcher.Next.Core/Contracts.cs +++ b/Launcher.Next.Core/Contracts.cs @@ -17,6 +17,9 @@ public sealed record ValidationResult(IReadOnlyList Issues) { public bool IsValid => Issues.Count == 0; public static ValidationResult Success { get; } = new([]); } public sealed record PackageProgress(string Stage, int Completed, int Total); public sealed record ActivationResult(string CurrentVersion, string? PreviousVersion); +public sealed record PackageActivationReceipt(string OperationId, string PackageId, string Version, string? PreviousVersion, DateTimeOffset ActivatedUtc); +public sealed record PackageActivationResult(string Outcome, PackageActivationReceipt? Receipt); +public sealed record PackageRollbackResult(string Outcome, string? CurrentVersion); public sealed record PackageRequirement(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity, bool AllowOfflineFallback); public sealed record CatalogPackage(PackageManifest Manifest, string SourceRoot); public sealed record PackageResolutionEvidence(string Code, string Detail); diff --git a/Launcher.Next.Infrastructure/LocalPackageStore.cs b/Launcher.Next.Infrastructure/LocalPackageStore.cs index 0ab5e08..559de1f 100644 --- a/Launcher.Next.Infrastructure/LocalPackageStore.cs +++ b/Launcher.Next.Infrastructure/LocalPackageStore.cs @@ -29,6 +29,22 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP var temp = PointerPath(id) + ".tmp"; await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(new Pointer(version, prior?.Current)), ct); File.Move(temp, PointerPath(id), true); return new(version, prior?.Current); } + public async Task ActivateImmutableAsync(ImmutableLocalPackage package, PackageRequirement requirement, CancellationToken ct) + { + if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal)) return new("Blocked", null); + var prior = await ReadPointer(package.PackageId, ct); + if (prior?.Current == package.Version) return new("AlreadyActiveExact", new(Guid.NewGuid().ToString("N"), package.PackageId, package.Version, prior.Previous, DateTimeOffset.UtcNow)); + var activated = await ActivateAsync(package.PackageId, package.Version, ct); + return new("Activated", new(Guid.NewGuid().ToString("N"), package.PackageId, activated.CurrentVersion, activated.PreviousVersion, DateTimeOffset.UtcNow)); + } + public async Task RollbackAsync(PackageActivationReceipt receipt, CancellationToken ct) + { + var current=await GetCurrentVersionAsync(receipt.PackageId,ct); + if (!string.Equals(current,receipt.Version,StringComparison.Ordinal)) return new("RollbackBlockedStateChanged",current); + if (string.IsNullOrWhiteSpace(receipt.PreviousVersion)) return new("RollbackNotApplicable",current); + var restored=await ActivateAsync(receipt.PackageId,receipt.PreviousVersion,ct); + return new("RolledBack",restored.CurrentVersion); + } public async Task RollbackAsync(string id, CancellationToken ct) { var p = await ReadPointer(id, ct); return p?.Previous is null ? null : (await ActivateAsync(id, p.Previous, ct)).CurrentVersion; } public async Task BindAsync(PackageRequirement requirement, string version, CancellationToken ct) {