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
10 changes: 5 additions & 5 deletions src/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ await Task.Run(() => Parallel.ForEach(itemsToRemove, uniqueId =>
// Passed to DependencyResolver.Sort so cross-type dependsOn references
// (e.g. a service depending on an app) are validated without error.
var globalResourceIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var r in config.Apps) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.EnvVars) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.Dotfiles) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.Apps) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.EnvVars) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.Dotfiles) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.RegistryTweaks) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.Services) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.Services) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);
foreach (var r in config.ScheduledTasks) if (r.ResourceId is not null) globalResourceIds.Add(r.ResourceId);

// Install Apps
Expand Down Expand Up @@ -734,4 +734,4 @@ private async Task<bool> WaitForNetwork(int timeoutSeconds = 30, CancellationTok
return false;
}
}
}
}
1 change: 1 addition & 0 deletions src/Infrastructure/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static void ConfigureServices(IConfiguration configuration, IServiceColle
// Domain Services
services.AddSingleton<IConfigValidator, ConfigValidator>();
services.AddSingleton<IConfigBackupService, ConfigBackupService>();
services.AddSingleton<IConfigDriftService, ConfigDriftService>();
services.AddSingleton<IDotfileService, DotfileService>();
services.AddSingleton<IRegistryService, RegistryService>();
services.AddSingleton<ISystemSettingsService, SystemSettingsService>();
Expand Down
36 changes: 30 additions & 6 deletions src/Infrastructure/CliBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ public static class CliBuilder
/// <summary>Constructs the root command with all options, subcommands (run, generate, state, completion), and their handlers.</summary>
/// <param name="runAction">Handler for the default run action (applies configuration).</param>
/// <param name="generateAction">Handler for the generate subcommand (generates config from system state).</param>
/// <param name="configBackupAction">Handler for configuration backup and restore.</param>
/// <param name="configAction">Handler for configuration backup and restore.</param>
/// <param name="stateAction">Handler for the state subcommand (manages tracking state).</param>
/// <returns>The configured root <see cref="RootCommand"/>.</returns>
public static RootCommand BuildRootCommand(
Func<FileInfo, bool, string?, bool, bool, bool, bool, bool, bool, LogLevel, Task<int>> runAction,
Func<FileInfo?, LogLevel, Task<int>> generateAction,
Func<string, string?, LogLevel, Task<int>> stateAction,
Func<string, string?, string?, LogLevel, Task<int>>? configBackupAction = null)
Func<string, string?, string?, LogLevel, Task<int>>? configAction = null)
{
var configOption = new Option<FileInfo>("--config");
configOption.Description = "Path to the YAML configuration file";
Expand Down Expand Up @@ -276,9 +276,9 @@ public static RootCommand BuildRootCommand(
var provider = result.GetValue(providerArgument)!;
var output = result.GetValue(backupOutputOption)!;

return configBackupAction == null
return configAction == null
? 1
: await configBackupAction(
: await configAction(
provider,
output,
null,
Expand All @@ -300,18 +300,42 @@ public static RootCommand BuildRootCommand(
{
var input = result.GetValue(restoreInput)!;

return configBackupAction == null
return configAction == null
? 1
: await configBackupAction(
: await configAction(
"restore",
input,
null,
ComputeLogLevel(false, false));
});

var configDriftCommand = new Command("drift");
configDriftCommand.Description = "Detect configuration drift";

var driftInput = new Argument<string>("input")
{
Description = "Backup file path"
};

configDriftCommand.Arguments.Add(driftInput);

configDriftCommand.SetAction(async (ParseResult result) =>
{
var input = result.GetValue(driftInput)!;

return configAction == null
? 1
: await configAction(
"drift",
input,
null,
ComputeLogLevel(false, false));
});


configCommand.Subcommands.Add(configBackupCommand);
configCommand.Subcommands.Add(configRestoreCommand);
configCommand.Subcommands.Add(configDriftCommand);

rootCommand.Add(configCommand);

Expand Down
1 change: 1 addition & 0 deletions src/Interfaces/IConfigBackupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public interface IConfigBackupService
Task BackupAsync(
string provider,
Configuration config,
string sourcePath,
string output);

Task<(string Provider, object? Settings)> RestoreAsync(
Expand Down
9 changes: 9 additions & 0 deletions src/Interfaces/IConfigDriftService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using WinHome.Models;

namespace WinHome.Interfaces
{
public interface IConfigDriftService
{
Task<List<ConfigDriftResult>> DetectDriftAsync(string backupFile);
}
}
12 changes: 12 additions & 0 deletions src/Models/ConfigDriftResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace WinHome.Models;

public class ConfigDriftResult
{
public string Provider { get; set; } = "";

public string Key { get; set; } = "";

public string? Expected { get; set; }

public string? Actual { get; set; }
}
46 changes: 23 additions & 23 deletions src/Models/ResourceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@

namespace WinHome.Models
{
/// <summary>
/// Base class for all resource config types that participate in the
/// dependency graph. Provides optional <see cref="ResourceId"/> and
/// <see cref="DependsOn"/> fields without touching existing functional
/// properties (e.g. AppConfig.Id which is the package identifier).
/// </summary>
public abstract class ResourceBase
{
/// <summary>
/// Base class for all resource config types that participate in the
/// dependency graph. Provides optional <see cref="ResourceId"/> and
/// <see cref="DependsOn"/> fields without touching existing functional
/// properties (e.g. AppConfig.Id which is the package identifier).
/// Optional unique identifier for this resource within the config file.
/// Used by other resources to reference this one in <see cref="DependsOn"/>.
/// Must be unique across ALL resources in the config if specified.
/// </summary>
public abstract class ResourceBase
{
/// <summary>
/// Optional unique identifier for this resource within the config file.
/// Used by other resources to reference this one in <see cref="DependsOn"/>.
/// Must be unique across ALL resources in the config if specified.
/// </summary>
[YamlMember(Alias = "resourceId")]
[JsonPropertyName("resourceId")]
public string? ResourceId { get; set; }
[YamlMember(Alias = "resourceId")]
[JsonPropertyName("resourceId")]
public string? ResourceId { get; set; }

/// <summary>
/// Optional list of <see cref="ResourceId"/> values that must be fully
/// applied before this resource is processed.
/// </summary>
[YamlMember(Alias = "dependsOn")]
[JsonPropertyName("dependsOn")]
public List<string>? DependsOn { get; set; }
}
}
/// <summary>
/// Optional list of <see cref="ResourceId"/> values that must be fully
/// applied before this resource is processed.
/// </summary>
[YamlMember(Alias = "dependsOn")]
[JsonPropertyName("dependsOn")]
public List<string>? DependsOn { get; set; }
}
}
22 changes: 22 additions & 0 deletions src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,30 @@ static async Task<int> Main(string[] args)
logger.SetMinLevel(minLogLevel);

var backupService = host.Services.GetRequiredService<IConfigBackupService>();
var driftService = host.Services.GetRequiredService<IConfigDriftService>();

try
{
if (provider == "drift")
{
var drifts = await driftService.DetectDriftAsync(path!);

if (drifts.Count == 0)
{
logger.LogSuccess("[Config] No configuration drift detected.");
return 0;
}

foreach (var drift in drifts)
{
logger.LogWarning(
$"[DRIFT] {drift.Provider}.{drift.Key} Expected='{drift.Expected}' Actual='{drift.Actual}'");
}

logger.LogWarning($"[Config] {drifts.Count} drift(s) detected.");

return 0;
}
if (provider == "restore")
{
var (restoredProvider, restoredSettings) =
Expand Down Expand Up @@ -234,6 +255,7 @@ static async Task<int> Main(string[] args)
await backupService.BackupAsync(
provider,
config,
configFile.FullName,
path!);

logger.LogSuccess($"[Config] Backup created: {path}");
Expand Down
16 changes: 8 additions & 8 deletions src/Services/Bootstrappers/ChocolateyBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ public void Install(bool dryRun)

Console.WriteLine($"[Bootstrapper] {Name} installed successfully.");
// Issue #392 Fix: Refresh the environment PATH for the current process so it can see the newly installed manager
if (OperatingSystem.IsWindows())
{
string userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
string machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? "";
string newPath = $"{machinePath};{userPath}";
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
}
if (OperatingSystem.IsWindows())
{
string userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
string machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? "";
string newPath = $"{machinePath};{userPath}";
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
}

}
}
}
16 changes: 8 additions & 8 deletions src/Services/Bootstrappers/ScoopBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,13 @@ public void Install(bool dryRun)

Console.WriteLine($"[Bootstrapper] {Name} installed successfully.");
// Issue #392 Fix: Refresh the environment PATH for the current process so it can see the newly installed manager
if (OperatingSystem.IsWindows())
{
string userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
string machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? "";
string newPath = $"{machinePath};{userPath}";
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
}
if (OperatingSystem.IsWindows())
{
string userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? "";
string machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? "";
string newPath = $"{machinePath};{userPath}";
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
}
}
}
}
}
Loading
Loading