diff --git a/src/Fallout.Cli/Program.Complete.cs b/src/Fallout.Cli/Commands/CompleteCommand.cs
similarity index 81%
rename from src/Fallout.Cli/Program.Complete.cs
rename to src/Fallout.Cli/Commands/CompleteCommand.cs
index d1af3e1fb..8c81d6890 100644
--- a/src/Fallout.Cli/Program.Complete.cs
+++ b/src/Fallout.Cli/Commands/CompleteCommand.cs
@@ -7,13 +7,18 @@
using Fallout.Utilities.Text.Yaml;
using static Fallout.Common.Constants;
-namespace Fallout.Cli;
+namespace Fallout.Cli.Commands;
-partial class Program
+///
+/// fallout :complete: emits shell-completion candidates for the partially typed command line.
+///
+public sealed class CompleteCommand : IFalloutCommand
{
private const string CommandName = "fallout";
- public static int Complete(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
+ public string Name => "complete";
+
+ public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
{
if (rootDirectory == null)
return 0;
diff --git a/src/Fallout.Cli/Program.cs b/src/Fallout.Cli/Program.cs
index 25c5839e1..b713e6154 100644
--- a/src/Fallout.Cli/Program.cs
+++ b/src/Fallout.Cli/Program.cs
@@ -54,6 +54,7 @@ private static void RegisterCommands(IServiceCollection services)
// Real command types — issue #392 converts one legacy handler per PR.
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.
@@ -62,7 +63,6 @@ private static void RegisterCommands(IServiceCollection services)
services.AddSingleton(new DelegateCommand("add-package", AddPackage));
services.AddSingleton(new DelegateCommand("cake-convert", CakeConvert));
services.AddSingleton(new DelegateCommand("cake-clean", CakeClean));
- services.AddSingleton(new DelegateCommand("complete", Complete));
services.AddSingleton(new DelegateCommand("get-configuration", GetConfiguration));
services.AddSingleton(new DelegateCommand("secrets", Secrets));
services.AddSingleton(new DelegateCommand("GetNextDirectory", GetNextDirectory));
diff --git a/tests/Fallout.Cli.Tests/Commands/CompleteCommandTests.cs b/tests/Fallout.Cli.Tests/Commands/CompleteCommandTests.cs
new file mode 100644
index 000000000..33ac3d7eb
--- /dev/null
+++ b/tests/Fallout.Cli.Tests/Commands/CompleteCommandTests.cs
@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+using Fallout.Cli.Commands;
+using Fallout.Common.IO;
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Cli.Tests.Commands;
+
+public class CompleteCommandTests
+{
+ [Fact]
+ public void Name_IsComplete()
+ => new CompleteCommand().Name.Should().Be("complete");
+
+ [Fact]
+ public void Execute_WithoutRootDirectory_ReturnsZero()
+ => new CompleteCommand().Execute(["fallout "], rootDirectory: null, buildScript: null).Should().Be(0);
+
+ [Fact]
+ public void Execute_WordNotStartingWithCommandName_ReturnsZero()
+ {
+ var dir = (AbsolutePath)Path.Combine(Path.GetTempPath(), "fallout-complete-" + Guid.NewGuid().ToString("N"));
+ dir.CreateDirectory();
+ try
+ {
+ // Completion only fires for the `fallout` command line; anything else short-circuits to 0.
+ new CompleteCommand().Execute(["notfallout foo"], dir, buildScript: null).Should().Be(0);
+ }
+ finally
+ {
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+}