diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs index 720830f..d31f1ea 100644 --- a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs @@ -40,6 +40,8 @@ public partial class MycroForge private async Task ExecuteAsync(string name) { + _context.AssertDirectoryExists("api/routers"); + var moduleName = name.Underscore(); await _context.CreateFile($"api/routers/{moduleName}.py", Template); diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs new file mode 100644 index 0000000..6088149 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs @@ -0,0 +1,66 @@ +using System.CommandLine; +using Humanizer; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Generate + { + public class Service : Command, ISubCommandOf + { + private static readonly Argument NameArgument = + new(name: "name", description: "The name of the service"); + + private static readonly Option WithSession = + new(name: "--with-session", description: "Create a service that uses database sessions"); + + + private static readonly string[] DefaultTemplate = + [ + "class %class_name%:", + "", + "\tdef do_stuff(self, stuff: str) -> str:", + "\t\treturn f\"Hey, I'm doing stuff!\"" + ]; + + private static readonly string[] WithSessionTemplate = + [ + "from orm.engine.async_session import async_session", + "# from orm.entities.user import User", + "", + "class %class_name%:", + "", + "\tasync def do_stuff(self, stuff: str) -> str:", + "\t\tasync with async_session() as session():", + "\t\t\t# stmt = select(User).where(User.firstname == \"John\")", + "\t\t\t# results = await session.scalars(stmt).all()", + "\t\t\t# print(len(results))", + "\t\t\tpass", + "\t\treturn f\"Hey, I'm doing stuff!\"" + ]; + + private readonly ProjectContext _context; + + public Service(ProjectContext context) : base("service", "Generate a service") + { + _context = context; + AddAlias("s"); + AddOption(WithSession); + this.SetHandler(ExecuteAsync, NameArgument, WithSession); + } + + private async Task ExecuteAsync(string name, bool withSession) + { + _context.AssertDirectoryExists("services"); + + var className = Path.GetFileName(name).Pascalize(); + var code = string.Join('\n', withSession ? WithSessionTemplate : DefaultTemplate) + .Replace("%class_name%", className); + + await _context.CreateFile($"services/{name.Underscore().ToLower()}.py", code); + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Venv.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Venv.cs new file mode 100644 index 0000000..bbda063 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Generate.Venv.cs @@ -0,0 +1,23 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Generate + { + public class Venv : Command, ISubCommandOf + { + private readonly ProjectContext _context; + + public Venv(ProjectContext context) : base("venv", "Generate a venv") + { + _context = context; + this.SetHandler(ExecuteAsync); + } + + private async Task ExecuteAsync() => await _context.Bash("python3 -m venv .venv"); + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.cs b/MycroForge.CLI/Commands/MycroForge.Generate.cs new file mode 100644 index 0000000..9cf7e52 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Generate.cs @@ -0,0 +1,18 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Generate : Command, ISubCommandOf + { + public Generate(IEnumerable> commands) + : base("generate", "Generate common items") + { + AddAlias("g"); + foreach (var command in commands.Cast()) + AddCommand(command); + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Init.cs b/MycroForge.CLI/Commands/MycroForge.Init.cs index 6a80e33..9e0cdff 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.cs @@ -6,6 +6,53 @@ namespace MycroForge.CLI.Commands; public partial class MycroForge { + #region GitIgnore + + private static readonly string[] GitIgnore = + [ + "# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions", + "*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/", + ".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/", + ".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller", + "# Usually these files are written by a python script from a template", + "# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec", + "# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports", + "htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml", + "*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot", + "# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:", + "instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/", + "# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython", + "profile_default/", "ipython_config.py", "# pyenv", + "# For a library or package, you might want to ignore these files since the code is", + "# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv", + "# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.", + "# However, in case of collaboration, if having platform-specific dependencies or dependencies", + "# having no cross-platform support, pipenv may install dependencies that don't work, or not", + "# install all needed dependencies.", "#Pipfile.lock", "# poetry", + "# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.", + "# This is especially recommended for binary packages to ensure reproducibility, and is more", + "# commonly ignored for libraries.", + "# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control", + "#poetry.lock", "# pdm", + "# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.", + "#pdm.lock", + "# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it", + "# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml", + "# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/", + "# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py", + "# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/", + "# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject", + "# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json", + "# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols", + "cython_debug/", "# PyCharm", + "# JetBrains specific template is maintained in a separate JetBrains.gitignore that can", + "# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore", + "# and can be added to the global gitignore or merged into this file. For a more nuclear", + "# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/" + ]; + + #endregion + public class Init : Command, ISubCommandOf { private static readonly string[] DefaultFeatures = @@ -20,7 +67,9 @@ public partial class MycroForge private static readonly Option> WithoutOption = new Option>(name: "--without", description: "Features to exclude") - .FromAmong(DefaultFeatures); + { + AllowMultipleArgumentsPerToken = true + }.FromAmong(DefaultFeatures); private readonly ProjectContext _context; private readonly List _features; @@ -54,6 +103,9 @@ public partial class MycroForge // Create the entrypoint file await _context.CreateFile("main.py"); + // Create the default .gitignore folder + await _context.CreateFile(".gitignore", GitIgnore); + // Create the venv await _context.Bash($"python3 -m venv {Path.Combine(projectRoot, ".venv")}"); diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs index a9ea7a4..00b06db 100644 --- a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs @@ -43,6 +43,8 @@ public partial class MycroForge private async Task ExecuteAsync(string name) { + _context.AssertDirectoryExists("orm"); + var className = name.Underscore().Pascalize(); var moduleName = name.Underscore(); var code = string.Join('\n', Template); diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs index b08dc95..3bfe585 100644 --- a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs @@ -26,6 +26,8 @@ public partial class MycroForge private async Task ExecuteAsync(string name) { + _context.AssertDirectoryExists("orm/versions"); + await _context.Bash( "source .venv/bin/activate", $"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")" diff --git a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs index 2361641..591ad72 100644 --- a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs +++ b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs @@ -24,6 +24,11 @@ public static class ServiceCollectionExtensions services.AddScoped, Commands.MycroForge.Install>(); services.AddScoped, Commands.MycroForge.Uninstall>(); + // Register "m4g generate" + services.AddScoped, Commands.MycroForge.Generate>(); + services.AddScoped, Commands.MycroForge.Generate.Service>(); + services.AddScoped, Commands.MycroForge.Generate.Venv>(); + // Register "m4g api" services.AddScoped, Commands.MycroForge.Api>(); services.AddScoped, Commands.MycroForge.Api.Run>(); diff --git a/MycroForge.CLI/Extensions/StringExtensions.cs b/MycroForge.CLI/Extensions/StringExtensions.cs new file mode 100644 index 0000000..60eea98 --- /dev/null +++ b/MycroForge.CLI/Extensions/StringExtensions.cs @@ -0,0 +1,13 @@ +using Humanizer; + +namespace MycroForge.CLI.Extensions; + +public static class StringExtensions +{ + public static string NormalizePath(this string name) + { + var directoryPath = Path.GetDirectoryName(name).Underscore(); + var filePath = Path.Join(directoryPath, name.Underscore().ToLower()); + return filePath; + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Features/Api.cs b/MycroForge.CLI/Features/Api.cs index c05b261..3fcd922 100644 --- a/MycroForge.CLI/Features/Api.cs +++ b/MycroForge.CLI/Features/Api.cs @@ -36,12 +36,6 @@ public sealed class Api : IFeature { var config = await context.LoadConfig(); - if (config.Features.Contains(FeatureName)) - { - Console.WriteLine($"Feature {FeatureName} has already been initialized."); - return; - } - await context.Bash( "source .venv/bin/activate", "python3 -m pip install fastapi uvicorn[standard]", @@ -54,8 +48,11 @@ public sealed class Api : IFeature main = string.Join('\n', Main) + main; await context.WriteFile("main.py", main); - config.Features.Add(FeatureName); - + config.Api = new() + { + Port = 8000 + }; + await context.SaveConfig(config); } } \ No newline at end of file diff --git a/MycroForge.CLI/Features/Git.cs b/MycroForge.CLI/Features/Git.cs index 804062e..05ecfa7 100644 --- a/MycroForge.CLI/Features/Git.cs +++ b/MycroForge.CLI/Features/Git.cs @@ -2,59 +2,11 @@ namespace MycroForge.CLI.Features; public class Git : IFeature { - #region GitIgnore - - private static readonly string[] GitIgnore = - [ - "# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions", - "*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/", - ".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/", - ".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller", - "# Usually these files are written by a python script from a template", - "# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec", - "# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports", - "htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml", - "*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot", - "# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:", - "instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/", - "# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython", - "profile_default/", "ipython_config.py", "# pyenv", - "# For a library or package, you might want to ignore these files since the code is", - "# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv", - "# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.", - "# However, in case of collaboration, if having platform-specific dependencies or dependencies", - "# having no cross-platform support, pipenv may install dependencies that don't work, or not", - "# install all needed dependencies.", "#Pipfile.lock", "# poetry", - "# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.", - "# This is especially recommended for binary packages to ensure reproducibility, and is more", - "# commonly ignored for libraries.", - "# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control", - "#poetry.lock", "# pdm", - "# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.", - "#pdm.lock", - "# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it", - "# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml", - "# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/", - "# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py", - "# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/", - "# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject", - "# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json", - "# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols", - "cython_debug/", "# PyCharm", - "# JetBrains specific template is maintained in a separate JetBrains.gitignore that can", - "# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore", - "# and can be added to the global gitignore or merged into this file. For a more nuclear", - "# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/" - ]; - - #endregion - public const string FeatureName = "git"; public string Name => FeatureName; public async Task ExecuteAsync(ProjectContext context) { - await context.CreateFile(".gitignore", GitIgnore); await context.Bash($"git -c init.defaultBranch=main init {context.RootDirectory}"); } } \ No newline at end of file diff --git a/MycroForge.CLI/Features/Orm.cs b/MycroForge.CLI/Features/Orm.cs index 3e10388..62c4f88 100644 --- a/MycroForge.CLI/Features/Orm.cs +++ b/MycroForge.CLI/Features/Orm.cs @@ -60,12 +60,6 @@ public sealed class Orm : IFeature { var config = await context.LoadConfig(); - if (config.Features.Contains(FeatureName)) - { - Console.WriteLine($"Feature {FeatureName} has already been initialized."); - return; - } - await context.Bash( "source .venv/bin/activate", "python3 -m pip install asyncmy sqlalchemy alembic", @@ -86,8 +80,6 @@ public sealed class Orm : IFeature await context.CreateFile("orm/entities/user.py", User); - config.Features.Add(FeatureName); - await context.SaveConfig(config); } } \ No newline at end of file diff --git a/MycroForge.CLI/Program.cs b/MycroForge.CLI/Program.cs index e47b520..690ad3d 100644 --- a/MycroForge.CLI/Program.cs +++ b/MycroForge.CLI/Program.cs @@ -15,7 +15,8 @@ using var host = Host try { - await host.Services.GetRequiredService().InvokeAsync(args); + await host.Services.GetRequiredService() + .InvokeAsync(args.Length == 0 ? ["--help"] : args); } catch(Exception e) { diff --git a/MycroForge.CLI/ProjectConfig.ApiConfig.cs b/MycroForge.CLI/ProjectConfig.ApiConfig.cs new file mode 100644 index 0000000..6ed878f --- /dev/null +++ b/MycroForge.CLI/ProjectConfig.ApiConfig.cs @@ -0,0 +1,9 @@ +namespace MycroForge.CLI; + +public partial class ProjectConfig +{ + public class ApiConfig + { + public int Port { get; set; } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/ProjectConfig.cs b/MycroForge.CLI/ProjectConfig.cs index 24ca395..80cf920 100644 --- a/MycroForge.CLI/ProjectConfig.cs +++ b/MycroForge.CLI/ProjectConfig.cs @@ -1,6 +1,6 @@ namespace MycroForge.CLI; -public class ProjectConfig +public partial class ProjectConfig { - public List Features { get; set; } = new(); + public ApiConfig Api { get; set; } = default!; } \ No newline at end of file diff --git a/MycroForge.CLI/ProjectContext.cs b/MycroForge.CLI/ProjectContext.cs index 95959a7..9e40d30 100644 --- a/MycroForge.CLI/ProjectContext.cs +++ b/MycroForge.CLI/ProjectContext.cs @@ -32,6 +32,18 @@ public class ProjectContext RootDirectory = path; } + public void AssertDirectoryExists(string path) + { + var fullPath = Path.Combine(RootDirectory, path); + + if (Directory.Exists(fullPath)) + { + throw new(string.Join('\n', + $"{path} does not exist, make sure your in the correct directory." + )); + } + } + public async Task CreateFile(string path, params string[] content) { var fullPath = Path.Combine(RootDirectory, path); diff --git a/MycroForge.CLI/scripts/publish-linux.sh b/MycroForge.CLI/scripts/publish-linux.sh index 03b3e61..ae2bad0 100644 --- a/MycroForge.CLI/scripts/publish-linux.sh +++ b/MycroForge.CLI/scripts/publish-linux.sh @@ -1,6 +1,11 @@ #!/usr/bin/bash -# Make sure to run this script form the MyrcoForge.CLI directory with sudo! +# Make sure to run this script from the MycroForge.CLI directory and prefixed with sudo! +# Example: +# sudo ./scripts/publish-linux.sh + dotnet publish --self-contained -r linux-x64 -sudo cp bin/Release/net8.0/linux-x64 /usr/share/m4g -sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/bin/local/m4g +sudo rm -rf /usr/share/m4g +sudo cp -r bin/Release/net8.0/linux-x64 /usr/share/m4g +sudo unlink /usr/local/bin/m4g +sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g