Added scripting capabilities and cleaned up
This commit is contained in:
parent
68b9b0e8c8
commit
b23ba99ba4
@ -1,6 +0,0 @@
|
||||
namespace MycroForge.CLI;
|
||||
|
||||
public class ArgsContext
|
||||
{
|
||||
public string[] Args { get; init; } = Array.Empty<string>();
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MycroForge.CLI;
|
||||
|
||||
public static class Bash
|
||||
{
|
||||
public static async Task ExecuteAsync(params string[] script)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = "bash",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(info);
|
||||
|
||||
if (process is null)
|
||||
throw new NullReferenceException("Could not initialize bash process.");
|
||||
|
||||
process.OutputDataReceived += (sender, args) =>
|
||||
{
|
||||
// Only print data when it's not empty to prevent noise in the shell
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
Console.WriteLine(args.Data);
|
||||
};
|
||||
process.BeginOutputReadLine();
|
||||
|
||||
process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
// Only print data when it's not empty to prevent noise in the shell
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
Console.WriteLine(args.Data);
|
||||
};
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await using var input = process.StandardInput;
|
||||
foreach (var line in script)
|
||||
await input.WriteLineAsync(line);
|
||||
|
||||
await input.FlushAsync();
|
||||
input.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
Console.WriteLine($"Process finished with exit code {process.ExitCode}.");
|
||||
}
|
||||
}
|
@ -9,14 +9,17 @@ public partial class MycroForge
|
||||
{
|
||||
public class Run : Command, ISubCommandOf<Api>
|
||||
{
|
||||
public Run() : base("run", "Run your app")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Run(ProjectContext context) : base("run", "Run your app")
|
||||
{
|
||||
_context = context;
|
||||
this.SetHandler(ExecuteAsync);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
await Bash.ExecuteAsync([
|
||||
await _context.Bash([
|
||||
"source .venv/bin/activate",
|
||||
"uvicorn main:app --reload"
|
||||
]);
|
||||
|
@ -50,13 +50,12 @@ public partial class MycroForge
|
||||
|
||||
// Create the config file and initialize the config
|
||||
await _context.CreateFile("m4g.json", "{}");
|
||||
await _context.LoadConfig(force: true);
|
||||
|
||||
// Create the entrypoint file
|
||||
await _context.CreateFile("main.py");
|
||||
|
||||
// Create the venv
|
||||
await Bash.ExecuteAsync($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
|
||||
await _context.Bash($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
|
||||
|
||||
// Initialize default features
|
||||
foreach (var feature in _features.Where(f => DefaultFeatures.Contains(f.Name)))
|
||||
@ -77,7 +76,7 @@ public partial class MycroForge
|
||||
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
await Bash.ExecuteAsync($"chmod -R 777 {directory}");
|
||||
await _context.Bash($"chmod -R 777 {directory}");
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
@ -10,8 +10,12 @@ public partial class MycroForge
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
new(name: "packages", description: "The names of the packages to install");
|
||||
|
||||
public Install() : base("install", "Install packages and update the requirements.txt")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Install(ProjectContext context) :
|
||||
base("install", "Install packages and update the requirements.txt")
|
||||
{
|
||||
_context = context;
|
||||
AddAlias("i");
|
||||
AddArgument(PackagesArgument);
|
||||
this.SetHandler(ExecuteAsync, PackagesArgument);
|
||||
@ -19,7 +23,7 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(IEnumerable<string> packages)
|
||||
{
|
||||
await Bash.ExecuteAsync(
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"pip install {string.Join(' ', packages)}",
|
||||
"pip freeze > requirements.txt"
|
||||
|
@ -14,8 +14,11 @@ public partial class MycroForge
|
||||
private static readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of the migration");
|
||||
|
||||
public Migration() : base("migration", "Generate a migration")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Migration(ProjectContext context) : base("migration", "Generate a migration")
|
||||
{
|
||||
_context = context;
|
||||
AddAlias("m");
|
||||
AddArgument(NameArgument);
|
||||
this.SetHandler(ExecuteAsync, NameArgument);
|
||||
@ -23,7 +26,7 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(string name)
|
||||
{
|
||||
await Bash.ExecuteAsync(
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
|
||||
);
|
||||
|
@ -9,14 +9,17 @@ public partial class MycroForge
|
||||
{
|
||||
public class Migrate : Command, ISubCommandOf<Orm>
|
||||
{
|
||||
public Migrate() : base("migrate", "Apply migrations to the database")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Migrate(ProjectContext context) : base("migrate", "Apply migrations to the database")
|
||||
{
|
||||
_context = context;
|
||||
this.SetHandler(ExecuteAsync);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
await Bash.ExecuteAsync([
|
||||
await _context.Bash([
|
||||
"source .venv/bin/activate",
|
||||
"alembic upgrade head"
|
||||
]);
|
||||
|
@ -9,14 +9,17 @@ public partial class MycroForge
|
||||
{
|
||||
public class Rollback : Command, ISubCommandOf<Orm>
|
||||
{
|
||||
public Rollback() : base("rollback", "Rollback the last migration")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Rollback(ProjectContext context) : base("rollback", "Rollback the last migration")
|
||||
{
|
||||
_context = context;
|
||||
this.SetHandler(ExecuteAsync);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
await Bash.ExecuteAsync([
|
||||
await _context.Bash([
|
||||
"source .venv/bin/activate",
|
||||
"alembic downgrade -1"
|
||||
]);
|
||||
|
82
MycroForge.CLI/Commands/MycroForge.Script.Run.cs
Normal file
82
MycroForge.CLI/Commands/MycroForge.Script.Run.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System.CommandLine;
|
||||
using System.Dynamic;
|
||||
using System.Text;
|
||||
using IronPython.Hosting;
|
||||
using MycroForge.CLI.Commands.Interfaces;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
public partial class Script
|
||||
{
|
||||
public class Run : Command, ISubCommandOf<Script>
|
||||
{
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
private static readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of the script");
|
||||
|
||||
private static readonly Argument<IEnumerable<string>> ArgsArgument =
|
||||
new(name: "args", description: "The args to the script");
|
||||
|
||||
public Run(ProjectContext context) : base("run", "Run a script")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
AddArgument(ArgsArgument);
|
||||
this.SetHandler(ExecuteAsync, NameArgument, ArgsArgument);
|
||||
}
|
||||
|
||||
private void ExecuteAsync(string name, IEnumerable<string> args)
|
||||
{
|
||||
var engine = Python.CreateEngine();
|
||||
using var output = new MemoryStream();
|
||||
using var error = new MemoryStream();
|
||||
engine.Runtime.IO.SetOutput(output, Encoding.Default);
|
||||
engine.Runtime.IO.SetErrorOutput(error, Encoding.Default);
|
||||
|
||||
var scope = engine.CreateScope();
|
||||
scope.SetVariable("args", args.ToArray());
|
||||
scope.SetVariable("context", CreateScriptContext());
|
||||
|
||||
try
|
||||
{
|
||||
var scriptPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".m4g", name
|
||||
);
|
||||
engine.ExecuteFile(scriptPath, scope);
|
||||
|
||||
if (output.Length > 0)
|
||||
Console.WriteLine(Encoding.Default.GetString(output.ToArray()));
|
||||
if (error.Length > 0)
|
||||
Console.WriteLine(Encoding.Default.GetString(error.ToArray()));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
engine.Runtime.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private dynamic CreateScriptContext()
|
||||
{
|
||||
var createFile = _context.CreateFile;
|
||||
var readFile = _context.ReadFile;
|
||||
var writeFile = _context.WriteFile;
|
||||
var bash = _context.Bash;
|
||||
|
||||
dynamic context = new ExpandoObject();
|
||||
context.create_file = createFile;
|
||||
context.read_file = readFile;
|
||||
context.write_file = writeFile;
|
||||
context.bash = bash;
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
MycroForge.CLI/Commands/MycroForge.Script.cs
Normal file
18
MycroForge.CLI/Commands/MycroForge.Script.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Interfaces;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
public partial class Script : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Script(IEnumerable<ISubCommandOf<Script>> subCommands) :
|
||||
base("script", "Script related commands")
|
||||
{
|
||||
AddAlias("s");
|
||||
foreach (var subCommandOf in subCommands.Cast<Command>())
|
||||
AddCommand(subCommandOf);
|
||||
}
|
||||
}
|
||||
}
|
@ -10,8 +10,11 @@ public partial class MycroForge
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
new(name: "packages", description: "The names of the packages to uninstall");
|
||||
|
||||
public Uninstall() : base("uninstall", "Uninstall packages and update the requirements.txt")
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Uninstall(ProjectContext context) : base("uninstall", "Uninstall packages and update the requirements.txt")
|
||||
{
|
||||
_context = context;
|
||||
AddAlias("u");
|
||||
AddArgument(PackagesArgument);
|
||||
this.SetHandler(ExecuteAsync, PackagesArgument);
|
||||
@ -19,7 +22,7 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(IEnumerable<string> packages)
|
||||
{
|
||||
await Bash.ExecuteAsync(
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"pip uninstall {string.Join(' ', packages)}",
|
||||
"pip freeze > requirements.txt"
|
||||
|
@ -6,14 +6,8 @@ namespace MycroForge.CLI.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddServices(this IServiceCollection services, string[] args)
|
||||
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ArgsContext>(_ =>
|
||||
new ArgsContext
|
||||
{
|
||||
// Make sure to display the help text when no args are passed
|
||||
Args = args.Length == 0 ? ["--help"] : args
|
||||
});
|
||||
services.AddScoped<ProjectContext>();
|
||||
services.AddScoped<IFeature, Git>();
|
||||
services.AddScoped<IFeature, Api>();
|
||||
@ -42,12 +36,15 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Rollback>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Generate>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Generate>, Commands.MycroForge.Orm.Generate.Entity>();
|
||||
services
|
||||
.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Generate>, Commands.MycroForge.Orm.Generate.Migration>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Generate>, Commands.MycroForge.Orm.Generate.Migration>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm>, Commands.MycroForge.Orm.Link>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Link>, Commands.MycroForge.Orm.Link.One>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Orm.Link>, Commands.MycroForge.Orm.Link.Many>();
|
||||
|
||||
// Register "m4g script"
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge>, Commands.MycroForge.Script>();
|
||||
services.AddScoped<ISubCommandOf<Commands.MycroForge.Script>, Commands.MycroForge.Script.Run>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
@ -34,13 +34,15 @@ public sealed class Api : IFeature
|
||||
|
||||
public async Task ExecuteAsync(ProjectContext context)
|
||||
{
|
||||
if (context.Config.Features.Contains(FeatureName))
|
||||
var config = await context.LoadConfig();
|
||||
|
||||
if (config.Features.Contains(FeatureName))
|
||||
{
|
||||
Console.WriteLine($"Feature {FeatureName} has already been initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Bash.ExecuteAsync(
|
||||
await context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
"python3 -m pip install fastapi uvicorn[standard]",
|
||||
"python3 -m pip freeze > requirements.txt"
|
||||
@ -52,6 +54,8 @@ public sealed class Api : IFeature
|
||||
main = string.Join('\n', Main) + main;
|
||||
await context.WriteFile("main.py", main);
|
||||
|
||||
context.Config.Features.Add(FeatureName);
|
||||
config.Features.Add(FeatureName);
|
||||
|
||||
await context.SaveConfig(config);
|
||||
}
|
||||
}
|
@ -55,6 +55,6 @@ public class Git : IFeature
|
||||
public async Task ExecuteAsync(ProjectContext context)
|
||||
{
|
||||
await context.CreateFile(".gitignore", GitIgnore);
|
||||
await Bash.ExecuteAsync($"git -c init.defaultBranch=main init {context.RootDirectory}");
|
||||
await context.Bash($"git -c init.defaultBranch=main init {context.RootDirectory}");
|
||||
}
|
||||
}
|
@ -58,13 +58,15 @@ public sealed class Orm : IFeature
|
||||
|
||||
public async Task ExecuteAsync(ProjectContext context)
|
||||
{
|
||||
if (context.Config.Features.Contains(FeatureName))
|
||||
var config = await context.LoadConfig();
|
||||
|
||||
if (config.Features.Contains(FeatureName))
|
||||
{
|
||||
Console.WriteLine($"Feature {FeatureName} has already been initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
await Bash.ExecuteAsync(
|
||||
await context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
"python3 -m pip install asyncmy sqlalchemy alembic",
|
||||
"python3 -m pip freeze > requirements.txt",
|
||||
@ -84,6 +86,8 @@ public sealed class Orm : IFeature
|
||||
|
||||
await context.CreateFile("orm/entities/user.py", User);
|
||||
|
||||
context.Config.Features.Add(FeatureName);
|
||||
config.Features.Add(FeatureName);
|
||||
|
||||
await context.SaveConfig(config);
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Antlr4.Runtime" Version="4.6.6" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||
<PackageReference Include="IronPython" Version="3.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
|
@ -9,17 +9,14 @@ using var host = Host
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
services
|
||||
.AddServices(args)
|
||||
.AddServices()
|
||||
.AddCommands();
|
||||
})
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var ctx = host.Services.GetRequiredService<ProjectContext>();
|
||||
await ctx.LoadConfig();
|
||||
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>().InvokeAsync(args);
|
||||
await ctx.SaveConfig();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using MycroForge.CLI.Extensions;
|
||||
|
||||
namespace MycroForge.CLI;
|
||||
@ -6,33 +7,23 @@ namespace MycroForge.CLI;
|
||||
public class ProjectContext
|
||||
{
|
||||
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
||||
public string ConfigPath => Path.Combine(RootDirectory, "m4g.json");
|
||||
public ProjectConfig Config { get; private set; } = default!;
|
||||
private string ConfigPath => Path.Combine(RootDirectory, "m4g.json");
|
||||
|
||||
private readonly ArgsContext _argsContext;
|
||||
|
||||
public ProjectContext(ArgsContext argsContext)
|
||||
public async Task<ProjectConfig> LoadConfig()
|
||||
{
|
||||
_argsContext = argsContext;
|
||||
}
|
||||
|
||||
public async Task LoadConfig(bool force = false)
|
||||
{
|
||||
if (_argsContext.Args
|
||||
is ["init", ..]
|
||||
or ["-?", ..] or [.., "-?"]
|
||||
or ["-h", ..] or [.., "-h"]
|
||||
or ["--help"] or [.., "--help"]
|
||||
or ["--version"] && !force)
|
||||
return;
|
||||
|
||||
if (!File.Exists(ConfigPath))
|
||||
throw new FileNotFoundException($"File {ConfigPath} does not exist.");
|
||||
|
||||
Config = (await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
||||
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
||||
File.OpenRead(ConfigPath),
|
||||
Shared.DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||
))!;
|
||||
);
|
||||
|
||||
if (config is null)
|
||||
throw new Exception($"Could not load {ConfigPath}");
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public void ChangeDirectory(string path)
|
||||
@ -50,7 +41,7 @@ public class ProjectContext
|
||||
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
await Bash.ExecuteAsync($"chmod 777 {fullPath}");
|
||||
await Bash($"chmod 777 {fullPath}");
|
||||
}
|
||||
|
||||
public async Task<string> ReadFile(string path)
|
||||
@ -72,12 +63,55 @@ public class ProjectContext
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
}
|
||||
|
||||
public async Task SaveConfig()
|
||||
public async Task Bash(params string[] script)
|
||||
{
|
||||
if (Config is not null)
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
var json = await Config.SerializeAsync(Shared.DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
||||
await File.WriteAllTextAsync(ConfigPath, json);
|
||||
}
|
||||
FileName = "bash",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
using var process = Process.Start(info);
|
||||
|
||||
if (process is null)
|
||||
throw new NullReferenceException("Could not initialize bash process.");
|
||||
|
||||
process.OutputDataReceived += (sender, args) =>
|
||||
{
|
||||
// Only print data when it's not empty to prevent noise in the shell
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
Console.WriteLine(args.Data);
|
||||
};
|
||||
process.BeginOutputReadLine();
|
||||
|
||||
process.ErrorDataReceived += (sender, args) =>
|
||||
{
|
||||
// Only print data when it's not empty to prevent noise in the shell
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
Console.WriteLine(args.Data);
|
||||
};
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await using var input = process.StandardInput;
|
||||
foreach (var line in script)
|
||||
await input.WriteLineAsync(line);
|
||||
|
||||
await input.FlushAsync();
|
||||
input.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
Console.WriteLine($"Process finished with exit code {process.ExitCode}.");
|
||||
}
|
||||
|
||||
public async Task SaveConfig(ProjectConfig config)
|
||||
{
|
||||
var json = await config.SerializeAsync(Shared.DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
||||
await File.WriteAllTextAsync(ConfigPath, json);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user