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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
external
nuke-global.*
fallout-global.*

# Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
[Bb]in/
Expand Down
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"
}
]
}
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- Thanks to Thomas Frenzel (https://github.com/dotnet/sdk/issues/1151#issuecomment-385133284) -->
<PropertyGroup>
<ReplacePackageReferences Condition="$(ReplacePackageReferences) == ''">true</ReplacePackageReferences>
<SolutionPath>$(MSBuildThisFileDirectory)\nuke-global.sln</SolutionPath>
<SolutionPath>$(MSBuildThisFileDirectory)fallout-global.sln</SolutionPath>
</PropertyGroup>

<Choose>
Expand Down
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Multi-provider CI support (Azure Pipelines, GitLab, TeamCity, AppVeyor) was remo

### Commits, issues, PRs (rolling 30 days)

![Repobeats analytics image](https://repobeats.axiom.co/api/embed/13bc62ac87b75cea7be4bdbbcba90af620665333.svg "Repobeats analytics image")
![Repobeats analytics image](https://repobeats.axiom.co/api/embed/c4ea2e2211409a86c7dba874c3ed6aa629efe700.svg "Repobeats analytics image")

Generated by [Repobeats](https://repobeats.axiom.co).

Expand Down
2 changes: 1 addition & 1 deletion build/Build.GlobalSolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ partial class Build
{
[Parameter] readonly bool UseHttps;

AbsolutePath GlobalSolution => RootDirectory / "nuke-global.sln";
AbsolutePath GlobalSolution => RootDirectory / "fallout-global.sln";
AbsolutePath ExternalRepositoriesDirectory => RootDirectory / "external";
AbsolutePath ExternalRepositoriesFile => ExternalRepositoriesDirectory / "repositories.yml";

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Shaped by [milestone #18](https://github.com/Fallout-build/Fallout/milestone/18)
- Don't add `secret`/`default` defaults to tool JSON files (see CONTRIBUTING.md).
- Don't introduce a new test framework or assertion library — stay on xUnit + FluentAssertions + Verify.
- Don't commit `output/` or any `bin/`/`obj/` directory.
- Don't commit `nuke-global.sln` or other `nuke-global.*` files — they're generated by `GenerateGlobalSolution`.
- Don't commit `fallout-global.sln` or other `fallout-global.*` files — they're generated by `GenerateGlobalSolution`.
- Don't bypass `Directory.Packages.props` or `Directory.Build.targets`.
- Don't reintroduce `.editorconfig` or `*.DotSettings` without a maintainer-level decision — they were intentionally removed.

Expand Down
6 changes: 2 additions & 4 deletions src/Fallout.Build/Utilities/ConsoleUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ public static string PromptForInput(string question, string defaultValue)
}

key = Console.ReadKey(intercept: true);
if (ConsoleKey.A <= key.Key && key.Key <= ConsoleKey.Z
|| ConsoleKey.D0 <= key.Key && key.Key <= ConsoleKey.D9
|| new[] { '.', '/', '\\', '_', '-' }.Any(x => x == key.KeyChar))
if (key.IsValidInputKey())
input.Append(key.KeyChar);
else if (key.Key == ConsoleKey.Backspace && input.Length > 0)
input.Remove(input.Length - 1, length: 1);
else if (key.Key == InterruptKey)
s_interrupted = true;
} while (!(key.Key == ConfirmationKey || key.Key == InterruptKey));
} while (key.Key is not (ConfirmationKey or InterruptKey));

var result = input.Length > 0 ? input.ToString() : defaultValue;
Console.CursorLeft = 0;
Expand Down
17 changes: 17 additions & 0 deletions src/Fallout.Build/Utilities/ConsoleUtilityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Linq;

namespace Fallout.Common.Utilities;

public static class ConsoleUtilityExtensions
{
private static readonly char[] AllowedSpecialCharacters = ['.', '/', '\\', '_', '-'];

public static bool IsValidInputKey(this ConsoleKeyInfo key)
{
return key.Key is >= ConsoleKey.A and <= ConsoleKey.Z
|| key.Key is >= ConsoleKey.D0 and <= ConsoleKey.D9
|| AllowedSpecialCharacters.Any(x => x == key.KeyChar)
|| char.IsLetterOrDigit(key.KeyChar);
}
}
72 changes: 72 additions & 0 deletions src/Fallout.Cli/CommandDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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)
{
this.commands = commands.ToList();
this.prompts = prompts;
}

public async Task<int> DispatchAsync(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 await command.ExecuteAsync(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?")
? await GetRequired("setup").ExecuteAsync(Array.Empty<string>(), rootDirectory: null, buildScript: null)
: 0;
}

// TODO: docker

return await GetRequired("run").ExecuteAsync(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);
}
30 changes: 30 additions & 0 deletions src/Fallout.Cli/Commands/DelegateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
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;
this.handler = handler;
}

public string Name { get; }

// The adapted legacy handlers are still synchronous; each #392 conversion replaces one
// registration of this adapter with a command type that overrides ExecuteAsync for real.
public Task<int> ExecuteAsync(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript)
=> Task.FromResult(handler(args, rootDirectory, buildScript));
}
31 changes: 31 additions & 0 deletions src/Fallout.Cli/Commands/IFalloutCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Threading.Tasks;
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>
internal interface IFalloutCommand
{
/// <summary>
/// The command name as typed after the <c>:</c> prefix (prefer dash form, e.g. <c>"add-package"</c>).\
/// Matched case-insensitively and with dashes ignored (so <c>:addpackage</c> also matches).\
/// Legacy commands may still use PascalCase names (e.g. <c>"GetNextDirectory"</c>).
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>
Task<int> ExecuteAsync(string[] args, AbsolutePath rootDirectory, AbsolutePath buildScript);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,42 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Fallout.Common;
using Fallout.Common.IO;
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 async Task<int> ExecuteAsync(string[] forwardedArgs, AbsolutePath rootDirectory, AbsolutePath buildProjectFile)
{
var dotnet = ResolveDotnet(rootDirectory);

var buildExitCode = StartDotnet(dotnet, GetBuildArguments(buildProjectFile));
var buildExitCode = await StartDotnetAsync(dotnet, GetBuildArguments(buildProjectFile));
if (buildExitCode != 0)
{
return buildExitCode;
}

return StartDotnet(dotnet, GetRunArguments(buildProjectFile, forwardedArgs));
return await StartDotnetAsync(dotnet, GetRunArguments(buildProjectFile, forwardedArgs));
}

private static string ResolveDotnet(AbsolutePath rootDirectory)
{
var pathDotnet = TryGetDotnetFromPath();
if (pathDotnet != null)
{
return pathDotnet;
}

var shimDirectoryName = EnvironmentInfo.IsWin ? "dotnet-win" : "dotnet-unix";
var shimExecutableName = EnvironmentInfo.IsWin ? "dotnet.exe" : "dotnet";
Expand All @@ -48,7 +59,7 @@ private static string TryGetDotnetFromPath()
.FirstOrDefault(File.Exists);
}

private static int StartDotnet(string dotnet, IEnumerable<string> arguments)
private static async Task<int> StartDotnetAsync(string dotnet, IEnumerable<string> arguments)
{
var startInfo = new ProcessStartInfo
{
Expand All @@ -57,17 +68,19 @@ private static int StartDotnet(string dotnet, IEnumerable<string> arguments)
};

foreach (var argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}

startInfo.Environment["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
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();
process.WaitForExit();
await process.WaitForExitAsync();
return process.ExitCode;
}

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
Loading
Loading