diff --git a/fallout.slnx b/fallout.slnx
index ee73c4481..7e66400fd 100644
--- a/fallout.slnx
+++ b/fallout.slnx
@@ -15,6 +15,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/src/Fallout.Migrate/CodeRewriter.cs b/src/Fallout.Migrate/CodeRewriter.cs
new file mode 100644
index 000000000..aa605d167
--- /dev/null
+++ b/src/Fallout.Migrate/CodeRewriter.cs
@@ -0,0 +1,33 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System.Text.RegularExpressions;
+
+namespace Fallout.Migrate;
+
+internal static class CodeRewriter
+{
+ // Anchored prefix swap: `\bNuke\.` → `Fallout.`. Covers using directives,
+ // attribute references, qualified type names, namespace declarations.
+ // The trailing `(?=[A-Z])` lookahead avoids matching `Nuke.json` filenames
+ // or other lowercase tails the prefix audit deliberately preserved.
+ private static readonly Regex NamespacePrefix =
+ new(@"\bNuke\.(?=[A-Z])", RegexOptions.Compiled);
+
+ // Bare type renames done in the Fallout rebrand (#59).
+ private static readonly Regex NukeBuildType = new(@"\bNukeBuild\b", RegexOptions.Compiled);
+ private static readonly Regex INukeBuildType = new(@"\bINukeBuild\b", RegexOptions.Compiled);
+
+ public static RewriteResult Rewrite(string original)
+ {
+ var edits = 0;
+
+ var content = NamespacePrefix.Replace(original, _ => { edits++; return "Fallout."; });
+ content = INukeBuildType.Replace(content, _ => { edits++; return "IFalloutBuild"; });
+ content = NukeBuildType.Replace(content, _ => { edits++; return "FalloutBuild"; });
+
+ return new RewriteResult(content, edits);
+ }
+}
diff --git a/src/Fallout.Migrate/CsprojRewriter.cs b/src/Fallout.Migrate/CsprojRewriter.cs
new file mode 100644
index 000000000..681a41bcb
--- /dev/null
+++ b/src/Fallout.Migrate/CsprojRewriter.cs
@@ -0,0 +1,38 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System.Text.RegularExpressions;
+
+namespace Fallout.Migrate;
+
+internal static class CsprojRewriter
+{
+ // PackageReference / ProjectReference `Include="Nuke.X"` → `Include="Fallout.X"`.
+ private static readonly Regex PackageReferencePattern =
+ new(@"(?<=\b(?:Include|Update|Remove)="")Nuke\.(?=[A-Z])", RegexOptions.Compiled);
+
+ // MSBuild element/property names that begin with `Nuke` followed by an uppercase
+ // letter (e.g. ...). Limited to known consumer-facing names from
+ // P3.5b so we don't rewrite unrelated user-defined identifiers that happen to start
+ // with the literal "Nuke".
+ private static readonly Regex MSBuildPropertyPattern = new(
+ @"\bNuke(?=" +
+ "(?:RootDirectory|ScriptDirectory|TelemetryVersion|BaseDirectory|BaseNamespace|" +
+ "UseNestedNamespaces|RepositoryUrl|UpdateReferences|ContinueOnError|TaskTimeout|" +
+ "Timeout|TasksEnabled|DefaultExcludes|ExcludeBoot|ExcludeConfig|ExcludeLogs|" +
+ "ExcludeDirectoryBuild|ExcludeCi|SpecificationFiles|ExternalFiles|TasksAssembly|" +
+ "TasksDirectory)\\b)",
+ RegexOptions.Compiled);
+
+ public static RewriteResult Rewrite(string original)
+ {
+ var edits = 0;
+
+ var content = PackageReferencePattern.Replace(original, _ => { edits++; return "Fallout."; });
+ content = MSBuildPropertyPattern.Replace(content, _ => { edits++; return "Fallout"; });
+
+ return new RewriteResult(content, edits);
+ }
+}
diff --git a/src/Fallout.Migrate/Fallout.Migrate.csproj b/src/Fallout.Migrate/Fallout.Migrate.csproj
new file mode 100644
index 000000000..4427b3e44
--- /dev/null
+++ b/src/Fallout.Migrate/Fallout.Migrate.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+ LatestMajor
+ true
+ fallout-migrate
+ CLI that migrates an existing NUKE consumer repo to Fallout in one command.
+ build automation continuous-integration tools orchestration migration
+
+
+
+
+
+
+
diff --git a/src/Fallout.Migrate/Migration.cs b/src/Fallout.Migrate/Migration.cs
new file mode 100644
index 000000000..4d26daaf9
--- /dev/null
+++ b/src/Fallout.Migrate/Migration.cs
@@ -0,0 +1,136 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Fallout.Migrate;
+
+public sealed class Migration
+{
+ private readonly string _rootDirectory;
+ private readonly bool _dryRun;
+ private readonly TextWriter _log;
+
+ public Migration(string rootDirectory, bool dryRun, TextWriter log)
+ {
+ _rootDirectory = rootDirectory ?? throw new ArgumentNullException(nameof(rootDirectory));
+ _dryRun = dryRun;
+ _log = log ?? throw new ArgumentNullException(nameof(log));
+ }
+
+ public Summary Run()
+ {
+ var summary = new Summary();
+
+ RewriteCsprojs(summary);
+ RewriteCsFiles(summary);
+ RewriteBootstrapScripts(summary);
+ RenameNukeDirectory(summary);
+
+ return summary;
+ }
+
+ private void RewriteCsprojs(Summary summary)
+ {
+ foreach (var path in EnumerateFiles("*.csproj"))
+ ApplyRewrite(path, CsprojRewriter.Rewrite, summary);
+ }
+
+ private void RewriteCsFiles(Summary summary)
+ {
+ foreach (var path in EnumerateFiles("*.cs"))
+ ApplyRewrite(path, CodeRewriter.Rewrite, summary);
+ }
+
+ private void RewriteBootstrapScripts(Summary summary)
+ {
+ foreach (var name in new[] { "build.cmd", "build.ps1", "build.sh" })
+ {
+ var path = Path.Combine(_rootDirectory, name);
+ if (File.Exists(path))
+ ApplyRewrite(path, ScriptRewriter.Rewrite, summary);
+ }
+ }
+
+ private void RenameNukeDirectory(Summary summary)
+ {
+ var legacy = Path.Combine(_rootDirectory, ".nuke");
+ var canonical = Path.Combine(_rootDirectory, ".fallout");
+
+ if (!Directory.Exists(legacy))
+ return;
+
+ if (Directory.Exists(canonical))
+ {
+ summary.Warnings.Add(
+ "Both .nuke/ and .fallout/ exist. Skipped rename; merge their contents manually.");
+ return;
+ }
+
+ Log($"rename {RelativePath(legacy)} -> {RelativePath(canonical)}");
+ if (!_dryRun)
+ Directory.Move(legacy, canonical);
+ summary.DirectoriesRenamed++;
+ }
+
+ private IEnumerable EnumerateFiles(string pattern)
+ {
+ foreach (var file in Directory.EnumerateFiles(_rootDirectory, pattern, SearchOption.AllDirectories))
+ {
+ if (IsIgnored(file))
+ continue;
+ yield return file;
+ }
+ }
+
+ private static bool IsIgnored(string path)
+ {
+ return path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
+ || path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
+ || path.Contains($"{Path.DirectorySeparatorChar}.git{Path.DirectorySeparatorChar}", StringComparison.Ordinal);
+ }
+
+ private void ApplyRewrite(string path, Func rewriter, Summary summary)
+ {
+ string original;
+ try
+ {
+ original = File.ReadAllText(path);
+ }
+ catch (IOException ex)
+ {
+ summary.Warnings.Add($"could not read {RelativePath(path)}: {ex.Message}");
+ return;
+ }
+
+ var result = rewriter(original);
+ if (result.EditCount == 0)
+ return;
+
+ Log($"edit {RelativePath(path)} ({result.EditCount} change{(result.EditCount == 1 ? "" : "s")})");
+ summary.FilesChanged++;
+ summary.EditCount += result.EditCount;
+
+ if (!_dryRun)
+ File.WriteAllText(path, result.Content);
+ }
+
+ private string RelativePath(string absolute) =>
+ Path.GetRelativePath(_rootDirectory, absolute).Replace('\\', '/');
+
+ private void Log(string line) => _log.WriteLine(line);
+
+ public sealed class Summary
+ {
+ public int FilesChanged { get; set; }
+ public int EditCount { get; set; }
+ public int DirectoriesRenamed { get; set; }
+ public List Warnings { get; } = new();
+ }
+}
+
+public readonly record struct RewriteResult(string Content, int EditCount);
diff --git a/src/Fallout.Migrate/Program.cs b/src/Fallout.Migrate/Program.cs
new file mode 100644
index 000000000..6171beafc
--- /dev/null
+++ b/src/Fallout.Migrate/Program.cs
@@ -0,0 +1,116 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System;
+using System.IO;
+
+namespace Fallout.Migrate;
+
+public static class Program
+{
+ public static int Main(string[] args)
+ {
+ var dryRun = Array.Exists(args, a => a is "--dry-run" or "-n");
+ var helpRequested = Array.Exists(args, a => a is "--help" or "-h" or "/?");
+ var rootArg = Array.Find(args, a => !a.StartsWith('-') && !a.StartsWith('/'));
+
+ if (helpRequested)
+ {
+ PrintHelp();
+ return 0;
+ }
+
+ var rootDirectory = ResolveRootDirectory(rootArg);
+ if (rootDirectory == null)
+ {
+ Console.Error.WriteLine("error: could not locate a repository root containing a build orchestrator project (_build.csproj) under the working directory.");
+ Console.Error.WriteLine(" pass an explicit path: fallout-migrate ");
+ return 1;
+ }
+
+ Console.WriteLine($"fallout-migrate — migrating: {rootDirectory}");
+ if (dryRun)
+ Console.WriteLine("(dry-run — no files will be modified)");
+ Console.WriteLine();
+
+ var migration = new Migration(rootDirectory, dryRun, Console.Out);
+ var summary = migration.Run();
+
+ Console.WriteLine();
+ Console.WriteLine($"Files changed: {summary.FilesChanged}");
+ Console.WriteLine($"Edits made: {summary.EditCount}");
+ Console.WriteLine($"Directories: {summary.DirectoriesRenamed} renamed");
+ if (summary.Warnings.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Warnings:");
+ foreach (var w in summary.Warnings)
+ Console.WriteLine($" - {w}");
+ }
+
+ Console.WriteLine();
+ Console.WriteLine(dryRun
+ ? "Dry-run complete. Re-run without --dry-run to apply changes."
+ : "Migration complete. Verify the build: ./build.ps1 (or ./build.sh on unix)");
+ Console.WriteLine("Migration guide: https://fallout.build (see #37 for the full guide)");
+ return 0;
+ }
+
+ private static string ResolveRootDirectory(string explicitArg)
+ {
+ if (explicitArg != null)
+ return Path.GetFullPath(explicitArg);
+
+ var current = new DirectoryInfo(Environment.CurrentDirectory);
+ while (current != null)
+ {
+ if (Directory.Exists(Path.Combine(current.FullName, "build")) ||
+ Directory.Exists(Path.Combine(current.FullName, ".nuke")) ||
+ Directory.Exists(Path.Combine(current.FullName, ".fallout")))
+ {
+ return current.FullName;
+ }
+
+ // Heuristic: any _build.csproj or build.cmd / build.ps1 anywhere below.
+ if (current.GetFiles("build.cmd", SearchOption.TopDirectoryOnly).Length > 0 ||
+ current.GetFiles("build.ps1", SearchOption.TopDirectoryOnly).Length > 0 ||
+ current.GetFiles("build.sh", SearchOption.TopDirectoryOnly).Length > 0)
+ {
+ return current.FullName;
+ }
+
+ current = current.Parent;
+ }
+
+ return null;
+ }
+
+ private static void PrintHelp()
+ {
+ Console.WriteLine("""
+ fallout-migrate — migrate a NUKE consumer repo to Fallout.
+
+ Usage:
+ fallout-migrate [path] [options]
+
+ Arguments:
+ path Repository root. Defaults to walking up from the working
+ directory to find one (looking for build.cmd / build.ps1 /
+ build.sh, .nuke/, or build/).
+
+ Options:
+ --dry-run, -n Show what would change without writing.
+ --help, -h, /? Show this message.
+
+ What it does:
+ - Rewrites Nuke.* PackageReferences and MSBuild properties in .csproj
+ - Rewrites `using Nuke.*` directives and qualified type references in .cs
+ - Rewrites `dotnet nuke` → `dotnet fallout` and legacy NUKE_* env vars
+ in build.cmd / build.ps1 / build.sh
+ - Renames .nuke/ to .fallout/
+ - Prints a summary of files changed and warnings to address manually
+ """);
+ }
+}
diff --git a/src/Fallout.Migrate/ScriptRewriter.cs b/src/Fallout.Migrate/ScriptRewriter.cs
new file mode 100644
index 000000000..cdb5edcc6
--- /dev/null
+++ b/src/Fallout.Migrate/ScriptRewriter.cs
@@ -0,0 +1,35 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System.Text.RegularExpressions;
+
+namespace Fallout.Migrate;
+
+internal static class ScriptRewriter
+{
+ private static readonly (Regex Pattern, string Replacement)[] Patterns =
+ {
+ // `dotnet nuke` invocations
+ (new Regex(@"\bdotnet\s+nuke\b", RegexOptions.Compiled), "dotnet fallout"),
+ // .nuke directory references → .fallout
+ (new Regex(@"(?<=[\\/.""'\s])\.nuke(?=[\\/""'\s])", RegexOptions.Compiled), ".fallout"),
+ // Legacy env vars (consumer-facing ones from P3.5c)
+ (new Regex(@"\bNUKE_TELEMETRY_OPTOUT\b", RegexOptions.Compiled), "FALLOUT_TELEMETRY_OPTOUT"),
+ (new Regex(@"\bNUKE_GLOBAL_TOOL_VERSION\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_VERSION"),
+ (new Regex(@"\bNUKE_GLOBAL_TOOL_START_TIME\b", RegexOptions.Compiled), "FALLOUT_GLOBAL_TOOL_START_TIME"),
+ (new Regex(@"\bNUKE_INTERNAL_INTERCEPTOR\b", RegexOptions.Compiled), "FALLOUT_INTERNAL_INTERCEPTOR"),
+ };
+
+ public static RewriteResult Rewrite(string original)
+ {
+ var edits = 0;
+ var content = original;
+ foreach (var (pattern, replacement) in Patterns)
+ {
+ content = pattern.Replace(content, _ => { edits++; return replacement; });
+ }
+ return new RewriteResult(content, edits);
+ }
+}
diff --git a/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs b/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs
new file mode 100644
index 000000000..928f2fe7f
--- /dev/null
+++ b/tests/Fallout.Migrate.Tests/CodeRewriterTest.cs
@@ -0,0 +1,74 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Tests;
+
+public class CodeRewriterTest
+{
+ [Fact]
+ public void RewritesUsingDirective()
+ {
+ const string input = """
+ using Nuke.Common;
+ using Nuke.Common.IO;
+ using Fallout.Common;
+ """;
+
+ var result = CodeRewriter.Rewrite(input);
+
+ result.EditCount.Should().Be(2);
+ result.Content.Should().Contain("using Fallout.Common;");
+ result.Content.Should().Contain("using Fallout.Common.IO;");
+ }
+
+ [Fact]
+ public void RewritesQualifiedTypeReference()
+ {
+ const string input = "var x = new Nuke.Common.Tools.DotNet.DotNetTasks();";
+ var result = CodeRewriter.Rewrite(input);
+ result.EditCount.Should().Be(1);
+ result.Content.Should().Be("var x = new Fallout.Common.Tools.DotNet.DotNetTasks();");
+ }
+
+ [Fact]
+ public void RewritesNukeBuildBaseType()
+ {
+ const string input = "class Build : NukeBuild { }";
+ var result = CodeRewriter.Rewrite(input);
+ result.EditCount.Should().Be(1);
+ result.Content.Should().Be("class Build : FalloutBuild { }");
+ }
+
+ [Fact]
+ public void RewritesINukeBuildInterface()
+ {
+ const string input = "public static int IsApplicable(INukeBuild build) => 0;";
+ var result = CodeRewriter.Rewrite(input);
+ result.EditCount.Should().Be(1);
+ result.Content.Should().Be("public static int IsApplicable(IFalloutBuild build) => 0;");
+ }
+
+ [Fact]
+ public void DoesNotMatchNukeAsPartOfAnotherIdentifier()
+ {
+ // A type like `NukeAdjacentThing` must not match `\bNukeBuild\b`.
+ const string input = "var x = new NukeBuilderXYZ();";
+ var result = CodeRewriter.Rewrite(input);
+ result.EditCount.Should().Be(0);
+ result.Content.Should().Be(input);
+ }
+
+ [Fact]
+ public void DoesNotMatchLowercaseNukePrefix()
+ {
+ // ".nuke/foo" filenames stay as-is — handled by ScriptRewriter / DirectoryRenamer.
+ const string input = """var path = "/repo/.nuke/parameters.json";""";
+ var result = CodeRewriter.Rewrite(input);
+ result.EditCount.Should().Be(0);
+ }
+}
diff --git a/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs b/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs
new file mode 100644
index 000000000..5c7472bd9
--- /dev/null
+++ b/tests/Fallout.Migrate.Tests/CsprojRewriterTest.cs
@@ -0,0 +1,87 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Tests;
+
+public class CsprojRewriterTest
+{
+ [Fact]
+ public void RewritesPackageReferenceNamespace()
+ {
+ const string input = """
+
+
+
+
+
+
+ """;
+
+ var result = CsprojRewriter.Rewrite(input);
+
+ result.EditCount.Should().Be(2);
+ result.Content.Should().Contain(@"Include=""Fallout.Common""");
+ result.Content.Should().Contain(@"Include=""Fallout.Components""");
+ result.Content.Should().NotContain(@"Include=""Nuke.");
+ }
+
+ [Fact]
+ public void RewritesNukeRootDirectoryProperty()
+ {
+ const string input = """
+
+
+ .\..
+ 1
+
+
+ """;
+
+ var result = CsprojRewriter.Rewrite(input);
+
+ result.EditCount.Should().Be(4); // 2 opening + 2 closing tags
+ result.Content.Should().Contain("");
+ result.Content.Should().Contain("");
+ result.Content.Should().Contain("");
+ result.Content.Should().NotContain("");
+ }
+
+ [Fact]
+ public void LeavesUnrelatedNukePrefixedIdentifiersAlone()
+ {
+ const string input = """
+
+
+ x
+
+
+ """;
+
+ var result = CsprojRewriter.Rewrite(input);
+
+ result.EditCount.Should().Be(0);
+ result.Content.Should().Be(input);
+ }
+
+ [Fact]
+ public void ReturnsZeroEditsForUnchangedContent()
+ {
+ const string input = """
+
+
+ net10.0
+
+
+ """;
+
+ var result = CsprojRewriter.Rewrite(input);
+
+ result.EditCount.Should().Be(0);
+ result.Content.Should().Be(input);
+ }
+}
diff --git a/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj b/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj
new file mode 100644
index 000000000..c9a546105
--- /dev/null
+++ b/tests/Fallout.Migrate.Tests/Fallout.Migrate.Tests.csproj
@@ -0,0 +1,11 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
diff --git a/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs b/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs
new file mode 100644
index 000000000..42016e1d4
--- /dev/null
+++ b/tests/Fallout.Migrate.Tests/MigrationIntegrationTest.cs
@@ -0,0 +1,147 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Tests;
+
+public class MigrationIntegrationTest
+{
+ [Fact]
+ public void MigratesVanillaConsumerRepo()
+ {
+ var temp = CreateVanillaFixture();
+
+ try
+ {
+ var migration = new Migration(temp, dryRun: false, TextWriter.Null);
+ var summary = migration.Run();
+
+ // Build file rewritten end to end.
+ var buildCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj"));
+ buildCsproj.Should().Contain(@"Include=""Fallout.Common""");
+ buildCsproj.Should().Contain("");
+ buildCsproj.Should().NotContain("Nuke.Common");
+ buildCsproj.Should().NotContain("");
+
+ var buildCs = File.ReadAllText(Path.Combine(temp, "build", "Build.cs"));
+ buildCs.Should().Contain("using Fallout.Common");
+ buildCs.Should().Contain(": FalloutBuild");
+ buildCs.Should().NotContain("using Nuke.");
+ buildCs.Should().NotContain("NukeBuild");
+
+ var buildSh = File.ReadAllText(Path.Combine(temp, "build.sh"));
+ buildSh.Should().Contain("dotnet fallout");
+ buildSh.Should().Contain("FALLOUT_TELEMETRY_OPTOUT");
+ buildSh.Should().Contain(".fallout/temp");
+
+ // .nuke/ moved to .fallout/.
+ Directory.Exists(Path.Combine(temp, ".nuke")).Should().BeFalse();
+ Directory.Exists(Path.Combine(temp, ".fallout")).Should().BeTrue();
+ File.Exists(Path.Combine(temp, ".fallout", "parameters.json")).Should().BeTrue();
+
+ summary.FilesChanged.Should().BeGreaterThan(0);
+ summary.EditCount.Should().BeGreaterThan(0);
+ summary.DirectoriesRenamed.Should().Be(1);
+ summary.Warnings.Should().BeEmpty();
+ }
+ finally
+ {
+ Directory.Delete(temp, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void DryRunDoesNotWriteFiles()
+ {
+ var temp = CreateVanillaFixture();
+
+ try
+ {
+ var beforeCsproj = File.ReadAllText(Path.Combine(temp, "build", "_build.csproj"));
+ var beforeNukeDir = Directory.Exists(Path.Combine(temp, ".nuke"));
+
+ var summary = new Migration(temp, dryRun: true, TextWriter.Null).Run();
+
+ File.ReadAllText(Path.Combine(temp, "build", "_build.csproj")).Should().Be(beforeCsproj);
+ Directory.Exists(Path.Combine(temp, ".nuke")).Should().Be(beforeNukeDir);
+ summary.FilesChanged.Should().BeGreaterThan(0); // counts intended edits
+ }
+ finally
+ {
+ Directory.Delete(temp, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void WarnsWhenBothNukeAndFalloutDirectoriesExist()
+ {
+ var temp = CreateVanillaFixture();
+ Directory.CreateDirectory(Path.Combine(temp, ".fallout"));
+
+ try
+ {
+ var summary = new Migration(temp, dryRun: false, TextWriter.Null).Run();
+
+ summary.Warnings.Should().Contain(w => w.Contains(".nuke/") && w.Contains(".fallout/"));
+ summary.DirectoriesRenamed.Should().Be(0);
+ Directory.Exists(Path.Combine(temp, ".nuke")).Should().BeTrue();
+ }
+ finally
+ {
+ Directory.Delete(temp, recursive: true);
+ }
+ }
+
+ private static string CreateVanillaFixture()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "fallout-migrate-test-" + Guid.NewGuid().ToString("N")[..8]);
+ Directory.CreateDirectory(dir);
+ Directory.CreateDirectory(Path.Combine(dir, "build"));
+ Directory.CreateDirectory(Path.Combine(dir, ".nuke"));
+
+ File.WriteAllText(Path.Combine(dir, "build", "_build.csproj"), """
+
+
+ Exe
+ net8.0
+ .\..
+ 1
+
+
+
+
+
+ """);
+
+ File.WriteAllText(Path.Combine(dir, "build", "Build.cs"), """
+ using Nuke.Common;
+ using Nuke.Common.Tools.DotNet;
+
+ class Build : NukeBuild
+ {
+ public static int Main () => Execute(x => x.Compile);
+
+ Target Compile => _ => _.Executes(() => { });
+ }
+ """);
+
+ File.WriteAllText(Path.Combine(dir, "build.sh"), """
+ #!/usr/bin/env bash
+ export NUKE_TELEMETRY_OPTOUT=1
+ TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp"
+ dotnet nuke "$@"
+ """, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
+
+ File.WriteAllText(Path.Combine(dir, ".nuke", "parameters.json"), "{}");
+
+ return dir;
+ }
+}
diff --git a/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs b/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs
new file mode 100644
index 000000000..a2dc6128d
--- /dev/null
+++ b/tests/Fallout.Migrate.Tests/ScriptRewriterTest.cs
@@ -0,0 +1,51 @@
+// Copyright 2026 Maintainers of Fallout.
+// Originally based on NUKE by Matthias Koch and contributors.
+// Distributed under the MIT License.
+// https://github.com/ChrisonSimtian/Fallout/blob/main/LICENSE
+
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Tests;
+
+public class ScriptRewriterTest
+{
+ [Fact]
+ public void RewritesDotnetNukeInvocations()
+ {
+ var result = ScriptRewriter.Rewrite("dotnet nuke Compile");
+ result.EditCount.Should().Be(1);
+ result.Content.Should().Be("dotnet fallout Compile");
+ }
+
+ [Fact]
+ public void RewritesDotDirectoryReferences()
+ {
+ var result = ScriptRewriter.Rewrite("""TEMP_DIRECTORY="$SCRIPT_DIR/.nuke/temp" """);
+ result.EditCount.Should().Be(1);
+ result.Content.Should().Contain(".fallout/temp");
+ result.Content.Should().NotContain(".nuke/");
+ }
+
+ [Fact]
+ public void RewritesLegacyEnvVars()
+ {
+ const string input = """
+ export NUKE_TELEMETRY_OPTOUT=1
+ $env:NUKE_GLOBAL_TOOL_VERSION = "10.0"
+ """;
+ var result = ScriptRewriter.Rewrite(input);
+ result.EditCount.Should().Be(2);
+ result.Content.Should().Contain("FALLOUT_TELEMETRY_OPTOUT");
+ result.Content.Should().Contain("FALLOUT_GLOBAL_TOOL_VERSION");
+ }
+
+ [Fact]
+ public void LeavesPlainWordNukeAlone()
+ {
+ // The word "nuke" in a comment or string isn't a command invocation.
+ const string input = "# This was previously a NUKE-based build.";
+ var result = ScriptRewriter.Rewrite(input);
+ result.EditCount.Should().Be(0);
+ }
+}
diff --git a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs
index 1ce48f85a..50c3206f6 100644
--- a/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs
+++ b/tests/Fallout.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#Solution.g.verified.cs
@@ -17,6 +17,8 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Common
public Fallout.Common.ProjectModel.Project Fallout_Components => this.GetProject("Fallout.Components");
public Fallout.Common.ProjectModel.Project Fallout_GlobalTool => this.GetProject("Fallout.GlobalTool");
public Fallout.Common.ProjectModel.Project Fallout_GlobalTool_Tests => this.GetProject("Fallout.GlobalTool.Tests");
+ public Fallout.Common.ProjectModel.Project Fallout_Migrate => this.GetProject("Fallout.Migrate");
+ public Fallout.Common.ProjectModel.Project Fallout_Migrate_Tests => this.GetProject("Fallout.Migrate.Tests");
public Fallout.Common.ProjectModel.Project Fallout_MSBuildTasks => this.GetProject("Fallout.MSBuildTasks");
public Fallout.Common.ProjectModel.Project Fallout_ProjectModel => this.GetProject("Fallout.ProjectModel");
public Fallout.Common.ProjectModel.Project Fallout_ProjectModel_Tests => this.GetProject("Fallout.ProjectModel.Tests");