添加项目文件。

This commit is contained in:
ShaoHua
2026-07-15 10:39:27 +08:00
parent cc7a15d44c
commit e639ade569
55 changed files with 2196 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NuGet.Versioning" Version="6.11.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Core" Version="9.0.0" />
<PackageReference Include="Volo.Abp.Json" Version="9.0.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,62 @@
using System;
using System.Linq;
using System.Text;
namespace Volo.Abp.Cli.Args;
public class CommandLineArgs
{
public string? Command { get; }
public string? Target { get; }
public HuaCommandLineOptions Options { get; }
public CommandLineArgs(string? command = null, string? target = null)
{
Command = command;
Target = target;
Options = new HuaCommandLineOptions();
}
public static CommandLineArgs Empty()
{
return new CommandLineArgs();
}
public bool IsCommand(string command)
{
return string.Equals(Command, command, StringComparison.OrdinalIgnoreCase);
}
public override string ToString()
{
var sb = new StringBuilder();
if (Command != null)
{
sb.Append("Command: ").AppendLine(Command);
}
if (Target != null)
{
sb.Append("Target: ").AppendLine(Target);
}
if (Options.Any())
{
sb.AppendLine("Options:");
foreach (var option in Options)
{
sb.Append(" - ").Append(option.Key).Append(" = ").AppendLine(option.Value);
}
}
if (sb.Length <= 0)
{
sb.Append("<EMPTY>");
}
return sb.ToString();
}
}
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Args;
public class CommandLineArgumentParser : ICommandLineArgumentParser, ITransientDependency
{
public CommandLineArgs Parse(string[] args)
{
if (args == null || args.Length == 0)
{
return CommandLineArgs.Empty();
}
var argumentList = args.ToList();
// Command
var command = argumentList[0];
argumentList.RemoveAt(0);
if (!argumentList.Any())
{
return new CommandLineArgs(command);
}
// Target
var target = argumentList[0];
if (target.StartsWith("-"))
{
target = null;
}
else
{
argumentList.RemoveAt(0);
}
if (!argumentList.Any())
{
return new CommandLineArgs(command, target);
}
// Options
var commandLineArgs = new CommandLineArgs(command, target);
while (argumentList.Any())
{
var optionName = ParseOptionName(argumentList[0]);
argumentList.RemoveAt(0);
if (!argumentList.Any())
{
commandLineArgs.Options[optionName] = null;
break;
}
if (IsOptionName(argumentList[0]))
{
commandLineArgs.Options[optionName] = null;
continue;
}
commandLineArgs.Options[optionName] = argumentList[0];
argumentList.RemoveAt(0);
}
return commandLineArgs;
}
public CommandLineArgs Parse(string lineText)
{
return Parse(GetArgsArrayFromLine(lineText));
}
private static bool IsOptionName(string argument)
{
return argument.StartsWith("-") || argument.StartsWith("--");
}
private static string ParseOptionName(string argument)
{
if (argument.StartsWith("--"))
{
if (argument.Length <= 2)
{
throw new ArgumentException("Should specify an option name after '--' prefix!");
}
return argument.Substring(2);
}
if (argument.StartsWith("-"))
{
if (argument.Length <= 1)
{
throw new ArgumentException("Should specify an option name after '-' prefix!");
}
return argument.Substring(1);
}
throw new ArgumentException("Option names should start with '-' or '--'.");
}
private static string[] GetArgsArrayFromLine(string lineText)
{
var args = new List<string>();
var currentArgBuilder = new StringBuilder();
string currentArg;
var isInQuotes = false;
for (int i = 0; i < lineText.Length; i++)
{
var c = lineText[i];
if (c == ' ' && !isInQuotes)
{
currentArg = currentArgBuilder.ToString();
if (!string.IsNullOrWhiteSpace(currentArg))
{
args.Add(currentArg);
}
currentArgBuilder = new StringBuilder();
}
else
{
if (c == '\"')
{
isInQuotes = !isInQuotes;
}
else
{
currentArgBuilder.Append(c);
}
}
}
currentArg = currentArgBuilder.ToString();
if (!string.IsNullOrWhiteSpace(currentArg))
{
args.Add(currentArg);
}
return args.ToArray();
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Volo.Abp.Cli.Args;
public class HuaCommandLineOptions : Dictionary<string, string?>
{
public string? GetOrNull(string name, params string[] alternativeNames)
{
var value = this.GetOrDefault(name);
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
if (alternativeNames != null && alternativeNames.Length > 0)
{
foreach (var alternativeName in alternativeNames)
{
value = this.GetOrDefault(alternativeName);
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
}
return null;
}
}
@@ -0,0 +1,8 @@
namespace Volo.Abp.Cli.Args;
public interface ICommandLineArgumentParser
{
CommandLineArgs Parse(string[] args);
CommandLineArgs Parse(string lineText);
}
+12
View File
@@ -0,0 +1,12 @@
namespace Volo.Abp.Cli;
public static class CliConsts
{
public const string Command = "HuaCliCommand";
public const string HttpClientName = "HuaHttpClient";
public const string AppSettingsJsonFileName = "appsettings.json";
public const string AppSettingsSecretJsonFileName = "appsettings.secrets.json";
}
+14
View File
@@ -0,0 +1,14 @@
using System;
using System.IO;
namespace Volo.Abp.Cli;
public static class CliPaths
{
public static string TemplateCache => Path.Combine(HuaRootPath, "templates");
public static string Log => Path.Combine(HuaRootPath, "cli", "logs");
public static string Root => Path.Combine(HuaRootPath, "cli");
public static readonly string HuaRootPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".hua");
}
+60
View File
@@ -0,0 +1,60 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli;
public class CliService : ITransientDependency
{
public ILogger<CliService> Logger { get; set; }
protected ICommandLineArgumentParser CommandLineArgumentParser { get; }
protected ICommandSelector CommandSelector { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
public CliService(
ICommandLineArgumentParser commandLineArgumentParser,
ICommandSelector commandSelector,
IServiceScopeFactory serviceScopeFactory)
{
CommandLineArgumentParser = commandLineArgumentParser;
CommandSelector = commandSelector;
ServiceScopeFactory = serviceScopeFactory;
Logger = NullLogger<CliService>.Instance;
}
public async Task RunAsync(string[] args)
{
var commandLineArgs = CommandLineArgumentParser.Parse(args);
try
{
await RunInternalAsync(commandLineArgs);
}
catch (CliUsageException usageException)
{
Logger.LogWarning(usageException.Message);
Environment.ExitCode = 1;
}
catch (Exception ex)
{
Logger.LogError(ex, "An unexpected error occurred.");
throw;
}
}
private async Task RunInternalAsync(CommandLineArgs commandLineArgs)
{
var commandType = CommandSelector.Select(commandLineArgs);
using (var scope = ServiceScopeFactory.CreateScope())
{
var command = (IConsoleCommand)scope.ServiceProvider.GetRequiredService(commandType);
await command.ExecuteAsync(commandLineArgs);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Volo.Abp.Cli;
public static class CliUrls
{
public const string HuaApiBaseUrl = "https://api.hua.com";
}
@@ -0,0 +1,16 @@
using System;
namespace Volo.Abp.Cli;
public class CliUsageException : Exception
{
public CliUsageException(string message)
: base(message)
{
}
public CliUsageException(string message, Exception innerException)
: base(message, innerException)
{
}
}
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Options;
using System;
using Volo.Abp.Cli.Args;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands;
public class CommandSelector : ICommandSelector, ITransientDependency
{
protected HuaCliOptions Options { get; }
public CommandSelector(IOptions<HuaCliOptions> options)
{
Options = options.Value;
}
public Type Select(CommandLineArgs commandLineArgs)
{
if (string.IsNullOrWhiteSpace(commandLineArgs.Command))
{
return typeof(HelpCommand);
}
Options.Commands.TryGetValue(commandLineArgs.Command, out var commandType);
return commandType ?? typeof(HelpCommand);
}
}
@@ -0,0 +1,97 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.Cli.Args;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands;
public class HelpCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "help";
public ILogger<HelpCommand> Logger { get; set; }
protected HuaCliOptions HuaCliOptions { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
public HelpCommand(IOptions<HuaCliOptions> cliOptions,
IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
Logger = NullLogger<HelpCommand>.Instance;
HuaCliOptions = cliOptions.Value;
}
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
if (string.IsNullOrWhiteSpace(commandLineArgs.Target))
{
Logger.LogInformation(GetUsageInfo());
return Task.CompletedTask;
}
if (!HuaCliOptions.Commands.ContainsKey(commandLineArgs.Target))
{
Logger.LogWarning($"There is no command named {commandLineArgs.Target}.");
Logger.LogInformation(GetUsageInfo());
return Task.CompletedTask;
}
var commandType = HuaCliOptions.Commands[commandLineArgs.Target];
using (var scope = ServiceScopeFactory.CreateScope())
{
var command = (IConsoleCommand)scope.ServiceProvider.GetRequiredService(commandType);
Logger.LogInformation(command.GetUsageInfo());
}
return Task.CompletedTask;
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine();
sb.AppendLine("Usage:");
sb.AppendLine();
sb.AppendLine(" hua <command> <target> [options]");
sb.AppendLine();
sb.AppendLine("Command List:");
sb.AppendLine();
foreach (var command in HuaCliOptions.Commands.ToArray().OrderBy(x => x.Key))
{
var method = command.Value.GetMethod("GetShortDescription", BindingFlags.Static | BindingFlags.Public);
if (method == null)
{
continue;
}
var shortDescription = (string)method.Invoke(null, null);
sb.Append(" > ");
sb.Append(command.Key);
sb.Append(string.IsNullOrWhiteSpace(shortDescription) ? "" : ":");
sb.Append(" ");
sb.AppendLine(shortDescription);
}
sb.AppendLine();
sb.AppendLine("To get a detailed help for a command:");
sb.AppendLine();
sb.AppendLine(" hua help <command>");
return sb.ToString();
}
public static string GetShortDescription()
{
return "Show command line help. Write ` hua help <command> `";
}
}
@@ -0,0 +1,9 @@
using System;
using Volo.Abp.Cli.Args;
namespace Volo.Abp.Cli.Commands;
public interface ICommandSelector
{
Type Select(CommandLineArgs commandLineArgs);
}
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
namespace Volo.Abp.Cli.Commands;
public interface IConsoleCommand
{
Task ExecuteAsync(CommandLineArgs commandLineArgs);
string GetUsageInfo();
}
@@ -0,0 +1,192 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands;
public class NewCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "new";
public ILogger<NewCommand> Logger { get; set; }
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
protected ITemplateInfoProvider TemplateInfoProvider { get; }
public NewCommand(
ITemplateInfoProvider templateInfoProvider,
TemplateProjectBuilder templateProjectBuilder)
{
TemplateInfoProvider = templateInfoProvider;
TemplateProjectBuilder = templateProjectBuilder;
Logger = NullLogger<NewCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
var projectName = commandLineArgs.Target;
if (string.IsNullOrWhiteSpace(projectName))
{
throw new CliUsageException(
"Project name is missing!" +
Environment.NewLine + Environment.NewLine +
GetUsageInfo());
}
Logger.LogInformation("Creating your project...");
Logger.LogInformation("Project name: " + projectName);
var template = commandLineArgs.Options.GetOrNull(Options.Template.Short, Options.Template.Long);
if (template != null)
{
Logger.LogInformation("Template: " + template);
}
else
{
template = "app";
}
var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long);
var outputDir = commandLineArgs.Options.GetOrNull(
Options.OutputDirectory.Short, Options.OutputDirectory.Long);
if (string.IsNullOrWhiteSpace(outputDir))
{
outputDir = Path.Combine(Directory.GetCurrentDirectory(), projectName);
}
else
{
outputDir = Path.Combine(outputDir, projectName);
}
var templateInfo = await TemplateInfoProvider.GetAsync(template);
if (templateInfo == null)
{
throw new CliUsageException($"Template '{template}' not found.");
}
var databaseProvider = commandLineArgs.Options.GetOrNull(
Options.DatabaseProvider.Short, Options.DatabaseProvider.Long);
var uiFramework = commandLineArgs.Options.GetOrNull(
Options.UiFramework.Short, Options.UiFramework.Long);
var connectionString = commandLineArgs.Options.GetOrNull(
Options.ConnectionString.Short, Options.ConnectionString.Long);
var createSolutionFolder = commandLineArgs.Options.ContainsKey(
Options.CreateSolutionFolder.Long);
var noRandomPort = commandLineArgs.Options.ContainsKey(
Options.NoRandomPort.Long);
var dryRun = commandLineArgs.Options.ContainsKey(
Options.DryRun.Short) || commandLineArgs.Options.ContainsKey(Options.DryRun.Long);
var buildArgs = new ProjectBuildArgs(
projectName,
templateInfo,
version,
outputDir,
databaseProvider,
uiFramework,
connectionString,
createSolutionFolder,
noRandomPort,
dryRun
);
await TemplateProjectBuilder.BuildAsync(buildArgs);
Logger.LogInformation($"Project '{projectName}' created successfully at: {outputDir}");
}
public string GetUsageInfo()
{
return @"Usage:
hua new <project-name> [options]
Options:
-t|--template <template-name> (default: app)
-v|--version <version> Semantic version number
-o|--output <output-directory> Output directory
-db|--database-provider <provider> Database provider (ef)
-u|--ui-framework <framework> UI framework (mvc / none)
-cs|--connection-string <string> Connection string
--create-solution-folder Create solution folder
--no-random-port Don't randomize ports
-d|--dry-run Dry run without creating files
Examples:
hua new MyProject
hua new MyProject -t app -db ef -u mvc
hua new MyProject -o D:\Projects";
}
public static string GetShortDescription()
{
return "Create a new solution based on a template.";
}
public static class Options
{
public static class Template
{
public const string Short = "t";
public const string Long = "template";
}
public static class Version
{
public const string Short = "v";
public const string Long = "version";
}
public static class OutputDirectory
{
public const string Short = "o";
public const string Long = "output";
}
public static class DatabaseProvider
{
public const string Short = "db";
public const string Long = "database-provider";
}
public static class UiFramework
{
public const string Short = "u";
public const string Long = "ui-framework";
}
public static class ConnectionString
{
public const string Short = "cs";
public const string Long = "connection-string";
}
public static class CreateSolutionFolder
{
public const string Long = "create-solution-folder";
}
public static class NoRandomPort
{
public const string Long = "no-random-port";
}
public static class DryRun
{
public const string Short = "d";
public const string Long = "dry-run";
}
}
}
@@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands;
public class UpdateCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "update";
public ILogger<UpdateCommand> Logger { get; set; }
protected NugetPackagesVersionUpdater NugetPackagesVersionUpdater { get; }
public UpdateCommand(NugetPackagesVersionUpdater nugetPackagesVersionUpdater)
{
NugetPackagesVersionUpdater = nugetPackagesVersionUpdater;
Logger = NullLogger<UpdateCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
var version = commandLineArgs.Options.GetOrNull(
Options.Version.Short, Options.Version.Long);
var dryRun = commandLineArgs.Options.ContainsKey(
Options.DryRun.Short) || commandLineArgs.Options.ContainsKey(Options.DryRun.Long);
if (dryRun)
{
Logger.LogInformation("Dry run mode. No changes will be made.");
}
await NugetPackagesVersionUpdater.UpdatePackagesAsync(version, dryRun);
if (version != null)
{
Logger.LogInformation($"Updated packages to version {version}.");
}
else
{
Logger.LogInformation("Updated packages to the latest version.");
}
}
public string GetUsageInfo()
{
return @"Usage:
hua update [options]
Options:
-v|--version <version> Target version number
-d|--dry-run Dry run without making changes
Examples:
hua update
hua update -v 9.0.0
hua update --dry-run";
}
public static string GetShortDescription()
{
return "Update NuGet packages to the specified or latest version.";
}
public static class Options
{
public static class Version
{
public const string Short = "v";
public const string Long = "version";
}
public static class DryRun
{
public const string Short = "d";
public const string Long = "dry-run";
}
}
}
@@ -0,0 +1,22 @@
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Configuration;
public class ConfigReader : IConfigReader, ITransientDependency
{
public async Task<HuaCliConfig> ReadAsync(string directory)
{
var configFilePath = Path.Combine(directory, "hua-cli.json");
if (!File.Exists(configFilePath))
{
return new HuaCliConfig();
}
var json = await File.ReadAllTextAsync(configFilePath);
return JsonConvert.DeserializeObject<HuaCliConfig>(json) ?? new HuaCliConfig();
}
}
@@ -0,0 +1,8 @@
namespace Volo.Abp.Cli.Configuration;
public class HuaCliConfig
{
public string? DefaultTemplate { get; set; }
public string? DefaultDatabaseProvider { get; set; }
public string? DefaultUiFramework { get; set; }
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.Configuration;
public interface IConfigReader
{
Task<HuaCliConfig> ReadAsync(string directory);
}
@@ -0,0 +1,12 @@
using System.Net.Http;
namespace Volo.Abp.Cli.Http;
public class CliHttpClientHandler : HttpClientHandler
{
public CliHttpClientHandler()
{
// Allow all certificates for development scenarios
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
}
@@ -0,0 +1,25 @@
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
namespace Volo.Abp.Cli;
[DependsOn(
typeof(AbpJsonModule)
)]
public class AbpCliCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Configure<HuaCliOptions>(options =>
{
options.Commands[HelpCommand.Name] = typeof(HelpCommand);
options.Commands[NewCommand.Name] = typeof(NewCommand);
options.Commands[UpdateCommand.Name] = typeof(UpdateCommand);
});
}
}
+18
View File
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Cli;
public class HuaCliOptions
{
public Dictionary<string, Type> Commands { get; }
public bool CacheTemplates { get; set; } = true;
public string ToolName { get; set; } = "CLI";
public HuaCliOptions()
{
Commands = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,8 @@
namespace Volo.Abp.Cli.ProjectBuilding.Building;
public class FileEntry
{
public string RelativePath { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public bool IsDirectory { get; set; }
}
@@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Volo.Abp.Cli.ProjectBuilding.Building;
public class ProjectBuildContext
{
public string ProjectName { get; }
public string TemplateName { get; }
public string? Version { get; }
public string OutputDirectory { get; }
public string? DatabaseProvider { get; }
public string? UiFramework { get; }
public string? ConnectionString { get; }
public bool CreateSolutionFolder { get; }
public bool NoRandomPort { get; }
public string TemplatePath { get; set; }
public List<FileEntry> Files { get; } = new List<FileEntry>();
public ProjectBuildContext(
string projectName,
string templateName,
string? version,
string outputDirectory,
string? databaseProvider,
string? uiFramework,
string? connectionString,
bool createSolutionFolder,
bool noRandomPort,
string templatePath)
{
ProjectName = projectName;
TemplateName = templateName;
Version = version;
OutputDirectory = outputDirectory;
DatabaseProvider = databaseProvider;
UiFramework = uiFramework;
ConnectionString = connectionString;
CreateSolutionFolder = createSolutionFolder;
NoRandomPort = noRandomPort;
TemplatePath = templatePath;
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building;
public class ProjectBuildPipeline
{
private readonly List<ProjectBuildPipelineStep> _steps = new List<ProjectBuildPipelineStep>();
public void AddStep(ProjectBuildPipelineStep step)
{
_steps.Add(step);
}
public async Task ExecuteAsync()
{
foreach (var step in _steps)
{
await step.ExecuteAsync(null!);
}
}
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building;
public abstract class ProjectBuildPipelineStep
{
public abstract Task ExecuteAsync(ProjectBuildContext context);
}
@@ -0,0 +1,37 @@
using System.IO;
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class CreateProjectResultStep : ProjectBuildPipelineStep
{
public override Task ExecuteAsync(ProjectBuildContext context)
{
Directory.CreateDirectory(context.OutputDirectory);
foreach (var file in context.Files)
{
if (file.IsDirectory)
{
continue;
}
var filePath = Path.Combine(context.OutputDirectory, file.RelativePath);
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
// Replace placeholder variables in content
var content = file.Content
.Replace("__PROJECT_NAME__", context.ProjectName)
.Replace("__VERSION__", context.Version ?? "1.0.0");
File.WriteAllText(filePath, content);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,45 @@
using System.IO;
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class FileEntryListReadStep : ProjectBuildPipelineStep
{
public override async Task ExecuteAsync(ProjectBuildContext context)
{
if (!File.Exists(context.TemplatePath))
{
return;
}
using var fileStream = File.OpenRead(context.TemplatePath);
using var zipStream = new ICSharpCode.SharpZipLib.Zip.ZipInputStream(fileStream);
var entry = zipStream.GetNextEntry();
while (entry != null)
{
if (!entry.IsDirectory)
{
using var reader = new StreamReader(zipStream);
var content = await reader.ReadToEndAsync();
context.Files.Add(new FileEntry
{
RelativePath = entry.Name,
Content = content,
IsDirectory = false
});
}
else
{
context.Files.Add(new FileEntry
{
RelativePath = entry.Name,
IsDirectory = true
});
}
entry = zipStream.GetNextEntry();
}
}
}
@@ -0,0 +1,25 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class ProjectReferenceReplaceStep : ProjectBuildPipelineStep
{
public override Task ExecuteAsync(ProjectBuildContext context)
{
foreach (var file in context.Files)
{
if (file.IsDirectory)
{
continue;
}
file.Content = file.Content.Replace(
$"MyCompanyName.MyProjectName", context.ProjectName);
file.Content = file.Content.Replace(
"MyProjectName", context.ProjectName);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,18 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class SolutionRenameStep : ProjectBuildPipelineStep
{
public override Task ExecuteAsync(ProjectBuildContext context)
{
var oldName = context.TemplateName;
foreach (var file in context.Files)
{
file.RelativePath = file.RelativePath.Replace(oldName, context.ProjectName);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,22 @@
using System.Linq;
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class TemplateCodeDeleteStep : ProjectBuildPipelineStep
{
public override Task ExecuteAsync(ProjectBuildContext context)
{
// Remove files marked as template-only
var filesToRemove = context.Files
.Where(f => f.RelativePath.Contains("__TEMPLATE__"))
.ToList();
foreach (var file in filesToRemove)
{
context.Files.Remove(file);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,20 @@
using Volo.Abp.Cli.ProjectBuilding.Building.Steps;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.ProjectBuilding.Building;
public class TemplateProjectBuildPipelineBuilder : ITransientDependency
{
public ProjectBuildPipeline Build(ProjectBuildContext context)
{
var pipeline = new ProjectBuildPipeline();
pipeline.AddStep(new FileEntryListReadStep());
pipeline.AddStep(new SolutionRenameStep());
pipeline.AddStep(new ProjectReferenceReplaceStep());
pipeline.AddStep(new TemplateCodeDeleteStep());
pipeline.AddStep(new CreateProjectResultStep());
return pipeline;
}
}
@@ -0,0 +1,24 @@
using System.IO;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.ProjectBuilding;
public class BuiltInSourceCodeStore : ISourceCodeStore, ITransientDependency
{
public Task<string> GetAsync(string name, string? version = null)
{
var templatePath = Path.Combine(
Path.GetDirectoryName(typeof(BuiltInSourceCodeStore).Assembly.Location) ?? ".",
"Templates",
"BuiltIn",
$"{name}.zip");
if (File.Exists(templatePath))
{
return Task.FromResult(templatePath);
}
throw new FileNotFoundException($"Template '{name}' not found at {templatePath}.");
}
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding;
public interface ISourceCodeStore
{
Task<string> GetAsync(string name, string? version = null);
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace Volo.Abp.Cli.ProjectBuilding;
public interface ITemplateInfoProvider
{
Task<TemplateInfo?> GetAsync(string name);
Task<TemplateInfo> GetDefaultAsync();
}
@@ -0,0 +1,39 @@
namespace Volo.Abp.Cli.ProjectBuilding;
public class ProjectBuildArgs
{
public string ProjectName { get; }
public TemplateInfo Template { get; }
public string? Version { get; }
public string OutputDirectory { get; }
public string? DatabaseProvider { get; }
public string? UiFramework { get; }
public string? ConnectionString { get; }
public bool CreateSolutionFolder { get; }
public bool NoRandomPort { get; }
public bool DryRun { get; }
public ProjectBuildArgs(
string projectName,
TemplateInfo template,
string? version,
string outputDirectory,
string? databaseProvider,
string? uiFramework,
string? connectionString,
bool createSolutionFolder,
bool noRandomPort,
bool dryRun)
{
ProjectName = projectName;
Template = template;
Version = version;
OutputDirectory = outputDirectory;
DatabaseProvider = databaseProvider;
UiFramework = uiFramework;
ConnectionString = connectionString;
CreateSolutionFolder = createSolutionFolder;
NoRandomPort = noRandomPort;
DryRun = dryRun;
}
}
@@ -0,0 +1,15 @@
namespace Volo.Abp.Cli.ProjectBuilding;
public abstract class TemplateInfo
{
public string Name { get; }
public string Description { get; }
public string? Version { get; }
protected TemplateInfo(string name, string description, string? version = null)
{
Name = name;
Description = description;
Version = version;
}
}
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.ProjectBuilding;
public class TemplateInfoProvider : ITemplateInfoProvider, ITransientDependency
{
public Task<TemplateInfo?> GetAsync(string name)
{
TemplateInfo? template = name switch
{
"app" => new AppTemplate(),
_ => null
};
return Task.FromResult(template);
}
public Task<TemplateInfo> GetDefaultAsync()
{
return Task.FromResult<TemplateInfo>(new AppTemplate());
}
}
@@ -0,0 +1,46 @@
using System.IO;
using System.Threading.Tasks;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.ProjectBuilding;
public class TemplateProjectBuilder : ITransientDependency
{
protected ISourceCodeStore SourceCodeStore { get; }
protected TemplateProjectBuildPipelineBuilder PipelineBuilder { get; }
public TemplateProjectBuilder(
ISourceCodeStore sourceCodeStore,
TemplateProjectBuildPipelineBuilder pipelineBuilder)
{
SourceCodeStore = sourceCodeStore;
PipelineBuilder = pipelineBuilder;
}
public async Task BuildAsync(ProjectBuildArgs args)
{
if (args.DryRun)
{
return;
}
var templatePath = await SourceCodeStore.GetAsync(
args.Template.Name, args.Version);
var context = new ProjectBuildContext(
args.ProjectName,
args.Template.Name,
args.Version,
args.OutputDirectory,
args.DatabaseProvider,
args.UiFramework,
args.ConnectionString,
args.CreateSolutionFolder,
args.NoRandomPort,
templatePath);
var pipeline = PipelineBuilder.Build(context);
await pipeline.ExecuteAsync();
}
}
@@ -0,0 +1,11 @@
namespace Volo.Abp.Cli.ProjectBuilding.Templates.App;
public class AppTemplate : TemplateInfo
{
public AppTemplate()
: base("app",
"Standard layered application (Application / Application.Contracts / Domain / Domain.Shared / EntityFrameworkCore / Web / HttpApi)",
null)
{
}
}
@@ -0,0 +1,66 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.ProjectModification;
public class NugetPackagesVersionUpdater : ITransientDependency
{
public Task UpdatePackagesAsync(string? version, bool dryRun)
{
var currentDir = Directory.GetCurrentDirectory();
var csprojFiles = Directory.GetFiles(currentDir, "*.csproj", SearchOption.AllDirectories);
foreach (var csprojPath in csprojFiles)
{
// Skip packages folder
if (csprojPath.Replace('\\', '/').Contains("/packages/"))
{
continue;
}
var content = File.ReadAllText(csprojPath);
var originalContent = content;
// Update Volo.Abp.* package references
if (!string.IsNullOrEmpty(version))
{
content = UpdatePackageReferences(content, version);
}
if (!dryRun && content != originalContent)
{
File.WriteAllText(csprojPath, content);
}
}
return Task.CompletedTask;
}
private string UpdatePackageReferences(string csprojContent, string version)
{
// Simple version replacement for PackageReference elements
var lines = csprojContent.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Contains("<PackageReference") &&
lines[i].Contains("Version="))
{
var line = lines[i];
var versionStart = line.IndexOf("Version=\"");
if (versionStart >= 0)
{
var versionEnd = line.IndexOf("\"", versionStart + 9);
if (versionEnd >= 0)
{
lines[i] = line.Substring(0, versionStart + 9) + version +
line.Substring(versionEnd);
}
}
}
}
return string.Join("\n", lines);
}
}
@@ -0,0 +1,55 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Utils;
public interface ICmdHelper
{
Task<CmdResult> RunAsync(string command, string? workingDirectory = null);
}
public class CmdResult
{
public string Output { get; set; } = string.Empty;
public string Error { get; set; } = string.Empty;
public int ExitCode { get; set; }
public bool IsSuccess => ExitCode == 0;
}
public class CmdHelper : ICmdHelper, ITransientDependency
{
public async Task<CmdResult> RunAsync(string command, string? workingDirectory = null)
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var processStartInfo = new ProcessStartInfo
{
FileName = isWindows ? "cmd.exe" : "/bin/bash",
Arguments = isWindows ? $"/c \"{command}\"" : $"-c \"{command}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
if (!string.IsNullOrEmpty(workingDirectory))
{
processStartInfo.WorkingDirectory = workingDirectory;
}
using var process = new Process { StartInfo = processStartInfo };
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return new CmdResult
{
Output = output,
Error = error,
ExitCode = process.ExitCode
};
}
}
@@ -0,0 +1,42 @@
using System;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Utils;
public interface IConsoleHelper
{
void WriteSuccess(string message);
void WriteInfo(string message);
void WriteWarning(string message);
void WriteError(string message);
}
public class ConsoleHelper : IConsoleHelper, ITransientDependency
{
public void WriteSuccess(string message)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(message);
Console.ResetColor();
}
public void WriteInfo(string message)
{
Console.ResetColor();
Console.WriteLine(message);
}
public void WriteWarning(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(message);
Console.ResetColor();
}
public void WriteError(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(message);
Console.ResetColor();
}
}
@@ -0,0 +1,29 @@
using System.IO;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Utils;
public interface IPathHelper
{
string GetCurrentDirectory();
bool IsDirectory(string path);
bool IsFile(string path);
}
public class PathHelper : IPathHelper, ITransientDependency
{
public string GetCurrentDirectory()
{
return Directory.GetCurrentDirectory();
}
public bool IsDirectory(string path)
{
return Directory.Exists(path);
}
public bool IsFile(string path)
{
return File.Exists(path);
}
}
@@ -0,0 +1,30 @@
using System.Reflection;
using System.Threading.Tasks;
using NuGet.Versioning;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Version;
public class CliVersionService : ITransientDependency
{
public Task<SemanticVersion> GetCurrentCliVersionAsync()
{
var assembly = typeof(CliVersionService).Assembly;
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion;
if (string.IsNullOrEmpty(version))
{
version = "1.0.0";
}
// Remove git commit hash suffix if present
var plusIndex = version.IndexOf('+');
if (plusIndex >= 0)
{
version = version.Substring(0, plusIndex);
}
return Task.FromResult(SemanticVersion.Parse(version));
}
}
@@ -0,0 +1,9 @@
using NuGet.Versioning;
namespace Volo.Abp.Cli.Version;
public class LatestVersionInfo
{
public SemanticVersion? Version { get; set; }
public string? Message { get; set; }
}
@@ -0,0 +1,56 @@
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NuGet.Versioning;
using Volo.Abp.Cli.Http;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Version;
public class PackageVersionCheckerService : ITransientDependency
{
protected IHttpClientFactory HttpClientFactory { get; }
public PackageVersionCheckerService(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
}
public async Task<LatestVersionInfo?> GetLatestVersionOrNullAsync(string packageId)
{
try
{
var client = HttpClientFactory.CreateClient(CliConsts.HttpClientName);
var response = await client.GetStringAsync(
$"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json");
var json = JObject.Parse(response);
var versions = json["versions"]?.ToObject<string[]>();
if (versions == null || versions.Length == 0)
{
return null;
}
SemanticVersion? latestVersion = null;
foreach (var versionStr in versions)
{
if (SemanticVersion.TryParse(versionStr, out var parsed))
{
if (!parsed.IsPrerelease && (latestVersion == null || parsed > latestVersion))
{
latestVersion = parsed;
}
}
}
return latestVersion != null
? new LatestVersionInfo { Version = latestVersion }
: null;
}
catch
{
return null;
}
}
}
+26
View File
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<TargetFramework>net9.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>hua</ToolCommandName>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Autofac" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Hua.Cli.Core\Hua.Cli.Core.csproj" />
</ItemGroup>
</Project>
+13
View File
@@ -0,0 +1,13 @@
using Volo.Abp.Autofac;
using Volo.Abp.Cli;
using Volo.Abp.Modularity;
namespace Hua.Cli;
[DependsOn(
typeof(AbpCliCoreModule),
typeof(AbpAutofacModule)
)]
public class HuaCliModule : AbpModule
{
}
+57
View File
@@ -0,0 +1,57 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Events;
using Volo.Abp;
using Volo.Abp.Cli;
namespace Hua.Cli;
public class Program
{
private static async Task Main(string[] args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning)
.MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
Path.Combine(CliPaths.Log, "hua-cli.log"),
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 10 * 1024 * 1024,
retainedFileCountLimit: 10
)
.CreateLogger();
try
{
using var application = AbpApplicationFactory.Create<HuaCliModule>(options =>
{
options.UseAutofac();
options.Services.AddLogging(c => c.AddSerilog());
});
application.Initialize();
await application.ServiceProvider
.GetRequiredService<CliService>()
.RunAsync(args);
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly.");
}
finally
{
Log.CloseAndFlush();
}
}
}