Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"version": "0.2.0",
"configurations": [
{
// Prompts you for the command/args each launch, e.g. ":get-configuration" or ":bogus".
"name": "fallout (ask for args)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-cli",
"program": "${workspaceFolder}/src/Fallout.Cli/bin/Debug/net10.0/Fallout.Cli.dll",
"args": "${input:falloutArgs}",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false
},
{
// Dispatch error path — prints the command manifest, no side effects.
"name": "fallout :bogus",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-cli",
"program": "${workspaceFolder}/src/Fallout.Cli/bin/Debug/net10.0/Fallout.Cli.dll",
"args": [":bogus"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
},
{
// Real command via the DelegateCommand path — read-only.
"name": "fallout :get-configuration",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-cli",
"program": "${workspaceFolder}/src/Fallout.Cli/bin/Debug/net10.0/Fallout.Cli.dll",
"args": [":get-configuration"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
],
"inputs": [
{
"id": "falloutArgs",
"type": "promptString",
"description": "Arguments to pass to fallout (space-separated), e.g. ':get-configuration'",
"default": ":bogus"
}
]
}
17 changes: 17 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-cli",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Fallout.Cli/Fallout.Cli.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="Humanizer" Version="3.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="10.0.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.7.115" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
Expand Down
69 changes: 69 additions & 0 deletions src/Fallout.Cli/CommandDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Fallout.Cli.Commands;
using Fallout.Cli.Prompts;
using Fallout.Common;
using Fallout.Common.IO;
using Fallout.Common.Utilities;
using Fallout.Common.Utilities.Collections;

namespace Fallout.Cli;

/// <summary>
/// Routes a command-line invocation to the matching <see cref="IFalloutCommand"/>. Commands are
/// resolved by <see cref="IFalloutCommand.Name"/> from the registered set — dash- and
/// case-insensitively, preserving every spelling the historical reflection dispatch accepted
/// (e.g. <c>:add-package</c> and <c>:addpackage</c>, <c>:PopDirectory</c> and <c>:popdirectory</c>).
/// </summary>
internal sealed class CommandDispatcher
{
private const char CommandPrefix = ':';

private readonly IReadOnlyList<IFalloutCommand> _commands;
private readonly IConsolePrompts _prompts;

public CommandDispatcher(IEnumerable<IFalloutCommand> commands, IConsolePrompts prompts)
{
_commands = commands.ToList();
_prompts = prompts;
}

public int Dispatch(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
{
var hasCommand = args.FirstOrDefault()?.StartsWithOrdinalIgnoreCase(CommandPrefix.ToString()) ?? false;
if (hasCommand)
{
var token = args.First().Trim(CommandPrefix);
if (string.IsNullOrWhiteSpace(token))
Assert.Fail($"No command specified. Usage is: fallout {CommandPrefix}<command> [args]");

var command = Resolve(token);
return command.Execute(args.Skip(count: 1).ToArray(), rootDirectory, buildScript);
}

if (rootDirectory == null)
{
return _prompts.PromptForConfirmation(
$"Could not find {Constants.FalloutDirectoryName} directory/file. Do you want to setup a build?")
? GetRequired("setup").Execute(Array.Empty<string>(), rootDirectory: null, buildScript: null)
: 0;
}

// TODO: docker

return GetRequired("run").Execute(args, rootDirectory, BuildProjectResolver.Resolve(rootDirectory));
}

private IFalloutCommand Resolve(string token)
{
return _commands.SingleOrDefault(x => Normalize(x.Name).EqualsOrdinalIgnoreCase(Normalize(token)))
.NotNull(new[] { $"Command '{token}' is not supported, available commands are:" }
.Concat(_commands.Select(x => $" - {x.Name}").OrderBy(x => x)).JoinNewLine());
}

private IFalloutCommand GetRequired(string name)
=> _commands.Single(x => x.Name.EqualsOrdinalIgnoreCase(name));

private static string Normalize(string value) => value.Replace("-", string.Empty);
}
27 changes: 27 additions & 0 deletions src/Fallout.Cli/Commands/DelegateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using Fallout.Common.IO;

namespace Fallout.Cli.Commands;

/// <summary>
/// Transitional adapter that exposes a not-yet-extracted <c>Program.X</c> handler as an
/// <see cref="IFalloutCommand"/>, so the dispatcher can route every command uniformly through the
/// registry while the per-command conversion (issue #392) lands one PR at a time. Each conversion
/// replaces one registration of this adapter with a real command type; the adapter is deleted once
/// the last legacy handler is gone.
/// </summary>
internal sealed class DelegateCommand : IFalloutCommand
{
private readonly Func<string[], AbsolutePath, AbsolutePath, int> _handler;

public DelegateCommand(string name, Func<string[], AbsolutePath, AbsolutePath, int> handler)
{
Name = name;
_handler = handler;
}

public string Name { get; }

public int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
=> _handler(args, rootDirectory, buildScript);
}
30 changes: 30 additions & 0 deletions src/Fallout.Cli/Commands/IFalloutCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Fallout.Common.IO;

namespace Fallout.Cli.Commands;

/// <summary>
/// A single global-tool command (e.g. <c>fallout :run</c>, <c>fallout :setup</c>).
/// One command = one type, resolved by <see cref="Name"/> from the dependency-injection
/// container — replacing the historical reflection-over-<c>Program</c> dispatch.
/// </summary>
/// <remarks>
/// This surface is intentionally minimal. It is <b>not</b> a stable public plugin contract yet;
/// when a public command SDK lands (milestone #7) the API will be annotated and versioned
/// explicitly. Until then, treat additions here as internal-by-convention.
/// </remarks>
public interface IFalloutCommand
{
/// <summary>
/// The canonical command name as typed after the <c>:</c> prefix, in dash form
/// (e.g. <c>"run"</c>, <c>"add-package"</c>, <c>"cake-convert"</c>). Matched case-insensitively.
/// </summary>
Comment on lines +18 to +20
string Name { get; }

/// <summary>
/// Executes the command and returns the process exit code.
/// </summary>
/// <param name="args">The arguments following the command token.</param>
/// <param name="rootDirectory">The resolved repository root, or <c>null</c> when none was found.</param>
/// <param name="buildScript">The resolved build script / project file, or <c>null</c> when none applies.</param>
int Execute(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
using Fallout.Common.Utilities;
using static Fallout.Common.Constants;

namespace Fallout.Cli;
namespace Fallout.Cli.Commands;

partial class Program
/// <summary>
/// <c>fallout :run</c> (and the default, command-less invocation): builds the build project and
/// runs it, forwarding any remaining arguments to the build.
/// </summary>
public sealed class RunCommand : IFalloutCommand
{
private static int Run(string[] forwardedArgs, AbsolutePath rootDirectory, AbsolutePath buildProjectFile)
public string Name => "run";

public int Execute(string[] forwardedArgs, AbsolutePath rootDirectory, AbsolutePath buildProjectFile)
{
var dotnet = ResolveDotnet(rootDirectory);

Expand Down Expand Up @@ -63,7 +69,7 @@ private static int StartDotnet(string dotnet, IEnumerable<string> arguments)
startInfo.Environment["DOTNET_NOLOGO"] = "1";
startInfo.Environment["DOTNET_ROLL_FORWARD"] = "Major";
startInfo.Environment["FALLOUT_TELEMETRY_OPTOUT"] = "1";
startInfo.Environment[GlobalToolVersionEnvironmentKey] = typeof(Program).Assembly.GetVersionText();
startInfo.Environment[GlobalToolVersionEnvironmentKey] = typeof(RunCommand).Assembly.GetVersionText();
startInfo.Environment[GlobalToolStartTimeEnvironmentKey] = DateTime.Now.ToString("O");

var process = Process.Start(startInfo).NotNull();
Expand Down
1 change: 1 addition & 0 deletions src/Fallout.Cli/Fallout.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Spectre.Console" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" />
Expand Down
2 changes: 1 addition & 1 deletion src/Fallout.Cli/Program.AddPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static int AddPackage(string[] args, AbsolutePath rootDirectory, Absolute
return 0;
}

private static void AddOrReplacePackage(string packageId, string packageVersion, string packageType, string buildProjectFile)
internal static void AddOrReplacePackage(string packageId, string packageVersion, string packageType, string buildProjectFile)
{
var buildProject = ProjectModelTasks.ParseProject(buildProjectFile).NotNull();

Expand Down
6 changes: 4 additions & 2 deletions src/Fallout.Cli/Program.GetConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ namespace Fallout.Cli;

partial class Program
{
private const string BUILD_PROJECT_FILE = nameof(BUILD_PROJECT_FILE);
// internal (not private): shared with AddPackage/Update/Cake; will move into a configuration
// service in the final #392 collapse PR.
internal const string BUILD_PROJECT_FILE = nameof(BUILD_PROJECT_FILE);
private const string TEMP_DIRECTORY = nameof(TEMP_DIRECTORY);
private const string DOTNET_GLOBAL_FILE = nameof(DOTNET_GLOBAL_FILE);
private const string DOTNET_INSTALL_URL = nameof(DOTNET_INSTALL_URL);
Expand All @@ -27,7 +29,7 @@ public static int GetConfiguration(string[] args, AbsolutePath rootDirectory, Ab
return 0;
}

private static Dictionary<string, string> GetConfiguration(AbsolutePath buildScript, bool evaluate)
internal static Dictionary<string, string> GetConfiguration(AbsolutePath buildScript, bool evaluate)
{
string ReplaceScriptDirectory(string value)
=> evaluate
Expand Down
6 changes: 3 additions & 3 deletions src/Fallout.Cli/Program.Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,13 @@ internal static void UpdateSolutionFileContent(
"EndProject");
}

private static string[] GetTemplate(string templateName)
internal static string[] GetTemplate(string templateName)
{
return ResourceUtility.GetResourceAllLines<Program>($"templates.{templateName}");
}


private static void WriteBuildScripts(
internal static void WriteBuildScripts(
AbsolutePath scriptDirectory,
AbsolutePath rootDirectory,
AbsolutePath buildDirectory,
Expand Down Expand Up @@ -243,7 +243,7 @@ void MakeExecutable(AbsolutePath scriptFile)
}
}

private static void WriteConfigurationFile(AbsolutePath rootDirectory, AbsolutePath solutionFile)
internal static void WriteConfigurationFile(AbsolutePath rootDirectory, AbsolutePath solutionFile)
{
var parametersFile = GetDefaultParametersFile(rootDirectory);
var dictionary = new Dictionary<string, string> { ["$schema"] = BuildSchemaFileName };
Expand Down
Loading
Loading