Compare commits
2 Commits
aa1c2422ef
...
5ccb40bb44
Author | SHA1 | Date | |
---|---|---|---|
5ccb40bb44 | |||
d210c6ac7c |
@ -0,0 +1,17 @@
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresFeatureAttribute : Attribute
|
||||
{
|
||||
public string FeatureName { get; }
|
||||
|
||||
public RequiresFeatureAttribute(string featureName)
|
||||
{
|
||||
FeatureName = featureName;
|
||||
}
|
||||
|
||||
public void RequireFeature(string command)
|
||||
{
|
||||
if (!Directory.Exists(Path.Join(Environment.CurrentDirectory, FeatureName)))
|
||||
throw new($"Command '{command}' requires feature {FeatureName}");
|
||||
}
|
||||
}
|
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresFileAttribute : Attribute
|
||||
{
|
||||
public string FilePath { get; }
|
||||
|
||||
public RequiresFileAttribute(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
public void RequireFile(string command)
|
||||
{
|
||||
if (!File.Exists(Path.Join(Environment.CurrentDirectory, FilePath)))
|
||||
throw new($"Command '{command}' requires file {FilePath}");
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresPluginAttribute : Attribute
|
||||
{
|
||||
public void RequirePluginProject(string command)
|
||||
{
|
||||
var matcher = new Matcher()
|
||||
.AddInclude("*.csproj")
|
||||
.Execute(new DirectoryInfoWrapper(new DirectoryInfo(Environment.CurrentDirectory)));
|
||||
|
||||
if (!matcher.HasMatches)
|
||||
throw new($"Command '{command}' must be run in a command plugin project.");
|
||||
|
||||
var csprojFileName = $"{Path.GetDirectoryName(Environment.CurrentDirectory)}.csproj";
|
||||
bool IsCsprojFile(FilePatternMatch file) => Path.GetFileNameWithoutExtension(file.Path).Equals(csprojFileName);
|
||||
var hasCsprojFile = matcher.Files.Any(IsCsprojFile);
|
||||
|
||||
if (!hasCsprojFile)
|
||||
throw new($"File '{csprojFileName}' was not found, make sure you're in a command plugin project.");
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ using System.CommandLine;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
@ -1,13 +1,15 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFeature(Features.Api.FeatureName)]
|
||||
public partial class Api : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
|
||||
base("api", "API related commands")
|
||||
{
|
||||
foreach (var command in commands)
|
||||
|
@ -2,7 +2,6 @@ using System.CommandLine;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFeature(Features.Db.FeatureName)]
|
||||
public partial class Db : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Db(IEnumerable<ISubCommandOf<Db>> commands)
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using Humanizer;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -6,11 +7,12 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFile("requirements.txt")]
|
||||
public class Hydrate : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Hydrate(ProjectContext context)
|
||||
|
||||
public Hydrate(ProjectContext context)
|
||||
: base("hydrate", "Initialize venv and install dependencies from requirements.txt")
|
||||
{
|
||||
_context = context;
|
||||
|
@ -28,7 +28,7 @@ public partial class MycroForge
|
||||
|
||||
if (packs.Length == 0)
|
||||
{
|
||||
Console.WriteLine("m4g install requires at least one package.");
|
||||
Console.WriteLine("'m4g install' requires at least one package.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Humanizer;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -11,6 +12,7 @@ public partial class MycroForge
|
||||
{
|
||||
public partial class Plugin
|
||||
{
|
||||
[RequiresPlugin]
|
||||
public class Install : Command, ISubCommandOf<Plugin>
|
||||
{
|
||||
public enum TargetPlatform
|
||||
|
@ -27,9 +27,17 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(IEnumerable<string> packages, bool yes)
|
||||
{
|
||||
var packs = packages.ToArray();
|
||||
|
||||
if (packs.Length == 0)
|
||||
{
|
||||
Console.WriteLine("'m4g uninstall' requires at least one package.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packages)}",
|
||||
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packs)}",
|
||||
"pip freeze > requirements.txt"
|
||||
);
|
||||
}
|
||||
|
90
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
90
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Reflection;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
namespace MycroForge.CLI.Extensions;
|
||||
|
||||
public static class CommandExtensions
|
||||
{
|
||||
public static async Task ExecuteAsync(this Commands.MycroForge rootCommand, string[] args)
|
||||
{
|
||||
var parser = new CommandLineBuilder(rootCommand)
|
||||
.AddMiddleware()
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler((ex, ctx) =>
|
||||
{
|
||||
/*
|
||||
* Use a custom ExceptionHandler to prevent the System.CommandLine library from printing the StackTrace by default.
|
||||
*/
|
||||
Console.WriteLine(ex.Message);
|
||||
|
||||
/*
|
||||
* Set the exit code to a non-zero value to indicate to the shell that the process has failed.
|
||||
*/
|
||||
Environment.ExitCode = -1;
|
||||
})
|
||||
.Build();
|
||||
|
||||
await parser.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder)
|
||||
{
|
||||
builder.AddMiddleware(async (context, next) =>
|
||||
{
|
||||
var commandChain = context.GetCommandChain();
|
||||
var commandText = string.Join(' ', commandChain.Select(cmd => cmd.Name));
|
||||
|
||||
foreach (var _command in commandChain)
|
||||
{
|
||||
if (_command.GetRequiresFeatureAttribute() is RequiresFeatureAttribute requiresFeatureAttribute)
|
||||
requiresFeatureAttribute.RequireFeature(commandText);
|
||||
|
||||
else if (_command.GetRequiresFileAttribute() is RequiresFileAttribute requiresFileAttribute)
|
||||
requiresFileAttribute.RequireFile(commandText);
|
||||
|
||||
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
||||
requiresPluginAttribute.RequirePluginProject(commandText);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static RequiresFeatureAttribute? GetRequiresFeatureAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresFeatureAttribute>();
|
||||
|
||||
private static RequiresFileAttribute? GetRequiresFileAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresFileAttribute>();
|
||||
|
||||
private static RequiresPluginAttribute? GetRequiresPluginAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresPluginAttribute>();
|
||||
|
||||
private static List<Command> GetCommandChain(this InvocationContext context)
|
||||
{
|
||||
var chain = new List<Command>();
|
||||
/*
|
||||
* The CommandResult property refers to the last command in the chain.
|
||||
* So if the command is 'm4g api run' the CommandResult will refer to 'run'.
|
||||
*/
|
||||
SymbolResult? cmd = context.ParseResult.CommandResult;
|
||||
|
||||
while (cmd is CommandResult result)
|
||||
{
|
||||
chain.Add(result.Command);
|
||||
cmd = cmd.Parent;
|
||||
}
|
||||
|
||||
/*
|
||||
* Reverse the chain to reflect the actual order of the commands.
|
||||
*/
|
||||
chain.Reverse();
|
||||
|
||||
return chain;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace MycroForge.CLI.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection RegisterCommandDefaults(this IServiceCollection services)
|
||||
public static IServiceCollection RegisterDefaultCommands(this IServiceCollection services)
|
||||
{
|
||||
// Register ProjectContext, OptionsContainer & features
|
||||
services.AddScoped<ProjectContext>();
|
||||
|
@ -1,7 +1,6 @@
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Extensions;
|
||||
|
||||
namespace MycroForge.CLI.Features;
|
||||
|
||||
@ -58,8 +57,7 @@ public sealed partial class Db : IFeature
|
||||
" - '%app_name%_mariadb:/var/lib/mysql'",
|
||||
"",
|
||||
" %app_name%_phpmyadmin:",
|
||||
" image: phpmyadmin/phpmyadmin",
|
||||
" platform: %dbu_platform%",
|
||||
" image: %dbu_platform%/phpmyadmin",
|
||||
" container_name: %app_name%_phpmyadmin",
|
||||
" ports:",
|
||||
" - '${DBU_PORT}:80'",
|
||||
@ -127,7 +125,7 @@ public sealed partial class Db : IFeature
|
||||
|
||||
var dockerCompose = string.Join('\n', DockerCompose)
|
||||
.Replace("%app_name%", appName)
|
||||
.Replace("%dbu_platform%", options.DbuPlatform.ToDockerPlatformString())
|
||||
.Replace("%dbu_platform%", options.DbuPlatform.ToString())
|
||||
;
|
||||
|
||||
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
|
||||
|
@ -1,25 +1,18 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using RootCommand = MycroForge.CLI.Commands.MycroForge;
|
||||
|
||||
using var host = Host
|
||||
.CreateDefaultBuilder()
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
services
|
||||
.RegisterCommandDefaults()
|
||||
.RegisterDefaultCommands()
|
||||
.RegisterCommandPlugins()
|
||||
;
|
||||
})
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>()
|
||||
.InvokeAsync(args.Length == 0 ? ["--help"] : args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
var command = host.Services.GetRequiredService<RootCommand>();
|
||||
await command.ExecuteAsync(args.Length == 0 ? ["--help"] : args);
|
||||
|
@ -1,19 +0,0 @@
|
||||
using System.Reflection;
|
||||
using MycroForge.Core.Attributes;
|
||||
|
||||
namespace MycroForge.Core.Extensions;
|
||||
|
||||
public static class EnumExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic extension method that aids in reflecting
|
||||
/// and retrieving any attribute that is applied to an `Enum`.
|
||||
/// </summary>
|
||||
public static string ToDockerPlatformString(this ProjectConfig.DbConfig.DbuPlatformOptions value)
|
||||
{
|
||||
return value.GetType()
|
||||
.GetMember(value.ToString())
|
||||
.FirstOrDefault()!
|
||||
.GetCustomAttribute<DockerPlatformAttribute>()!.Platform;
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
using MycroForge.Core.Attributes;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public partial class ProjectConfig
|
||||
{
|
||||
@ -8,20 +6,11 @@ public partial class ProjectConfig
|
||||
{
|
||||
public enum DbuPlatformOptions
|
||||
{
|
||||
[DockerPlatform(Platform = "linux/amd64")]
|
||||
linux_amd64,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v5")]
|
||||
linux_arm32v5,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v6")]
|
||||
linux_arm32v6,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm32/v7")]
|
||||
linux_arm32v7,
|
||||
|
||||
[DockerPlatform(Platform = "linux/arm64/v8")]
|
||||
linux_arm64v8
|
||||
amd64,
|
||||
arm32v5,
|
||||
arm32v6,
|
||||
arm32v7,
|
||||
arm64v8
|
||||
}
|
||||
}
|
||||
}
|
@ -59,6 +59,7 @@ public class ProjectContext
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
await Bash($"chmod 777 {fullPath}");
|
||||
Console.WriteLine($"Created file {path}");
|
||||
}
|
||||
|
||||
public async Task<string> ReadFile(string path)
|
||||
@ -78,6 +79,7 @@ public class ProjectContext
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
Console.WriteLine($"Modified file {path}");
|
||||
}
|
||||
|
||||
public async Task Bash(params string[] script)
|
||||
@ -114,16 +116,15 @@ public class ProjectContext
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await using var input = process.StandardInput;
|
||||
foreach (var line in script)
|
||||
await input.WriteLineAsync(line);
|
||||
// Concat with '&&' operator to make sure that script does not continue on failure.
|
||||
await input.WriteAsync(string.Join(" && ", script));
|
||||
|
||||
await input.FlushAsync();
|
||||
input.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
Console.WriteLine($"Process finished with exit code {process.ExitCode}.");
|
||||
Environment.ExitCode = process.ExitCode;
|
||||
}
|
||||
|
||||
public async Task SaveConfig(ProjectConfig config)
|
||||
|
4
MycroForge.Core/scripts/publish-nuget.sh
Normal file
4
MycroForge.Core/scripts/publish-nuget.sh
Normal file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
dotnet build -r Release
|
||||
dotnet nuget push --source devdisciples bin/Release/MycroForge.Core.1.0.0.nupkg
|
@ -31,8 +31,7 @@ dotnet nuget add source --name devdisciples --username username --password passw
|
||||
### TODO
|
||||
|
||||
- Fix `-c` option for `m4g db generate entity`
|
||||
- Mention `--dbu-platform` option for `m4g init`
|
||||
- Research if System.CommandLine middleware can be used to safeguard commands like `m4g add` or `m4g api`.
|
||||
- Fix up exception handling
|
||||
- Add a CLI UI library
|
||||
- Clean up README files
|
||||
- Theme the site with a custom color scheme and icon/logos
|
||||
-
|
@ -15,10 +15,10 @@ Arguments:
|
||||
<name> The name of your project
|
||||
|
||||
Options:
|
||||
--without <api|db|git|gitignore> Features to exclude
|
||||
--api-port <api-port> The API port
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
--without <api|db|git|gitignore> Features to exclude
|
||||
--api-port <api-port> The API port
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <amd64|arm32v5|arm32v6|arm32v7|arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
|
@ -36,13 +36,22 @@ This command starts the services defined in the `db.docker-compose.yml` file.
|
||||
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
|
||||
command should output the service names defined in `db.docker-compose.yml`.
|
||||
|
||||
If you go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051), you should now be able to login with the
|
||||
Go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051). You should now be able to login with the
|
||||
following credentials.
|
||||
- user: root
|
||||
- pass: password
|
||||
|
||||
When you're done developing, you can shut down the local database by running `m4g db stop`
|
||||
|
||||
:::info
|
||||
|
||||
If you're running on MacOS, Docker might complain about a platform mismatch for PhpMyAdmin.
|
||||
In that case you might need to specify the platform for the PhpMyAdmin image.
|
||||
You can do this by passing the `--dbu-platform` flag to `m4g init`.
|
||||
Run `m4g init -?` for all the available options.
|
||||
If you've already initialized a project, you can also change the platform prefix of the PhpMyAdmin image in the `db.docker-compose.yml`.
|
||||
|
||||
:::
|
||||
|
||||
### Create the entities
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user