Problem
Fallout.Cli implements every CLI command as a static int method on one partial class Program, split across ~10 Program.*.cs files and dispatched by reflection (Program.Handle reflects over its own int-returning methods). The partial split looks organized, but the underlying type is a god object — it owns dispatch, secrets, packages, Cake interop, navigation, config, run orchestration, and the Spectre prompts. Reflection dispatch is stringly-typed: a typo'd signature compiles and fails at runtime, and nothing is DI-able or unit-testable in isolation.
Outcome
One command = one type behind a typed IFalloutCommand, discovered by type via DI instead of reflection. The :command CLI surface and shell-function names stay exactly as they are (non-breaking).
public interface IFalloutCommand
{
string Name { get; } // e.g. "run", "setup"
int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript);
}
Each Program.X.cs partial becomes a standalone XCommand implementing IFalloutCommand; the dispatcher resolves by Name. The partial keyword disappears. Shared helpers move into injectable services in the final collapse PR.
Acceptance criteria
Approach
Incremental — one command per PR behind a transitional DelegateCommand adapter so nothing regresses. Tracked by the #394 stack.
Problem
Fallout.Cliimplements every CLI command as astatic intmethod on onepartial class Program, split across ~10Program.*.csfiles and dispatched by reflection (Program.Handlereflects over its ownint-returning methods). The partial split looks organized, but the underlying type is a god object — it owns dispatch, secrets, packages, Cake interop, navigation, config, run orchestration, and the Spectre prompts. Reflection dispatch is stringly-typed: a typo'd signature compiles and fails at runtime, and nothing is DI-able or unit-testable in isolation.Outcome
One command = one type behind a typed
IFalloutCommand, discovered by type via DI instead of reflection. The:commandCLI surface and shell-function names stay exactly as they are (non-breaking).Each
Program.X.cspartial becomes a standaloneXCommandimplementingIFalloutCommand; the dispatcher resolves byName. Thepartialkeyword disappears. Shared helpers move into injectable services in the final collapse PR.Acceptance criteria
IFalloutCommand+ a type/DI-based dispatcher replace reflection dispatchProgramcollapses to a thin entry point (nopartial)Approach
Incremental — one command per PR behind a transitional
DelegateCommandadapter so nothing regresses. Tracked by the #394 stack.