diff --git a/docs/plans/2026-07-16-001-three-project-parity-architecture-plan.md b/docs/plans/2026-07-16-001-three-project-parity-architecture-plan.md
index 4f144f82..be8eedb8 100644
--- a/docs/plans/2026-07-16-001-three-project-parity-architecture-plan.md
+++ b/docs/plans/2026-07-16-001-three-project-parity-architecture-plan.md
@@ -354,7 +354,7 @@ Gate itself remains **U7 / #170** until that PR lands — do not bypass it when
| Unit | Status |
|------|--------|
-| U3 CLI `--profile` + shared session polish | Deferred |
+| U3 CLI `--profile` + shared session polish | In PR (CLI `--managed`/`--no-managed`/`--profile`) |
| U4 Managed VFS / validation parity | Deferred |
| U5 Uninstall / purge GUI | Deferred |
| U6 Patcher provenance (ImmutableCheckpoint) | Deferred |
diff --git a/src/ModSync.Core/CLI/ModBuildConverter.cs b/src/ModSync.Core/CLI/ModBuildConverter.cs
index e2162b03..d898b25e 100644
--- a/src/ModSync.Core/CLI/ModBuildConverter.cs
+++ b/src/ModSync.Core/CLI/ModBuildConverter.cs
@@ -18,6 +18,8 @@
using ModSync.Core.Services;
using ModSync.Core.Services.Download;
using ModSync.Core.Services.Fomod;
+using ModSync.Core.Services.Installation;
+using ModSync.Core.Services.Settings;
using ModSync.Core.Services.Validation;
using ModSync.Core.Utility;
@@ -652,6 +654,15 @@ public class InstallOptions : BaseOptions
[Option("ignore-errors", Required = false, Default = false, HelpText = "Ignore dependency resolution errors and attempt to load components in the best possible order")]
public bool IgnoreErrors { get; set; }
+
+ [Option("managed", Required = false, Default = false, HelpText = "Force managed deployment for this install (requires --profile or an active profile in settings)")]
+ public bool Managed { get; set; }
+
+ [Option("no-managed", Required = false, Default = false, HelpText = "Force classic (non-managed) install for this run, ignoring settings.json managedDeploymentEnabled")]
+ public bool NoManaged { get; set; }
+
+ [Option("profile", Required = false, HelpText = "Install profile name for managed deployment (overrides activeProfileName for this run)")]
+ public string Profile { get; set; }
}
[Verb("set-nexus-api-key", HelpText = "Set and validate your Nexus Mods API key")]
@@ -3267,14 +3278,45 @@ await LogValidationPipelineOutputAsync(
await Logger.LogAsync("Starting installation...").ConfigureAwait(false);
await Logger.LogAsync(new string('=', 50)).ConfigureAwait(false);
+ ModSyncSettings managedSettings = ModSyncSettings.Load();
+ string managedCliError = ManagedInstallCliOverrides.Apply(
+ managedSettings,
+ opts.Managed,
+ opts.NoManaged,
+ opts.Profile);
+ if (managedCliError != null)
+ {
+ await Logger.LogErrorAsync(managedCliError).ConfigureAwait(false);
+ return 1;
+ }
+
+ bool? managedOverride = ManagedInstallCliOverrides.ResolveManagedOverride(opts.Managed, opts.NoManaged);
+ string profileOverride = string.IsNullOrWhiteSpace(opts.Profile) ? null : opts.Profile.Trim();
+ if (managedSettings.ManagedDeploymentEnabled)
+ {
+ await Logger.LogAsync(
+ $"Managed deployment enabled for profile '{managedSettings.ActiveProfileName}'."
+ ).ConfigureAwait(false);
+ }
+
ModComponent.InstallExitCode exitCode = await InstallationService.InstallAllSelectedComponentsAsync(
components,
async (currentIndex, total, componentName) =>
{
await Logger.LogAsync($"[{currentIndex + 1}/{total}] Installing: {componentName}").ConfigureAwait(false);
- }
+ },
+ cancellationToken: default,
+ profileOverride: profileOverride,
+ managedDeploymentOverride: managedOverride
).ConfigureAwait(false);
+ if (InstallationService.LastManagedInstallResult != null)
+ {
+ await Logger.LogAsync(
+ ManagedInstallSummaryFormatter.FormatWizardSummary(InstallationService.LastManagedInstallResult)
+ ).ConfigureAwait(false);
+ }
+
await Logger.LogAsync(new string('=', 50)).ConfigureAwait(false);
if (exitCode == ModComponent.InstallExitCode.Success)
diff --git a/src/ModSync.Core/Services/Installation/ManagedInstallCliOverrides.cs b/src/ModSync.Core/Services/Installation/ManagedInstallCliOverrides.cs
new file mode 100644
index 00000000..a56e7ef7
--- /dev/null
+++ b/src/ModSync.Core/Services/Installation/ManagedInstallCliOverrides.cs
@@ -0,0 +1,88 @@
+// Copyright 2021-2025 ModSync
+// Licensed under the Business Source License 1.1 (BSL 1.1).
+// See LICENSE.txt file in the project root for full license information.
+
+using System;
+
+using JetBrains.Annotations;
+
+using ModSync.Core.Services.Settings;
+
+namespace ModSync.Core.Services.Installation
+{
+ ///
+ /// Applies CLI --managed / --no-managed / --profile overrides onto
+ /// loaded without persisting them (process-local for the install).
+ ///
+ public static class ManagedInstallCliOverrides
+ {
+ ///
+ /// Merges optional CLI overrides into in place.
+ ///
+ /// Error message when flags conflict; otherwise null.
+ [CanBeNull]
+ public static string Apply(
+ [NotNull] ModSyncSettings settings,
+ bool enableManaged,
+ bool disableManaged,
+ [CanBeNull] string profileName)
+ {
+ if (settings is null)
+ {
+ throw new ArgumentNullException(nameof(settings));
+ }
+
+ if (enableManaged && disableManaged)
+ {
+ return "Cannot combine --managed and --no-managed.";
+ }
+
+ if (enableManaged)
+ {
+ settings.ManagedDeploymentEnabled = true;
+ }
+ else if (disableManaged)
+ {
+ settings.ManagedDeploymentEnabled = false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(profileName))
+ {
+ settings.ActiveProfileName = profileName.Trim();
+ }
+
+ if (settings.ManagedDeploymentEnabled && string.IsNullOrWhiteSpace(settings.ActiveProfileName))
+ {
+ return ManagedInstallSession.MissingActiveProfileMessage
+ + " Pass --profile (or activate a profile in Settings).";
+ }
+
+ return null;
+ }
+
+ ///
+ /// Resolves the managed-enabled override for :
+ /// true/false when CLI forces a value; null to keep settings.json.
+ ///
+ [CanBeNull]
+ public static bool? ResolveManagedOverride(bool enableManaged, bool disableManaged)
+ {
+ if (enableManaged && disableManaged)
+ {
+ return null;
+ }
+
+ if (enableManaged)
+ {
+ return true;
+ }
+
+ if (disableManaged)
+ {
+ return false;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/ModSync.Core/Services/InstallationService.cs b/src/ModSync.Core/Services/InstallationService.cs
index 7e4d6591..2de54ed1 100644
--- a/src/ModSync.Core/Services/InstallationService.cs
+++ b/src/ModSync.Core/Services/InstallationService.cs
@@ -43,12 +43,23 @@ public class InstallationService
///
private static async Task RunWithManagedInstallSessionAsync(
[NotNull] Func> installAction,
- [CanBeNull] string profileOverride = null)
+ [CanBeNull] string profileOverride = null,
+ bool? managedDeploymentOverride = null)
{
ManagedInstallSession managedSession = null;
try
{
ModSyncSettings settings = ModSyncSettings.Load();
+ if (managedDeploymentOverride.HasValue)
+ {
+ settings.ManagedDeploymentEnabled = managedDeploymentOverride.Value;
+ }
+
+ if (!string.IsNullOrWhiteSpace(profileOverride))
+ {
+ settings.ActiveProfileName = profileOverride.Trim();
+ }
+
var profileService = new ProfileService(ModSyncSettings.GetSettingsDirectory());
managedSession = ManagedInstallSession.TryCreate(settings, profileService, profileOverride);
ManagedInstallSession.Current = managedSession;
@@ -968,7 +979,9 @@ private static string FormatHolopatcherError(Exception ex, string errorType)
public static async Task InstallSingleComponentAsync(
[NotNull] ModComponent component,
[NotNull][ItemNotNull] IReadOnlyList allComponents,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken = default,
+ [CanBeNull] string profileOverride = null,
+ bool? managedDeploymentOverride = null)
{
if (component is null)
{
@@ -982,7 +995,8 @@ private static string FormatHolopatcherError(Exception ex, string errorType)
return await RunWithManagedInstallSessionAsync(
() => InstallSingleComponentCoreAsync(component, allComponents, cancellationToken),
- profileOverride: null).ConfigureAwait(false);
+ profileOverride,
+ managedDeploymentOverride).ConfigureAwait(false);
}
private static async Task InstallSingleComponentCoreAsync(
@@ -1033,7 +1047,8 @@ await Logger.LogErrorAsync(
[NotNull][ItemNotNull] List allComponents,
[CanBeNull] Action progressCallback = null,
CancellationToken cancellationToken = default,
- [CanBeNull] string profileOverride = null)
+ [CanBeNull] string profileOverride = null,
+ bool? managedDeploymentOverride = null)
{
if (allComponents is null)
{
@@ -1042,7 +1057,8 @@ await Logger.LogErrorAsync(
return await RunWithManagedInstallSessionAsync(
() => InstallAllSelectedComponentsCoreAsync(allComponents, progressCallback, cancellationToken),
- profileOverride).ConfigureAwait(false);
+ profileOverride,
+ managedDeploymentOverride).ConfigureAwait(false);
}
private static async Task InstallAllSelectedComponentsCoreAsync(
@@ -1219,14 +1235,20 @@ await Logger.LogWarningAsync(
[NotNull][ItemNotNull] IReadOnlyList allComponents,
[CanBeNull] Action progressCallback = null,
CancellationToken cancellationToken = default,
- [CanBeNull] string profileOverride = null)
+ [CanBeNull] string profileOverride = null,
+ bool? managedDeploymentOverride = null)
{
if (allComponents is null)
{
throw new ArgumentNullException(nameof(allComponents));
}
- return InstallAllSelectedComponentsAsync(allComponents.ToList(), progressCallback, cancellationToken, profileOverride);
+ return InstallAllSelectedComponentsAsync(
+ allComponents.ToList(),
+ progressCallback,
+ cancellationToken,
+ profileOverride,
+ managedDeploymentOverride);
}
}
diff --git a/src/ModSync.Tests/ManagedInstallCliOverridesTests.cs b/src/ModSync.Tests/ManagedInstallCliOverridesTests.cs
new file mode 100644
index 00000000..fb8bb5ed
--- /dev/null
+++ b/src/ModSync.Tests/ManagedInstallCliOverridesTests.cs
@@ -0,0 +1,96 @@
+// Copyright 2021-2025 ModSync
+// Licensed under the Business Source License 1.1 (BSL 1.1).
+// See LICENSE.txt file in the project root for full license information.
+
+using ModSync.Core.Services.Installation;
+using ModSync.Core.Services.Settings;
+
+using NUnit.Framework;
+
+namespace ModSync.Tests
+{
+ [TestFixture]
+ public sealed class ManagedInstallCliOverridesTests
+ {
+ [Test]
+ public void Apply_EnableManaged_SetsFlagAndRequiresProfile()
+ {
+ var settings = new ModSyncSettings();
+ string error = ManagedInstallCliOverrides.Apply(settings, enableManaged: true, disableManaged: false, profileName: null);
+
+ Assert.That(settings.ManagedDeploymentEnabled, Is.True);
+ Assert.That(error, Does.Contain("no active profile"));
+ }
+
+ [Test]
+ public void Apply_EnableManagedWithProfile_Succeeds()
+ {
+ var settings = new ModSyncSettings { ActiveProfileName = "Old" };
+ string error = ManagedInstallCliOverrides.Apply(
+ settings,
+ enableManaged: true,
+ disableManaged: false,
+ profileName: "CLI Profile");
+
+ Assert.That(error, Is.Null);
+ Assert.That(settings.ManagedDeploymentEnabled, Is.True);
+ Assert.That(settings.ActiveProfileName, Is.EqualTo("CLI Profile"));
+ }
+
+ [Test]
+ public void Apply_DisableManaged_ClearsManagedEvenIfSettingsHadItOn()
+ {
+ var settings = new ModSyncSettings
+ {
+ ManagedDeploymentEnabled = true,
+ ActiveProfileName = "Alpha",
+ };
+
+ string error = ManagedInstallCliOverrides.Apply(
+ settings,
+ enableManaged: false,
+ disableManaged: true,
+ profileName: null);
+
+ Assert.That(error, Is.Null);
+ Assert.That(settings.ManagedDeploymentEnabled, Is.False);
+ }
+
+ [Test]
+ public void Apply_ConflictingFlags_ReturnsError()
+ {
+ var settings = new ModSyncSettings();
+ string error = ManagedInstallCliOverrides.Apply(
+ settings,
+ enableManaged: true,
+ disableManaged: true,
+ profileName: "X");
+
+ Assert.That(error, Does.Contain("--managed").And.Contain("--no-managed"));
+ }
+
+ [Test]
+ public void ResolveManagedOverride_MapsFlags()
+ {
+ Assert.That(ManagedInstallCliOverrides.ResolveManagedOverride(true, false), Is.True);
+ Assert.That(ManagedInstallCliOverrides.ResolveManagedOverride(false, true), Is.False);
+ Assert.That(ManagedInstallCliOverrides.ResolveManagedOverride(false, false), Is.Null);
+ Assert.That(ManagedInstallCliOverrides.ResolveManagedOverride(true, true), Is.Null);
+ }
+
+ [Test]
+ public void Apply_ProfileOnly_UpdatesNameWithoutForcingManaged()
+ {
+ var settings = new ModSyncSettings { ManagedDeploymentEnabled = false };
+ string error = ManagedInstallCliOverrides.Apply(
+ settings,
+ enableManaged: false,
+ disableManaged: false,
+ profileName: "Beta");
+
+ Assert.That(error, Is.Null);
+ Assert.That(settings.ManagedDeploymentEnabled, Is.False);
+ Assert.That(settings.ActiveProfileName, Is.EqualTo("Beta"));
+ }
+ }
+}