diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..fe1152bd
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/MintPlayer.Dotnet.Tools.sln b/MintPlayer.Dotnet.Tools.sln
index 9e0dd67a..2a23b933 100644
--- a/MintPlayer.Dotnet.Tools.sln
+++ b/MintPlayer.Dotnet.Tools.sln
@@ -176,6 +176,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MintPlayer.AdminHelper", "A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdminTest", "AdminHelper\AdminTest\AdminTest.csproj", "{CC34556F-529A-4186-AD6C-5F986808D8A5}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandLineApp", "CommandLineApp", "{752F83A4-B5A2-431F-B7A9-871FD0BD1617}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MintPlayer.CommandLineApp", "SourceGenerators\CommandLineApp\MintPlayer.CommandLineApp\MintPlayer.CommandLineApp.csproj", "{83309DAC-700C-4A03-B7CD-EC1C4F77CC85}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MintPlayer.CommandLineApp.Attributes", "SourceGenerators\CommandLineApp\MintPlayer.CommandLineApp.Attributes\MintPlayer.CommandLineApp.Attributes.csproj", "{53451C47-4933-47BE-8B44-ED911D9163AB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineAppDebugging", "SourceGenerators\TestProjects\CommandLineAppDebugging\CommandLineAppDebugging.csproj", "{4CA948F7-9118-4442-885F-CE8CFFE5E0A2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -390,6 +398,18 @@ Global
{9BCC691D-02F6-4F52-BB14-BB99825959B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BCC691D-02F6-4F52-BB14-BB99825959B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BCC691D-02F6-4F52-BB14-BB99825959B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83309DAC-700C-4A03-B7CD-EC1C4F77CC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83309DAC-700C-4A03-B7CD-EC1C4F77CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83309DAC-700C-4A03-B7CD-EC1C4F77CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83309DAC-700C-4A03-B7CD-EC1C4F77CC85}.Release|Any CPU.Build.0 = Release|Any CPU
+ {53451C47-4933-47BE-8B44-ED911D9163AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {53451C47-4933-47BE-8B44-ED911D9163AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {53451C47-4933-47BE-8B44-ED911D9163AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {53451C47-4933-47BE-8B44-ED911D9163AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4CA948F7-9118-4442-885F-CE8CFFE5E0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4CA948F7-9118-4442-885F-CE8CFFE5E0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4CA948F7-9118-4442-885F-CE8CFFE5E0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4CA948F7-9118-4442-885F-CE8CFFE5E0A2}.Release|Any CPU.Build.0 = Release|Any CPU
{447355EB-ED1F-4701-955F-598D00E1E207}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{447355EB-ED1F-4701-955F-598D00E1E207}.Debug|Any CPU.Build.0 = Debug|Any CPU
{447355EB-ED1F-4701-955F-598D00E1E207}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -466,6 +486,10 @@ Global
{9BCC691D-02F6-4F52-BB14-BB99825959B9} = {BE967E23-E7A7-4A55-912D-38FD21068E1D}
{447355EB-ED1F-4701-955F-598D00E1E207} = {41AC5E0E-BCB6-4F0F-A991-C5D19C66828C}
{CC34556F-529A-4186-AD6C-5F986808D8A5} = {41AC5E0E-BCB6-4F0F-A991-C5D19C66828C}
+ {752F83A4-B5A2-431F-B7A9-871FD0BD1617} = {C65054D9-CAD1-4124-B474-D03C178D9D78}
+ {83309DAC-700C-4A03-B7CD-EC1C4F77CC85} = {752F83A4-B5A2-431F-B7A9-871FD0BD1617}
+ {53451C47-4933-47BE-8B44-ED911D9163AB} = {752F83A4-B5A2-431F-B7A9-871FD0BD1617}
+ {4CA948F7-9118-4442-885F-CE8CFFE5E0A2} = {1614A8B2-CA9A-4F94-B68A-BCC8ED244648}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D5378D43-9246-4355-8C4B-880DB15B07DA}
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/ConsoleAppAttribute.cs b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/ConsoleAppAttribute.cs
new file mode 100644
index 00000000..6381cd7b
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/ConsoleAppAttribute.cs
@@ -0,0 +1,9 @@
+namespace MintPlayer.CommandLineApp.Attributes;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public class ConsoleAppAttribute : Attribute
+{
+ public ConsoleAppAttribute(string description)
+ {
+ }
+}
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/MintPlayer.CommandLineApp.Attributes.csproj b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/MintPlayer.CommandLineApp.Attributes.csproj
new file mode 100644
index 00000000..d9f9f7e9
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp.Attributes/MintPlayer.CommandLineApp.Attributes.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.0
+ 13
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Shipped.md b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000..60b59dd9
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Unshipped.md b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000..dfb7955a
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,8 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+COMLAPP001 | ConsoleAppGenerator | Error | DiagnosticRules
\ No newline at end of file
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Producer.cs b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Producer.cs
new file mode 100644
index 00000000..0ca65dcc
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Producer.cs
@@ -0,0 +1,78 @@
+using Microsoft.CodeAnalysis;
+using MintPlayer.CommandLineApp.Models;
+using MintPlayer.SourceGenerators.Tools;
+using MintPlayer.SourceGenerators.Tools.Extensions;
+using System.CodeDom.Compiler;
+
+namespace MintPlayer.CommandLineApp.Generators;
+
+public class LaunchSettingsSummaryProducer : Producer, IDiagnosticReporter
+{
+ private readonly IEnumerable consoleApps;
+ private readonly IEnumerable filesWithTopLevelStatements;
+ public LaunchSettingsSummaryProducer(IEnumerable consoleApps, IEnumerable filesWithTopLevelStatements, string rootNamespace) : base(rootNamespace, "LaunchSettingsSummary.g.cs")
+ {
+ this.consoleApps = consoleApps;
+ this.filesWithTopLevelStatements = filesWithTopLevelStatements;
+ }
+
+ public IEnumerable GetDiagnostics()
+ {
+ if (consoleApps.Count() > 1)
+ return consoleApps.Select(ca => DiagnosticRules.OnlyOneConsoleAppAllowed.Create(ca.ClassSymbolLocation));
+ else
+ return Enumerable.Empty();
+
+ //return filesWithTopLevelStatements.Select(f => DiagnosticRules.CannotHaveTopLevelStatements.Create(f));
+ }
+
+ protected override void ProduceSource(IndentedTextWriter writer, CancellationToken cancellationToken)
+ {
+ foreach (var consoleApp in consoleApps)
+ {
+ if (!string.IsNullOrEmpty(consoleApp.Namespace))
+ {
+ writer.WriteLine($"namespace {consoleApp.Namespace}");
+ writer.WriteLine("{");
+ writer.Indent++;
+ }
+
+ writer.WriteLine($"public static class {consoleApp.ClassName}App");
+ writer.WriteLine("{");
+ writer.Indent++;
+
+ writer.WriteLine("public static async global::System.Threading.Tasks.Task Run(string[] args)");
+ writer.WriteLine("{");
+ writer.Indent++;
+
+ ProduceMainMethod(writer, consoleApp);
+
+ writer.Indent--;
+ writer.WriteLine("}");
+
+ writer.Indent--;
+ writer.WriteLine("}");
+
+
+ if (!string.IsNullOrEmpty(consoleApp.Namespace))
+ {
+ writer.Indent--;
+ writer.WriteLine("}");
+ }
+ }
+ }
+
+ private void ProduceMainMethod(IndentedTextWriter writer, ConsoleApp consoleApp)
+ {
+ var description = consoleApp.Description ?? string.Empty;
+ writer.WriteLine($""""
+ var rootCommand = new System.CommandLine.RootCommand("{description.Replace("\"", "\\\"")}");
+ """");
+ writer.WriteLine($""""
+ var parsed = rootCommand.Parse(args);
+ """");
+ writer.WriteLine($""""
+ await parsed.InvokeAsync();
+ """");
+ }
+}
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Rules.cs b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Rules.cs
new file mode 100644
index 00000000..d7fcf42b
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.Rules.cs
@@ -0,0 +1,22 @@
+using Microsoft.CodeAnalysis;
+
+namespace MintPlayer.CommandLineApp.Generators;
+
+public static partial class DiagnosticRules
+{
+ public static readonly DiagnosticDescriptor CannotHaveTopLevelStatements = new(
+ id: "COMLAPP001",
+ title: "A console app that uses the [ConsoleApp] attribute cannot have top-level statements",
+ messageFormat: "A console app that uses the [ConsoleApp] attribute cannot have top-level statements",
+ category: "ConsoleAppGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor OnlyOneConsoleAppAllowed = new(
+ id: "COMLAPP002",
+ title: "Only one console app with the [ConsoleApp] attribute is allowed",
+ messageFormat: "Only one console app with the [ConsoleApp] attribute is allowed",
+ category: "ConsoleAppGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+}
\ No newline at end of file
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.cs b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.cs
new file mode 100644
index 00000000..5a5261ad
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Generators/LaunchSettingsSummaryGenerator.cs
@@ -0,0 +1,65 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using MintPlayer.SourceGenerators.Tools;
+using MintPlayer.SourceGenerators.Tools.ValueComparers;
+
+namespace MintPlayer.CommandLineApp.Generators
+{
+ [Generator(LanguageNames.CSharp)]
+ public sealed class LaunchSettingsSummaryGenerator : IncrementalGenerator
+ {
+ const string consoleAppAttribute = "MintPlayer.CommandLineApp.Attributes.ConsoleAppAttribute";
+
+ public override void Initialize(IncrementalGeneratorInitializationContext context, IncrementalValueProvider settingsProvider)
+ {
+ var filesWithTopLevelStatements = context.CompilationProvider
+ .SelectMany(static (compilation, ct) => compilation.SyntaxTrees
+ .Where(st => st.GetRoot(ct).DescendantNodesAndSelf().OfType().Any())
+ .Select(f => f.GetLocation(TextSpan.FromBounds(0, f.Length - 1))))
+ .WithComparer(ValueComparer.Instance)
+ .Collect();
+
+ var consoleAppsProvider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ consoleAppAttribute,
+ static (node, ct) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
+ static (context, ct) =>
+ {
+ if (context.TargetNode is ClassDeclarationSyntax classDeclaration &&
+ context.SemanticModel.GetDeclaredSymbol(classDeclaration, ct) is INamedTypeSymbol classSymbol)
+ {
+ return new Models.ConsoleApp
+ {
+ Description = context.Attributes.FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == consoleAppAttribute)
+ ?.ConstructorArguments.FirstOrDefault().Value?.ToString() ?? string.Empty,
+ ClassName = classSymbol.Name,
+ Namespace = classSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : classSymbol.ContainingNamespace?.ToDisplayString(),
+ ClassSymbolLocation = classSymbol.Locations.FirstOrDefault(),
+ };
+ }
+
+ return default;
+ })
+ .WithNullableComparer()
+ .Collect();
+
+ var consoleAppsSourceProvider = consoleAppsProvider
+ .Join(filesWithTopLevelStatements)
+ .Join(settingsProvider)
+ .Select(static Producer (prov, ct) => new LaunchSettingsSummaryProducer(prov.Item1, prov.Item2, prov.Item3.RootNamespace!));
+
+ var consoleAppsDiagnosticProvider = consoleAppsProvider
+ .Join(filesWithTopLevelStatements)
+ .Join(settingsProvider)
+ .Select(static IDiagnosticReporter (prov, ct) => new LaunchSettingsSummaryProducer(prov.Item1, prov.Item2, prov.Item3.RootNamespace!));
+
+ context.ProduceCode(consoleAppsSourceProvider);
+ context.ReportDiagnostics(consoleAppsDiagnosticProvider);
+ }
+
+ public override void RegisterComparers()
+ {
+ }
+ }
+}
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/MintPlayer.CommandLineApp.csproj b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/MintPlayer.CommandLineApp.csproj
new file mode 100644
index 00000000..33eea4be
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/MintPlayer.CommandLineApp.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(GetTargetPathDependsOn);GetDependencyTargetPaths
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Models/ConsoleApp.cs b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Models/ConsoleApp.cs
new file mode 100644
index 00000000..60270fb1
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Models/ConsoleApp.cs
@@ -0,0 +1,12 @@
+using MintPlayer.ValueComparerGenerator.Attributes;
+
+namespace MintPlayer.CommandLineApp.Models;
+
+[AutoValueComparer]
+public partial class ConsoleApp
+{
+ public Microsoft.CodeAnalysis.Location? ClassSymbolLocation { get; set; }
+ public string? Namespace { get; set; }
+ public string? ClassName { get; set; }
+ public string? Description { get; set; }
+}
diff --git a/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Properties/launchSettings.json b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Properties/launchSettings.json
new file mode 100644
index 00000000..271fde0e
--- /dev/null
+++ b/SourceGenerators/CommandLineApp/MintPlayer.CommandLineApp/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Debug CommandLineApp Generators on CommandLineAppDebugging": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "..\\..\\TestProjects\\CommandLineAppDebugging\\CommandLineAppDebugging.csproj"
+ }
+ }
+}
\ No newline at end of file
diff --git a/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/ComparerExtensions.cs b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/ComparerExtensions.cs
new file mode 100644
index 00000000..50f8cf16
--- /dev/null
+++ b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/ComparerExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.CodeAnalysis;
+using MintPlayer.SourceGenerators.Tools.ValueComparers;
+
+namespace MintPlayer.SourceGenerators.Tools;
+
+public static class ComparerExtensions
+{
+ ///
+ /// Only use this method on simple types, like bool
+ ///
+ public static IncrementalValueProvider WithDefaultComparer(this IncrementalValueProvider provider) where T : struct
+ {
+ return provider.WithComparer(DefaultValueComparer.Instance);
+ }
+}
diff --git a/SourceGenerators/TestProjects/CommandLineAppDebugging/CommandLineAppDebugging.csproj b/SourceGenerators/TestProjects/CommandLineAppDebugging/CommandLineAppDebugging.csproj
new file mode 100644
index 00000000..5be0e189
--- /dev/null
+++ b/SourceGenerators/TestProjects/CommandLineAppDebugging/CommandLineAppDebugging.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/SourceGenerators/TestProjects/CommandLineAppDebugging/Program.cs b/SourceGenerators/TestProjects/CommandLineAppDebugging/Program.cs
new file mode 100644
index 00000000..04e1ac8c
--- /dev/null
+++ b/SourceGenerators/TestProjects/CommandLineAppDebugging/Program.cs
@@ -0,0 +1,9 @@
+// See https://aka.ms/new-console-template for more information
+using MintPlayer.CommandLineApp.Attributes;
+
+await ListAspnetcoreAppsApp.Run(args);
+
+[ConsoleApp("Lists all ASP.NET Core apps under the current folder")]
+public class ListAspnetcoreApps
+{
+}
diff --git a/SourceGenerators/TestProjects/CommandLineAppDebugging/Properties/launchSettings.json b/SourceGenerators/TestProjects/CommandLineAppDebugging/Properties/launchSettings.json
new file mode 100644
index 00000000..71e133b2
--- /dev/null
+++ b/SourceGenerators/TestProjects/CommandLineAppDebugging/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "CommandLineAppDebugging": {
+ "commandName": "Project"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker"
+ }
+ }
+}
\ No newline at end of file