diff --git a/src/Fallout.Cli/Commands/Navigation/GetNextDirectoryCommand.cs b/src/Fallout.Cli/Commands/Navigation/GetNextDirectoryCommand.cs
new file mode 100644
index 000000000..1ee9ace2e
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/GetNextDirectoryCommand.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Linq;
+using Fallout.Common;
+using Fallout.Common.IO;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+/// fallout :GetNextDirectory: prints (and consumes) the queued next directory.
+public sealed class GetNextDirectoryCommand : IFalloutCommand
+{
+ public string Name => "GetNextDirectory";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ {
+ var content = NavigationSession.SessionFile.Existing()?.ReadAllLines();
+ if (content == null || string.IsNullOrWhiteSpace(content[0]))
+ {
+ Console.WriteLine(EnvironmentInfo.WorkingDirectory);
+ return 1;
+ }
+
+ var nextDirectory = content[0];
+ content[0] = string.Empty;
+ NavigationSession.SessionFile.WriteAllLines(content);
+ Console.WriteLine(nextDirectory);
+ return 0;
+ }
+}
diff --git a/src/Fallout.Cli/Commands/Navigation/NavigationSession.cs b/src/Fallout.Cli/Commands/Navigation/NavigationSession.cs
new file mode 100644
index 000000000..395740663
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/NavigationSession.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Fallout.Common;
+using Fallout.Common.IO;
+using Fallout.Common.Utilities;
+using static Fallout.Common.Constants;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+///
+/// Shared per-terminal-session state for the directory-navigation commands. The companion shell
+/// functions invoke the commands by name (preserved on conversion):
+///
+/// function nuke- { nuke :PopDirectory; cd $(nuke :GetNextDirectory) }
+/// function nuke/ { nuke :PushWithChosenRootDirectory; cd $(nuke :GetNextDirectory) }
+/// function nuke. { nuke :PushWithCurrentRootDirectory; cd $(nuke :GetNextDirectory) }
+/// function nuke.. { nuke :PushWithParentRootDirectory; cd $(nuke :GetNextDirectory) }
+///
+///
+internal static class NavigationSession
+{
+ public static string SessionId
+ => EnvironmentInfo.Platform switch
+ {
+ PlatformFamily.OSX => EnvironmentInfo.GetVariable("TERM_SESSION_ID").NotNull()[7..],
+ PlatformFamily.Windows => EnvironmentInfo.GetVariable("WT_SESSION").NotNull(),
+ _ => throw new NotSupportedException($"{EnvironmentInfo.Platform} has no session id selector.")
+ };
+
+ public static AbsolutePath SessionFile => GlobalTemporaryDirectory / $"nuke-{SessionId}.dat";
+
+ public static int PushAndSetNext(Func directoryProvider)
+ {
+ try
+ {
+ var content = SessionFile.Existing()?.ReadAllLines().ToList() ?? new List { null };
+ content[0] = directoryProvider.Invoke();
+ content.Insert(index: 1, EnvironmentInfo.WorkingDirectory);
+ SessionFile.WriteAllLines(content);
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ Console.Error.WriteLine(exception.Message);
+ return 1;
+ }
+ }
+}
diff --git a/src/Fallout.Cli/Commands/Navigation/PopDirectoryCommand.cs b/src/Fallout.Cli/Commands/Navigation/PopDirectoryCommand.cs
new file mode 100644
index 000000000..5095cfee8
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/PopDirectoryCommand.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Linq;
+using Fallout.Common.IO;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+/// fallout :PopDirectory: pops the previous directory back to the front of the queue.
+public sealed class PopDirectoryCommand : IFalloutCommand
+{
+ public string Name => "PopDirectory";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ {
+ var content = NavigationSession.SessionFile.Existing()?.ReadAllLines().ToList();
+ if (content == null || content.Count <= 1)
+ {
+ Console.Error.WriteLine("No previous directory");
+ return 1;
+ }
+
+ content[0] = content[1];
+ content.RemoveAt(1);
+ NavigationSession.SessionFile.WriteAllLines(content);
+ return 0;
+ }
+}
diff --git a/src/Fallout.Cli/Commands/Navigation/PushWithChosenRootDirectoryCommand.cs b/src/Fallout.Cli/Commands/Navigation/PushWithChosenRootDirectoryCommand.cs
new file mode 100644
index 000000000..94428b646
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/PushWithChosenRootDirectoryCommand.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Fallout.Cli.Prompts;
+using Fallout.Common;
+using Fallout.Common.IO;
+using static Fallout.Common.Constants;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+///
+/// fallout :PushWithChosenRootDirectory: prompts for a discovered root directory and queues it.
+///
+public sealed class PushWithChosenRootDirectoryCommand : IFalloutCommand
+{
+ private readonly IConsolePrompts _prompts;
+
+ public PushWithChosenRootDirectoryCommand(IConsolePrompts prompts) => _prompts = prompts;
+
+ public string Name => "PushWithChosenRootDirectory";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ {
+ return NavigationSession.PushAndSetNext(() =>
+ {
+ var directories = EnvironmentInfo.WorkingDirectory.GlobDirectories($"**/{FalloutDirectoryName}")
+ .Concat(EnvironmentInfo.WorkingDirectory.GlobFiles($"**/{FalloutDirectoryName}"))
+ .Where(x => !x.Equals(EnvironmentInfo.WorkingDirectory))
+ .Select(x => x.Parent)
+ .Select(x => (x, EnvironmentInfo.WorkingDirectory.GetRelativePathTo(x).ToString()))
+ .OrderBy(x => x.Item2).ToArray();
+
+ return _prompts.PromptForChoice("Where to go next?", directories);
+ });
+ }
+}
diff --git a/src/Fallout.Cli/Commands/Navigation/PushWithCurrentRootDirectoryCommand.cs b/src/Fallout.Cli/Commands/Navigation/PushWithCurrentRootDirectoryCommand.cs
new file mode 100644
index 000000000..4c2557212
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/PushWithCurrentRootDirectoryCommand.cs
@@ -0,0 +1,16 @@
+using Fallout.Common;
+using Fallout.Common.IO;
+using Fallout.Common.Utilities;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+/// fallout :PushWithCurrentRootDirectory: queues the current root directory.
+public sealed class PushWithCurrentRootDirectoryCommand : IFalloutCommand
+{
+ public string Name => "PushWithCurrentRootDirectory";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ {
+ return NavigationSession.PushAndSetNext(() => rootDirectory.NotNull("No root directory"));
+ }
+}
diff --git a/src/Fallout.Cli/Commands/Navigation/PushWithParentRootDirectoryCommand.cs b/src/Fallout.Cli/Commands/Navigation/PushWithParentRootDirectoryCommand.cs
new file mode 100644
index 000000000..ad5165a48
--- /dev/null
+++ b/src/Fallout.Cli/Commands/Navigation/PushWithParentRootDirectoryCommand.cs
@@ -0,0 +1,19 @@
+using System.IO;
+using Fallout.Common;
+using Fallout.Common.IO;
+using Fallout.Common.Utilities;
+using static Fallout.Common.Constants;
+
+namespace Fallout.Cli.Commands.Navigation;
+
+/// fallout :PushWithParentRootDirectory: queues the parent repository's root directory.
+public sealed class PushWithParentRootDirectoryCommand : IFalloutCommand
+{
+ public string Name => "PushWithParentRootDirectory";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ {
+ return NavigationSession.PushAndSetNext(() => TryGetRootDirectoryFrom(Path.GetDirectoryName(rootDirectory.NotNull("No root directory")))
+ .NotNull("No parent root directory"));
+ }
+}
diff --git a/src/Fallout.Cli/Program.Navigation.cs b/src/Fallout.Cli/Program.Navigation.cs
deleted file mode 100644
index 71f8e53a5..000000000
--- a/src/Fallout.Cli/Program.Navigation.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using Fallout.Common;
-using Fallout.Common.IO;
-using static Fallout.Common.Constants;
-
-namespace Fallout.Cli;
-
-partial class Program
-{
- // function nuke- { nuke :PopDirectory; cd $(nuke :GetNextDirectory) }
- // function nuke/ { nuke :PushWithChosenRootDirectory; cd $(nuke :GetNextDirectory) }
- // function nuke. { nuke :PushWithCurrentRootDirectory; cd $(nuke :GetNextDirectory) }
- // function nuke.. { nuke :PushWithParentRootDirectory; cd $(nuke :GetNextDirectory) }
-
- private static string SessionId
- => EnvironmentInfo.Platform switch
- {
- PlatformFamily.OSX => EnvironmentInfo.GetVariable("TERM_SESSION_ID").NotNull()[7..],
- PlatformFamily.Windows => EnvironmentInfo.GetVariable("WT_SESSION").NotNull(),
- _ => throw new NotSupportedException($"{EnvironmentInfo.Platform} has no session id selector.")
- };
-
- private static AbsolutePath SessionFile => GlobalTemporaryDirectory / $"nuke-{SessionId}.dat";
-
- private static int GetNextDirectory(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
- {
- var content = SessionFile.Existing()?.ReadAllLines();
- if (content == null || string.IsNullOrWhiteSpace(content[0]))
- {
- Console.WriteLine(EnvironmentInfo.WorkingDirectory);
- return 1;
- }
-
- var nextDirectory = content[0];
- content[0] = string.Empty;
- SessionFile.WriteAllLines(content);
- Console.WriteLine(nextDirectory);
- return 0;
- }
-
- private static int PopDirectory(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
- {
- var content = SessionFile.Existing()?.ReadAllLines().ToList();
- if (content == null || content.Count <= 1)
- {
- Console.Error.WriteLine("No previous directory");
- return 1;
- }
-
- content[0] = content[1];
- content.RemoveAt(1);
- SessionFile.WriteAllLines(content);
- return 0;
- }
-
- private static int PushWithCurrentRootDirectory(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
- {
- return PushAndSetNext(() => rootDirectory.NotNull("No root directory"));
- }
-
- private static int PushWithParentRootDirectory(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
- {
- return PushAndSetNext(() => TryGetRootDirectoryFrom(Path.GetDirectoryName(rootDirectory.NotNull("No root directory")))
- .NotNull("No parent root directory"));
- }
-
- private static int PushWithChosenRootDirectory(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
- {
- return PushAndSetNext(() =>
- {
- var directories = EnvironmentInfo.WorkingDirectory.GlobDirectories($"**/{FalloutDirectoryName}")
- .Concat(EnvironmentInfo.WorkingDirectory.GlobFiles($"**/{FalloutDirectoryName}"))
- .Where(x => !x.Equals(EnvironmentInfo.WorkingDirectory))
- .Select(x => x.Parent)
- .Select(x => (x, EnvironmentInfo.WorkingDirectory.GetRelativePathTo(x).ToString()))
- .OrderBy(x => x.Item2).ToArray();
-
- return PromptForChoice("Where to go next?", directories);
- });
- }
-
- private static int PushAndSetNext(Func directoryProvider)
- {
- try
- {
- var content = SessionFile.Existing()?.ReadAllLines().ToList() ?? new List { null };
- content[0] = directoryProvider.Invoke();
- content.Insert(index: 1, EnvironmentInfo.WorkingDirectory);
- SessionFile.WriteAllLines(content);
- return 0;
- }
- catch (Exception exception)
- {
- Console.Error.WriteLine(exception.Message);
- return 1;
- }
- }
-}
diff --git a/src/Fallout.Cli/Program.cs b/src/Fallout.Cli/Program.cs
index 7548a71d2..b5f165af2 100644
--- a/src/Fallout.Cli/Program.cs
+++ b/src/Fallout.Cli/Program.cs
@@ -71,11 +71,14 @@ private static void RegisterCommands(IServiceCollection services)
// Legacy handlers still living on Program, adapted until they are extracted into command
// types. Each conversion deletes one line here plus its Program.X.cs partial.
- services.AddSingleton(new DelegateCommand("GetNextDirectory", GetNextDirectory));
- services.AddSingleton(new DelegateCommand("PopDirectory", PopDirectory));
- services.AddSingleton(new DelegateCommand("PushWithCurrentRootDirectory", PushWithCurrentRootDirectory));
- services.AddSingleton(new DelegateCommand("PushWithParentRootDirectory", PushWithParentRootDirectory));
- services.AddSingleton(new DelegateCommand("PushWithChosenRootDirectory", PushWithChosenRootDirectory));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Legacy handlers still living on Program, adapted until they are extracted into command
+ // types. Each conversion deletes one line here plus its Program.X.cs partial.
}
internal static void PrintInfo()