diff --git a/Hua.Cli.sln b/Hua.Cli.sln new file mode 100644 index 0000000..782610d --- /dev/null +++ b/Hua.Cli.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hua.Cli", "src\Hua.Cli\Hua.Cli.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hua.Cli.Core", "src\Hua.Cli.Core\Hua.Cli.Core.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hua.Cli.Tests", "test\Hua.Cli.Tests\Hua.Cli.Tests.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..0854396 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,342 @@ +# Hua.Cli 产品需求文档 (PRD) + +## 一、产品概述 + +### 1.1 产品定位 + +**Hua.Cli** 是面向 .NET 生态的全局命令行工具(Global .NET CLI Tool),为开发者提供项目脚手架生成和依赖版本管理等基础开发工作流支持。它借鉴 **Volo.Abp.Cli** 的分层插件化架构设计,面向 "Hua" 框架生态的快速开发场景。 + +### 1.2 产品目标 + +| 目标 | 描述 | +|---|---| +| 快速创建项目 | 通过预置项目模板,一行命令创建标准化的 .NET 解决方案 | +| 版本管理 | 支持 NuGet 包版本更新 | + +### 1.3 参考项目 + +本项目架构参考 ABP Framework 的命令行工具设计: + +| 参考项目 | 路径 | +|---|---| +| Volo.Abp.Cli | `D:\Codes\Abp\abp\framework\src\Volo.Abp.Cli` | +| Volo.Abp.Cli.Core | `D:\Codes\Abp\abp\framework\src\Volo.Abp.Cli.Core` | + +--- + +## 二、用户角色 + +| 角色 | 场景描述 | +|---|---| +| 后端开发者 | 创建项目、更新依赖版本 | + +--- + +## 三、功能需求 + +### 3.1 命令体系总览 + +``` +hua [target] [options] +``` + +| 序号 | 命令名 | 功能描述 | 优先级 | +|---|---|---|---| +| 1 | `help` | 显示帮助信息 | P0 | +| 2 | `new` | 基于模板创建新项目/解决方案 | P0 | +| 3 | `update` | 更新项目 NuGet 包版本 | P0 | + +### 3.2 核心命令详细需求 + +#### 3.2.1 `hua new` — 创建项目 + +``` +hua new <模板名> [选项] + +选项: + -n|--name 解决方案名称 + -o|--output 输出目录 + -t|--template 指定模板名称(默认 app) + --database-provider 指定数据库提供器 (ef) + --ui-framework 指定 UI 框架 (mvc / none) + --connection-string 指定连接字符串 + --create-solution-folder 创建解决方案文件夹 + --dry-run 试运行,不实际创建文件 + --no-random-port 不随机化端口 +``` + +**支持的模板类型:** + +| 模板名 | 描述 | 优先级 | +|---|---|---| +| `app` | 标准分层应用(Application / Application.Contracts / Domain / Domain.Shared / EntityFrameworkCore / Web / HttpApi) | P0 | + +#### 3.2.2 `hua update` — 更新依赖 + +``` +hua update [选项] + +选项: + -v|--version 目标版本号 + --dry-run 试运行 +``` + +--- + +## 四、技术架构 + +### 4.1 分层架构概览 + +``` +┌──────────────────────────────────────────────────────────┐ +│ Hua.Cli (启动入口) │ +│ - Program.cs (Main 入口) │ +│ - HuaCliModule.cs (ABP 模块定义) │ +│ - Serilog 日志配置 / Autofac DI 容器初始化 │ +└──────────────────────┬───────────────────────────────────┘ + │ 引用 +┌──────────────────────▼───────────────────────────────────┐ +│ Hua.Cli.Core (核心逻辑) │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Args │ │ Commands │ │ ProjectBuilding │ │ +│ │ (参数解析) │ │ (命令实现) │ │ (项目构建流水线) │ │ +│ └─────────────┘ └─────────────┘ └─────────────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Version │ │ ProjectModification │ │ Http │ │ +│ │ (版本管理) │ │ (项目修改引擎) │ │ (HTTP客户端) │ │ +│ └─────────────┘ └─────────────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Utils │ │ Configuration │ │ +│ │ (工具类) │ │ (配置管理) │ │ +│ └─────────────┘ └─────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +### 4.2 核心设计模式 + +#### 4.2.1 命令模式 (Command Pattern) + +所有操作封装为 `IConsoleCommand` 的实现,通过 `CommandSelector` 实现动态路由: + +``` +IConsoleCommand 接口: + - GetUsageInfo() → 返回使用说明 + - GetShortDescription() → 返回简短描述 + - ExecuteAsync(CommandLineArgs) → 执行命令 +``` + +命令在 `HuaCliCoreModule` 的 `ConfigureServices` 中通过 `HuaCliOptions.Commands` 字典注册。 + +#### 4.2.2 流水线模式 (Pipeline Pattern) + +项目构建采用 `ProjectBuildPipeline`: + +``` +ProjectBuildPipeline + ├── FileEntryListReadStep (读取模板文件) + ├── CreateAppSettingsSecretsStep (创建密钥文件) + ├── SolutionRenameStep (重命名解决方案) + ├── ProjectReferenceReplaceStep (替换项目引用) + ├── TemplateCodeDeleteStep (删除模板标记) + ├── DatabaseManagementSystemChangeStep (数据库适配) + ├── LicenseCodeReplaceStep (替换许可证) + ├── CreateProjectResultZipStep (打包结果) + └── ...自定义步骤 +``` + +#### 4.2.3 策略模式 (Strategy Pattern) + +- **项目模板**:不同模板通过继承 `TemplateInfo` 基类实现各自的自定义构建步骤 + +#### 4.2.4 依赖注入 (DI) + +- 通过 ABP 的 `ITransientDependency` / `ISingletonDependency` 接口实现自动注册 +- 使用 Autofac 作为 DI 容器 +- 所有服务均按约定自动注册,无需手动配置 + +### 4.3 启动流程 + +``` +Program.Main(args) + → Serilog 日志配置(文件 + 控制台双输出) + → AbpApplicationFactory.Create() + → application.Initialize() + → CliService.RunAsync(args) + → CommandLineArgumentParser.Parse(args) + → 解析命令名、目标、选项 + → 版本检查(非 Debug 模式) + → CommandSelector.Select(args) (选择命令) + → DI 容器解析命令实例 + → command.ExecuteAsync(args) (执行命令) +``` + +### 4.4 项目结构规划 + +参照 Volo.Abp.Cli 的两个程序集分离设计: + +``` +Hua.Cli/ +├── Hua.Cli.sln +│ +├── src/ +│ ├── Hua.Cli/ # 启动入口项目 +│ │ ├── Hua.Cli.csproj # OutputType: Exe, PackAsTool, ToolCommandName: hua +│ │ ├── Program.cs # Main 入口 +│ │ └── Hua/ +│ │ └── Cli/ +│ │ └── HuaCliModule.cs # ABP 模块定义 +│ │ +│ └── Hua.Cli.Core/ # 核心逻辑类库 +│ ├── Hua.Cli.Core.csproj +│ └── Hua/ +│ └── Cli/ +│ ├── HuaCliCoreModule.cs # 核心模块(注册所有命令) +│ ├── CliService.cs # 核心服务(命令路由主引擎) +│ ├── CliConsts.cs # 全局常量 +│ ├── CliPaths.cs # 文件路径常量 (~/.hua/cli/) +│ ├── CliUrls.cs # API 端点 URL +│ ├── HuaCliOptions.cs # CLI 全局选项(命令注册字典) +│ ├── CliUsageException.cs # 命令使用异常 +│ │ +│ ├── Args/ # 命令行参数解析 +│ │ ├── CommandLineArgs.cs +│ │ ├── CommandLineArgumentParser.cs +│ │ ├── ICommandLineArgumentParser.cs +│ │ └── HuaCommandLineOptions.cs +│ │ +│ ├── Commands/ # 命令实现 +│ │ ├── IConsoleCommand.cs +│ │ ├── ICommandSelector.cs +│ │ ├── CommandSelector.cs +│ │ ├── HelpCommand.cs +│ │ ├── NewCommand.cs +│ │ └── UpdateCommand.cs +│ │ +│ ├── Configuration/ # 配置读取 +│ │ ├── HuaCliConfig.cs +│ │ ├── IConfigReader.cs +│ │ └── ConfigReader.cs +│ │ +│ ├── Http/ # HTTP 客户端 +│ │ ├── CliHttpClientFactory.cs +│ │ └── CliHttpClientHandler.cs +│ │ +│ ├── ProjectBuilding/ # 项目构建引擎(核心) +│ │ ├── Building/ +│ │ │ ├── Steps/ # 构建步骤 +│ │ │ ├── ProjectBuildPipeline.cs +│ │ │ ├── ProjectBuildContext.cs +│ │ │ ├── ProjectBuildPipelineStep.cs +│ │ │ └── TemplateProjectBuildPipelineBuilder.cs +│ │ ├── Events/ # 构建事件 +│ │ ├── Files/ # 文件条目模型 +│ │ ├── Templates/ # 模板定义 +│ │ │ └── App/AppTemplate.cs +│ │ ├── TemplateProjectBuilder.cs +│ │ ├── TemplateInfoProvider.cs +│ │ └── ISourceCodeStore.cs +│ │ +│ ├── ProjectModification/ # 项目修改工具 +│ │ └── NugetPackagesVersionUpdater.cs +│ │ +│ ├── Utils/ # 工具类 +│ │ ├── CmdHelper.cs +│ │ ├── ConsoleHelper.cs +│ │ └── PathHelper.cs +│ │ +│ └── Version/ # 版本管理 +│ ├── CliVersionService.cs +│ ├── PackageVersionCheckerService.cs +│ └── LatestVersionInfo.cs +│ +├── test/ +│ └── Hua.Cli.Tests/ # 单元测试项目 +│ ├── Hua.Cli.Tests.csproj +│ └── ... +│ +└── templates/ # 项目模板文件 + └── app/ +``` + +### 4.5 技术栈 + +| 分类 | 技术/组件 | 说明 | +|---|---|---| +| 目标框架 (CLI入口) | .NET 9.0 | 启动项目 TargetFramework | +| 目标框架 (Core) | .NET Standard 2.0/2.1 + .NET 8.0 + .NET 9.0 | 兼容多框架 | +| DI 容器 | Autofac (Volo.Abp.Autofac) | 模块化的依赖注入 | +| 日志 | Serilog | 结构化日志 | +| 命令行解析 | 自研 CommandLineArgumentParser | 类似 System.CommandLine | +| HTTP | IHttpClientFactory + Polly | HTTP 客户端 + 重试策略 | +| JSON | Newtonsoft.Json / System.Text.Json | JSON 序列化 | +| 压缩 | SharpZipLib | ZIP 解压(模板下载) | +| 模板引擎 | 自研文件替换引擎 | 模板变量替换 | +| NuGet | NuGet.Versioning + NuGet.Protocol | 包版本管理 | +| 测试框架 | xUnit + NSubstitute + Shouldly | 单元测试 | + +--- + +## 五、非功能需求 + +### 5.1 性能要求 + +| 指标 | 要求 | +|---|---| +| 命令冷启动时间 | < 3 秒 | +| `new` 命令执行时间 | < 30 秒(标准 app 模板) | +| 内存占用 | < 200 MB | + +### 5.2 跨平台 + +- Windows 10+ +- macOS 12+ +- Linux (Ubuntu 20.04+, Debian 11+) + +### 5.3 可用性 + +- 支持彩色终端输出(ANSI 颜色码) +- 命令执行进度指示(Spinner / 进度条) +- 详细的错误信息和解决建议 +- 完善的 `--help` 文档和实例 + +### 5.4 可扩展性 + +- 通过 `IConsoleCommand` 接口,新增命令只需实现接口 + DI 注册 +- 通过 `ProjectBuildPipelineStep` 基类,新增构建步骤只需实现基类 +- 通过 `TemplateInfo` 基类,新增项目模板只需继承并实现自定义步骤 + +--- + +## 六、项目规划 + +### 6.1 开发阶段 + +| 阶段 | 内容 | 预计产出 | +|---|---|---| +| **P0 - 基础框架** | 项目结构搭建,模块定义,CliService 主引擎,CommandLineArgumentParser,Help 命令,new 命令(app 模板),update 命令,项目构建流水线核心,NuGet 包版本更新 | 可运行的基本 CLI 工具 | + +### 6.2 关键依赖方 + +| 依赖方 | 依赖内容 | +|---|---| +| Hua.Templates | 项目模板仓库(远端或内嵌) | + +--- + +## 七、风险与约束 + +| 风险/约束 | 影响 | 应对策略 | +|---|---|---| +| ABP 框架版本升级 | 模块 API 不兼容 | 锁定 ABP 主版本,定期同步升级 | +| .NET SDK 版本依赖 | 用户需要安装特定 SDK | 明确版本要求,自动检测并提示 | +| 模板仓库可用性 | 离线环境无法下载模板 | 支持内嵌模板 + 本地缓存机制 | + +--- + +## 八、术语表 + +| 术语 | 说明 | +|---|---| +| CLI Tool | .NET Global Tool,通过 `dotnet tool install -g` 安装 | +| Project Building Pipeline | 项目构建流水线,多个 Step 串联执行的构建过程 | +| Template | 项目模板,包含完整的解决方案文件结构和代码骨架 | diff --git a/src/Hua.Cli.Core/Hua.Cli.Core.csproj b/src/Hua.Cli.Core/Hua.Cli.Core.csproj new file mode 100644 index 0000000..85e4c89 --- /dev/null +++ b/src/Hua.Cli.Core/Hua.Cli.Core.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + + + + + + + + + + + + + + + + + + diff --git a/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgs.cs b/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgs.cs new file mode 100644 index 0000000..e106515 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgs.cs @@ -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(""); + } + + return sb.ToString(); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgumentParser.cs b/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgumentParser.cs new file mode 100644 index 0000000..10abad9 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Args/CommandLineArgumentParser.cs @@ -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(); + 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(); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Args/HuaCommandLineOptions.cs b/src/Hua.Cli.Core/Hua/Cli/Args/HuaCommandLineOptions.cs new file mode 100644 index 0000000..45e2af5 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Args/HuaCommandLineOptions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Cli.Args; + +public class HuaCommandLineOptions : Dictionary +{ + 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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Args/ICommandLineArgumentParser.cs b/src/Hua.Cli.Core/Hua/Cli/Args/ICommandLineArgumentParser.cs new file mode 100644 index 0000000..7c27f10 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Args/ICommandLineArgumentParser.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Cli.Args; + +public interface ICommandLineArgumentParser +{ + CommandLineArgs Parse(string[] args); + + CommandLineArgs Parse(string lineText); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/CliConsts.cs b/src/Hua.Cli.Core/Hua/Cli/CliConsts.cs new file mode 100644 index 0000000..30301e8 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/CliConsts.cs @@ -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"; +} diff --git a/src/Hua.Cli.Core/Hua/Cli/CliPaths.cs b/src/Hua.Cli.Core/Hua/Cli/CliPaths.cs new file mode 100644 index 0000000..1a2e4ce --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/CliPaths.cs @@ -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"); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/CliService.cs b/src/Hua.Cli.Core/Hua/Cli/CliService.cs new file mode 100644 index 0000000..ce3b9f1 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/CliService.cs @@ -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 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.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); + } + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/CliUrls.cs b/src/Hua.Cli.Core/Hua/Cli/CliUrls.cs new file mode 100644 index 0000000..98b8ed7 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/CliUrls.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.Cli; + +public static class CliUrls +{ + public const string HuaApiBaseUrl = "https://api.hua.com"; +} diff --git a/src/Hua.Cli.Core/Hua/Cli/CliUsageException.cs b/src/Hua.Cli.Core/Hua/Cli/CliUsageException.cs new file mode 100644 index 0000000..8ad0a7d --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/CliUsageException.cs @@ -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) + { + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/CommandSelector.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/CommandSelector.cs new file mode 100644 index 0000000..7f60374 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/CommandSelector.cs @@ -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 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); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/HelpCommand.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/HelpCommand.cs new file mode 100644 index 0000000..fb4a5a2 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/HelpCommand.cs @@ -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 Logger { get; set; } + protected HuaCliOptions HuaCliOptions { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } + + public HelpCommand(IOptions cliOptions, + IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + Logger = NullLogger.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 [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 "); + + return sb.ToString(); + } + + public static string GetShortDescription() + { + return "Show command line help. Write ` hua help `"; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/ICommandSelector.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/ICommandSelector.cs new file mode 100644 index 0000000..817ed4b --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/ICommandSelector.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Cli.Args; + +namespace Volo.Abp.Cli.Commands; + +public interface ICommandSelector +{ + Type Select(CommandLineArgs commandLineArgs); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/IConsoleCommand.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/IConsoleCommand.cs new file mode 100644 index 0000000..9ddada3 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/IConsoleCommand.cs @@ -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(); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/NewCommand.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/NewCommand.cs new file mode 100644 index 0000000..d8a2551 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/NewCommand.cs @@ -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 Logger { get; set; } + protected TemplateProjectBuilder TemplateProjectBuilder { get; } + protected ITemplateInfoProvider TemplateInfoProvider { get; } + + public NewCommand( + ITemplateInfoProvider templateInfoProvider, + TemplateProjectBuilder templateProjectBuilder) + { + TemplateInfoProvider = templateInfoProvider; + TemplateProjectBuilder = templateProjectBuilder; + Logger = NullLogger.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 [options] + +Options: + -t|--template (default: app) + -v|--version Semantic version number + -o|--output Output directory + -db|--database-provider Database provider (ef) + -u|--ui-framework UI framework (mvc / none) + -cs|--connection-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"; + } + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Commands/UpdateCommand.cs b/src/Hua.Cli.Core/Hua/Cli/Commands/UpdateCommand.cs new file mode 100644 index 0000000..358855e --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Commands/UpdateCommand.cs @@ -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 Logger { get; set; } + protected NugetPackagesVersionUpdater NugetPackagesVersionUpdater { get; } + + public UpdateCommand(NugetPackagesVersionUpdater nugetPackagesVersionUpdater) + { + NugetPackagesVersionUpdater = nugetPackagesVersionUpdater; + Logger = NullLogger.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 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"; + } + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Configuration/ConfigReader.cs b/src/Hua.Cli.Core/Hua/Cli/Configuration/ConfigReader.cs new file mode 100644 index 0000000..ad209ac --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Configuration/ConfigReader.cs @@ -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 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(json) ?? new HuaCliConfig(); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Configuration/HuaCliConfig.cs b/src/Hua.Cli.Core/Hua/Cli/Configuration/HuaCliConfig.cs new file mode 100644 index 0000000..88f8d90 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Configuration/HuaCliConfig.cs @@ -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; } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Configuration/IConfigReader.cs b/src/Hua.Cli.Core/Hua/Cli/Configuration/IConfigReader.cs new file mode 100644 index 0000000..88c1929 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Configuration/IConfigReader.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.Configuration; + +public interface IConfigReader +{ + Task ReadAsync(string directory); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Http/CliHttpClientHandler.cs b/src/Hua.Cli.Core/Hua/Cli/Http/CliHttpClientHandler.cs new file mode 100644 index 0000000..980248a --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Http/CliHttpClientHandler.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/HuaCliCoreModule.cs b/src/Hua.Cli.Core/Hua/Cli/HuaCliCoreModule.cs new file mode 100644 index 0000000..31b953d --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/HuaCliCoreModule.cs @@ -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(options => + { + options.Commands[HelpCommand.Name] = typeof(HelpCommand); + options.Commands[NewCommand.Name] = typeof(NewCommand); + options.Commands[UpdateCommand.Name] = typeof(UpdateCommand); + }); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/HuaCliOptions.cs b/src/Hua.Cli.Core/Hua/Cli/HuaCliOptions.cs new file mode 100644 index 0000000..0814716 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/HuaCliOptions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Cli; + +public class HuaCliOptions +{ + public Dictionary Commands { get; } + + public bool CacheTemplates { get; set; } = true; + + public string ToolName { get; set; } = "CLI"; + + public HuaCliOptions() + { + Commands = new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/FileEntry.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/FileEntry.cs new file mode 100644 index 0000000..7e1b9d6 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/FileEntry.cs @@ -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; } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildContext.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildContext.cs new file mode 100644 index 0000000..58f22f8 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildContext.cs @@ -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 Files { get; } = new List(); + + 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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipeline.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipeline.cs new file mode 100644 index 0000000..67aed51 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipeline.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.ProjectBuilding.Building; + +public class ProjectBuildPipeline +{ + private readonly List _steps = new List(); + + public void AddStep(ProjectBuildPipelineStep step) + { + _steps.Add(step); + } + + public async Task ExecuteAsync() + { + foreach (var step in _steps) + { + await step.ExecuteAsync(null!); + } + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipelineStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipelineStep.cs new file mode 100644 index 0000000..4245bcd --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/ProjectBuildPipelineStep.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.ProjectBuilding.Building; + +public abstract class ProjectBuildPipelineStep +{ + public abstract Task ExecuteAsync(ProjectBuildContext context); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/CreateProjectResultStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/CreateProjectResultStep.cs new file mode 100644 index 0000000..33930bb --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/CreateProjectResultStep.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/FileEntryListReadStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/FileEntryListReadStep.cs new file mode 100644 index 0000000..080a3ec --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/FileEntryListReadStep.cs @@ -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(); + } + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs new file mode 100644 index 0000000..493bf4d --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/ProjectReferenceReplaceStep.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/SolutionRenameStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/SolutionRenameStep.cs new file mode 100644 index 0000000..738a124 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/SolutionRenameStep.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs new file mode 100644 index 0000000..290d2df --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs new file mode 100644 index 0000000..c27fd55 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/BuiltInSourceCodeStore.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/BuiltInSourceCodeStore.cs new file mode 100644 index 0000000..9c264ad --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/BuiltInSourceCodeStore.cs @@ -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 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}."); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ISourceCodeStore.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ISourceCodeStore.cs new file mode 100644 index 0000000..2d4c104 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ISourceCodeStore.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.ProjectBuilding; + +public interface ISourceCodeStore +{ + Task GetAsync(string name, string? version = null); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ITemplateInfoProvider.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ITemplateInfoProvider.cs new file mode 100644 index 0000000..e46b1a5 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ITemplateInfoProvider.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Cli.ProjectBuilding; + +public interface ITemplateInfoProvider +{ + Task GetAsync(string name); + + Task GetDefaultAsync(); +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ProjectBuildArgs.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ProjectBuildArgs.cs new file mode 100644 index 0000000..aac4e2c --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/ProjectBuildArgs.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfo.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfo.cs new file mode 100644 index 0000000..cb79a2d --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfo.cs @@ -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; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfoProvider.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfoProvider.cs new file mode 100644 index 0000000..fece073 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateInfoProvider.cs @@ -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 GetAsync(string name) + { + TemplateInfo? template = name switch + { + "app" => new AppTemplate(), + _ => null + }; + + return Task.FromResult(template); + } + + public Task GetDefaultAsync() + { + return Task.FromResult(new AppTemplate()); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateProjectBuilder.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateProjectBuilder.cs new file mode 100644 index 0000000..d5b776d --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/TemplateProjectBuilder.cs @@ -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(); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Templates/App/AppTemplate.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Templates/App/AppTemplate.cs new file mode 100644 index 0000000..7766f6e --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectBuilding/Templates/App/AppTemplate.cs @@ -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) + { + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/ProjectModification/NugetPackagesVersionUpdater.cs b/src/Hua.Cli.Core/Hua/Cli/ProjectModification/NugetPackagesVersionUpdater.cs new file mode 100644 index 0000000..4945e03 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/ProjectModification/NugetPackagesVersionUpdater.cs @@ -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("= 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); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Utils/CmdHelper.cs b/src/Hua.Cli.Core/Hua/Cli/Utils/CmdHelper.cs new file mode 100644 index 0000000..48d84f9 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Utils/CmdHelper.cs @@ -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 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 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 + }; + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Utils/ConsoleHelper.cs b/src/Hua.Cli.Core/Hua/Cli/Utils/ConsoleHelper.cs new file mode 100644 index 0000000..467e21b --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Utils/ConsoleHelper.cs @@ -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(); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Utils/PathHelper.cs b/src/Hua.Cli.Core/Hua/Cli/Utils/PathHelper.cs new file mode 100644 index 0000000..f26fcd7 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Utils/PathHelper.cs @@ -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); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Version/CliVersionService.cs b/src/Hua.Cli.Core/Hua/Cli/Version/CliVersionService.cs new file mode 100644 index 0000000..a13ed9e --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Version/CliVersionService.cs @@ -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 GetCurrentCliVersionAsync() + { + var assembly = typeof(CliVersionService).Assembly; + var version = assembly.GetCustomAttribute()? + .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)); + } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Version/LatestVersionInfo.cs b/src/Hua.Cli.Core/Hua/Cli/Version/LatestVersionInfo.cs new file mode 100644 index 0000000..521ecf8 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Version/LatestVersionInfo.cs @@ -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; } +} diff --git a/src/Hua.Cli.Core/Hua/Cli/Version/PackageVersionCheckerService.cs b/src/Hua.Cli.Core/Hua/Cli/Version/PackageVersionCheckerService.cs new file mode 100644 index 0000000..2988134 --- /dev/null +++ b/src/Hua.Cli.Core/Hua/Cli/Version/PackageVersionCheckerService.cs @@ -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 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(); + + 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; + } + } +} diff --git a/src/Hua.Cli/Hua.Cli.csproj b/src/Hua.Cli/Hua.Cli.csproj new file mode 100644 index 0000000..f6e1b55 --- /dev/null +++ b/src/Hua.Cli/Hua.Cli.csproj @@ -0,0 +1,26 @@ + + + + Exe + enable + net9.0 + true + hua + + + + + + + + + + + + + + + + + + diff --git a/src/Hua.Cli/Hua/Cli/HuaCliModule.cs b/src/Hua.Cli/Hua/Cli/HuaCliModule.cs new file mode 100644 index 0000000..609b32c --- /dev/null +++ b/src/Hua.Cli/Hua/Cli/HuaCliModule.cs @@ -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 +{ +} diff --git a/src/Hua.Cli/Program.cs b/src/Hua.Cli/Program.cs new file mode 100644 index 0000000..c61a79b --- /dev/null +++ b/src/Hua.Cli/Program.cs @@ -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(options => + { + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + }); + + application.Initialize(); + + await application.ServiceProvider + .GetRequiredService() + .RunAsync(args); + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly."); + } + finally + { + Log.CloseAndFlush(); + } + } +} diff --git a/test/Hua.Cli.Tests/Hua.Cli.Tests.csproj b/test/Hua.Cli.Tests/Hua.Cli.Tests.csproj new file mode 100644 index 0000000..9f63164 --- /dev/null +++ b/test/Hua.Cli.Tests/Hua.Cli.Tests.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + false + + + + + + + + + + + + + + + + + diff --git a/test/Hua.Cli.Tests/Hua/Cli/Args/CommandLineArgumentParserTests.cs b/test/Hua.Cli.Tests/Hua/Cli/Args/CommandLineArgumentParserTests.cs new file mode 100644 index 0000000..5619a43 --- /dev/null +++ b/test/Hua.Cli.Tests/Hua/Cli/Args/CommandLineArgumentParserTests.cs @@ -0,0 +1,55 @@ +using Shouldly; +using Volo.Abp.Cli.Args; +using Xunit; + +namespace Hua.Cli.Args; + +public class CommandLineArgumentParserTests +{ + private readonly CommandLineArgumentParser _parser; + + public CommandLineArgumentParserTests() + { + _parser = new CommandLineArgumentParser(); + } + + [Fact] + public void Parse_EmptyArgs_ReturnsEmpty() + { + var result = _parser.Parse(new string[0]); + result.Command.ShouldBeNull(); + result.Target.ShouldBeNull(); + } + + [Fact] + public void Parse_OnlyCommand_ReturnsCommand() + { + var result = _parser.Parse(new[] { "new" }); + result.Command.ShouldBe("new"); + result.Target.ShouldBeNull(); + } + + [Fact] + public void Parse_CommandAndTarget_ReturnsBoth() + { + var result = _parser.Parse(new[] { "new", "MyProject" }); + result.Command.ShouldBe("new"); + result.Target.ShouldBe("MyProject"); + } + + [Fact] + public void Parse_CommandAndShortOption_ReturnsOptions() + { + var result = _parser.Parse(new[] { "new", "MyProject", "-t", "app" }); + result.Command.ShouldBe("new"); + result.Target.ShouldBe("MyProject"); + result.Options.GetOrNull("t").ShouldBe("app"); + } + + [Fact] + public void Parse_CommandAndLongOption_ReturnsOptions() + { + var result = _parser.Parse(new[] { "new", "MyProject", "--template", "app" }); + result.Options.GetOrNull("template").ShouldBe("app"); + } +} diff --git a/test/Hua.Cli.Tests/Hua/Cli/Commands/CommandSelectorTests.cs b/test/Hua.Cli.Tests/Hua/Cli/Commands/CommandSelectorTests.cs new file mode 100644 index 0000000..85910ac --- /dev/null +++ b/test/Hua.Cli.Tests/Hua/Cli/Commands/CommandSelectorTests.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.Cli; +using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.Commands; +using Xunit; + +namespace Hua.Cli.Commands; + +public class CommandSelectorTests +{ + [Fact] + public void Select_NullCommand_ReturnsHelpCommand() + { + var options = CreateOptions(); + var selector = new CommandSelector(Options.Create(options)); + var result = selector.Select(new CommandLineArgs()); + result.ShouldBe(typeof(HelpCommand)); + } + + [Fact] + public void Select_NewCommand_ReturnsNewCommand() + { + var options = CreateOptions(); + options.Commands["new"] = typeof(NewCommand); + var selector = new CommandSelector(Options.Create(options)); + var result = selector.Select(new CommandLineArgs("new")); + result.ShouldBe(typeof(NewCommand)); + } + + [Fact] + public void Select_UnknownCommand_ReturnsHelpCommand() + { + var options = CreateOptions(); + var selector = new CommandSelector(Options.Create(options)); + var result = selector.Select(new CommandLineArgs("unknown")); + result.ShouldBe(typeof(HelpCommand)); + } + + private HuaCliOptions CreateOptions() + { + var options = new HuaCliOptions(); + options.Commands["help"] = typeof(HelpCommand); + return options; + } +}