From 265249ee500a0f176550569a01fc69e6f54fabf2 Mon Sep 17 00:00:00 2001 From: sinclair-aefinder <156668112+sinclair-aefinder@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:10:49 +0800 Subject: [PATCH] Feature: AeFinder Cli tool (#2) * feat: add cli * feat: add ci * feat: check project name * feat: remove unused file * fix: naming * fix: change key and .sln file name in sonarqube.yaml * feat: add GitHub Action for NuGet publish * feat: improve code * feat: use ImmutableDictionary --------- Co-authored-by: jacob-finder --- .github/ISSUE_TEMPLATE/bug_report.md | 33 +++ .github/ISSUE_TEMPLATE/config.yml | 10 + .github/ISSUE_TEMPLATE/feature_request.md | 27 ++ .github/workflows/nuget-publish.yml | 40 +++ .github/workflows/sonarqube.yaml | 46 ++++ .gitignore | 260 ++++++++++++++++++ AeFinder.Cli.sln | 28 ++ .../AeFinder.Cli.Core.csproj | 17 ++ .../AeFinderCliCoreModule.cs | 20 ++ src/AeFinder.Cli.Core/AeFinderEndpoint.cs | 7 + src/AeFinder.Cli.Core/AeFinderNetwork.cs | 7 + ...inderIdentityModelAuthenticationService.cs | 89 ++++++ src/AeFinder.Cli.Core/Auth/AuthService.cs | 30 ++ src/AeFinder.Cli.Core/Auth/IAuthService.cs | 6 + src/AeFinder.Cli.Core/CliConsts.cs | 24 ++ src/AeFinder.Cli.Core/CliService.cs | 58 ++++ .../Http/CliHttpClientFactory.cs | 30 ++ .../Http/IRemoteServiceExceptionHandler.cs | 8 + .../Http/RemoteServiceExceptionHandler.cs | 97 +++++++ .../Options/CommonOptions.cs | 15 + .../Options/DeployAppOptions.cs | 13 + .../Options/InitAppOptions.cs | 13 + .../Options/UpdateAppOptions.cs | 16 ++ src/AeFinder.Cli.Core/Services/AppService.cs | 112 ++++++++ .../Services/DevelopmentTemplateAppService.cs | 90 ++++++ src/AeFinder.Cli.Core/Services/IAppService.cs | 9 + .../IDevelopmentTemplateAppService.cs | 8 + src/AeFinder.Cli.Core/Utils/ZipHelper.cs | 19 ++ src/AeFinder.Cli/AeFinder.Cli.csproj | 23 ++ src/AeFinder.Cli/AeFinderCliModule.cs | 13 + src/AeFinder.Cli/Program.cs | 46 ++++ 31 files changed, 1214 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/nuget-publish.yml create mode 100644 .github/workflows/sonarqube.yaml create mode 100644 .gitignore create mode 100644 AeFinder.Cli.sln create mode 100644 src/AeFinder.Cli.Core/AeFinder.Cli.Core.csproj create mode 100644 src/AeFinder.Cli.Core/AeFinderCliCoreModule.cs create mode 100644 src/AeFinder.Cli.Core/AeFinderEndpoint.cs create mode 100644 src/AeFinder.Cli.Core/AeFinderNetwork.cs create mode 100644 src/AeFinder.Cli.Core/Auth/AeFinderIdentityModelAuthenticationService.cs create mode 100644 src/AeFinder.Cli.Core/Auth/AuthService.cs create mode 100644 src/AeFinder.Cli.Core/Auth/IAuthService.cs create mode 100644 src/AeFinder.Cli.Core/CliConsts.cs create mode 100644 src/AeFinder.Cli.Core/CliService.cs create mode 100644 src/AeFinder.Cli.Core/Http/CliHttpClientFactory.cs create mode 100644 src/AeFinder.Cli.Core/Http/IRemoteServiceExceptionHandler.cs create mode 100644 src/AeFinder.Cli.Core/Http/RemoteServiceExceptionHandler.cs create mode 100644 src/AeFinder.Cli.Core/Options/CommonOptions.cs create mode 100644 src/AeFinder.Cli.Core/Options/DeployAppOptions.cs create mode 100644 src/AeFinder.Cli.Core/Options/InitAppOptions.cs create mode 100644 src/AeFinder.Cli.Core/Options/UpdateAppOptions.cs create mode 100644 src/AeFinder.Cli.Core/Services/AppService.cs create mode 100644 src/AeFinder.Cli.Core/Services/DevelopmentTemplateAppService.cs create mode 100644 src/AeFinder.Cli.Core/Services/IAppService.cs create mode 100644 src/AeFinder.Cli.Core/Services/IDevelopmentTemplateAppService.cs create mode 100644 src/AeFinder.Cli.Core/Utils/ZipHelper.cs create mode 100644 src/AeFinder.Cli/AeFinder.Cli.csproj create mode 100644 src/AeFinder.Cli/AeFinderCliModule.cs create mode 100644 src/AeFinder.Cli/Program.cs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ea59817 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: 👾 Bug Report +about: Report a bug or issue with the project. +title: '' +labels: 'bug' +assignees: '' + +--- + +### Description +A clear and concise description of what the bug is. + +### Steps To Reproduce +1. Log in... +2. Ensure that... +3. Allow a long period of inactivity to pass... +4. Observe that... +5. Attempt to log in... + +### Current Behavior +- After the period of inactivity... +- When the user tries to log in using another method... +- This causes a bug due to... + +### Expected Behavior +- After a long period of inactivity... +- When a user logs in successfully... +- This ensures that only... + +### Environment +- Platform: PC +- Node: v18.18.0 +- Browser: Chrome 126.0.6478.56 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..163c1c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false +issue_template: + - name: 👾 Bug Report + description: Report a bug or issue with the project. + labels: ["bug"] + template: bug_report.md + - name: 💡 Feature Request + description: Create a new ticket for a new feature request. + labels: ["enhancement"] + template: feature_request.md \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a4b8175 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: 💡 Feature Request +about: Create a new ticket for a new feature request +title: '' +labels: 'enhancement' +assignees: '' + +--- + +### Expected Behavior +Describe the expected behavior here. + +### Specifications +As a `user`, I would like to `action` so that `reason`. + +**Features:** +- describe feature details here. + +**Development Tasks:** +- [ ] Task 1 +- [ ] Task 2 + +### Dependencies +List any dependencies that are required for this feature by providing links to the issues or repositories. + +### References +List any references that are related to this feature request. \ No newline at end of file diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 0000000..ed46252 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,40 @@ +name: Publish NuGet Package + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Get the version from git tags + id: get_version + run: | + TAG=$(git describe --tags --abbrev=0) + VERSION=${TAG#v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Build the project + run: dotnet build --configuration Release --no-restore + + - name: Pack the NuGet package + run: dotnet pack --configuration Release --no-build --output ./nupkg -p:PackageVersion=${{ env.VERSION }} + + - name: Publish the NuGet package + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push ./nupkg/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json diff --git a/.github/workflows/sonarqube.yaml b/.github/workflows/sonarqube.yaml new file mode 100644 index 0000000..f976704 --- /dev/null +++ b/.github/workflows/sonarqube.yaml @@ -0,0 +1,46 @@ +on: + pull_request: + types: [opened, synchronize, reopened] + +name: PR Static Code Analysis +jobs: + static-code-analysis: + runs-on: ubuntu-latest + steps: + - name: Code Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '7.0' + - name: Create temporary global.json + run: echo '{"sdk":{"version":"7.0.x"}}' > ./global.json + - name: Install protobuf + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Cache SonarQube packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache SonarQube scanner + id: cache-sonar-scanner + uses: actions/cache@v1 + with: + path: ./.sonar/scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + - name: Install SonarScanner for .NET + run: dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + - name: Add .NET global tools to PATH + run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + - name: Begin SonarQube analysis + run: | + ./.sonar/scanner/dotnet-sonarscanner begin /k:"aefinder-cli" /d:sonar.host.url="${{ secrets.SONAR_HOST_URL }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + dotnet build AeFinder.Cli.sln + ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29fbe68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,260 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +.dotnet + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +#Codecov +CodeCoverage + +src/AeFinder.Cli/Logs/* +*.dll \ No newline at end of file diff --git a/AeFinder.Cli.sln b/AeFinder.Cli.sln new file mode 100644 index 0000000..a5161b2 --- /dev/null +++ b/AeFinder.Cli.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{951256B1-01BA-4BB0-B066-0F58F2B7D50C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AeFinder.Cli", "src\AeFinder.Cli\AeFinder.Cli.csproj", "{2B2A3975-667E-40B5-9819-E375559D9626}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AeFinder.Cli.Core", "src\AeFinder.Cli.Core\AeFinder.Cli.Core.csproj", "{EA636516-5627-4DC7-9CF6-785B48C346D3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2B2A3975-667E-40B5-9819-E375559D9626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B2A3975-667E-40B5-9819-E375559D9626}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B2A3975-667E-40B5-9819-E375559D9626}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B2A3975-667E-40B5-9819-E375559D9626}.Release|Any CPU.Build.0 = Release|Any CPU + {EA636516-5627-4DC7-9CF6-785B48C346D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA636516-5627-4DC7-9CF6-785B48C346D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA636516-5627-4DC7-9CF6-785B48C346D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA636516-5627-4DC7-9CF6-785B48C346D3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2B2A3975-667E-40B5-9819-E375559D9626} = {951256B1-01BA-4BB0-B066-0F58F2B7D50C} + {EA636516-5627-4DC7-9CF6-785B48C346D3} = {951256B1-01BA-4BB0-B066-0F58F2B7D50C} + EndGlobalSection +EndGlobal diff --git a/src/AeFinder.Cli.Core/AeFinder.Cli.Core.csproj b/src/AeFinder.Cli.Core/AeFinder.Cli.Core.csproj new file mode 100644 index 0000000..9add1e2 --- /dev/null +++ b/src/AeFinder.Cli.Core/AeFinder.Cli.Core.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + AeFinder.Cli + + + + + + + + + + + diff --git a/src/AeFinder.Cli.Core/AeFinderCliCoreModule.cs b/src/AeFinder.Cli.Core/AeFinderCliCoreModule.cs new file mode 100644 index 0000000..76206b6 --- /dev/null +++ b/src/AeFinder.Cli.Core/AeFinderCliCoreModule.cs @@ -0,0 +1,20 @@ +using Volo.Abp.Domain; +using Volo.Abp.Http; +using Volo.Abp.IdentityModel; +using Volo.Abp.Json; +using Volo.Abp.Minify; +using Volo.Abp.Modularity; + +namespace AeFinder.Cli; + +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpJsonModule), + typeof(AbpIdentityModelModule), + typeof(AbpMinifyModule), + typeof(AbpHttpModule) +)] +public class AeFinderCliCoreModule: AbpModule +{ + +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/AeFinderEndpoint.cs b/src/AeFinder.Cli.Core/AeFinderEndpoint.cs new file mode 100644 index 0000000..4b6c6a1 --- /dev/null +++ b/src/AeFinder.Cli.Core/AeFinderEndpoint.cs @@ -0,0 +1,7 @@ +namespace AeFinder.Cli; + +public class AeFinderEndpoint +{ + public string AuthEndpoint { get; set; } + public string ApiEndpoint { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/AeFinderNetwork.cs b/src/AeFinder.Cli.Core/AeFinderNetwork.cs new file mode 100644 index 0000000..022ed0b --- /dev/null +++ b/src/AeFinder.Cli.Core/AeFinderNetwork.cs @@ -0,0 +1,7 @@ +namespace AeFinder.Cli; + +public enum AeFinderNetwork +{ + MainNet, + TestNet +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Auth/AeFinderIdentityModelAuthenticationService.cs b/src/AeFinder.Cli.Core/Auth/AeFinderIdentityModelAuthenticationService.cs new file mode 100644 index 0000000..1c6d823 --- /dev/null +++ b/src/AeFinder.Cli.Core/Auth/AeFinderIdentityModelAuthenticationService.cs @@ -0,0 +1,89 @@ +using IdentityModel; +using IdentityModel.Client; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IdentityModel; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; + +namespace AeFinder.Cli.Auth; + +[Dependency(ReplaceServices = true)] +public class AeFinderIdentityModelAuthenticationService : IdentityModelAuthenticationService +{ + public AeFinderIdentityModelAuthenticationService(IOptions options, + ICancellationTokenProvider cancellationTokenProvider, IHttpClientFactory httpClientFactory, + ICurrentTenant currentTenant, + IOptions identityModelHttpRequestMessageOptions, + IDistributedCache tokenCache, + IDistributedCache discoveryDocumentCache, + IAbpHostEnvironment abpHostEnvironment) : base(options, cancellationTokenProvider, httpClientFactory, + currentTenant, identityModelHttpRequestMessageOptions, tokenCache, discoveryDocumentCache, abpHostEnvironment) + { + } + + protected override async Task GetTokenResponse(IdentityClientConfiguration configuration) + { + using var httpClient = HttpClientFactory.CreateClient(HttpClientName); + AddHeaders(httpClient); + + switch (configuration.GrantType) + { + case OidcConstants.GrantTypes.ClientCredentials: + return await RequestClientCredentialsTokenAsync(httpClient, + await CreateClientCredentialsTokenRequestAsync(configuration), + CancellationTokenProvider.Token + ); + case OidcConstants.GrantTypes.Password: + return await httpClient.RequestPasswordTokenAsync( + await CreatePasswordTokenRequestAsync(configuration), + CancellationTokenProvider.Token + ); + + case OidcConstants.GrantTypes.DeviceCode: + return await RequestDeviceAuthorizationAsync(httpClient, configuration); + + default: + throw new AbpException("Grant type was not implemented: " + configuration.GrantType); + } + } + + private static async Task RequestClientCredentialsTokenAsync(HttpMessageInvoker client, + ClientCredentialsTokenRequest request, CancellationToken cancellationToken = default) + { + var clone = request.Clone(); + + clone.Parameters.AddRequired(OidcConstants.TokenRequest.GrantType, OidcConstants.GrantTypes.ClientCredentials); + clone.Parameters.AddRequired(OidcConstants.TokenRequest.ClientId, request.ClientId); + clone.Parameters.AddRequired(OidcConstants.TokenRequest.ClientSecret, request.ClientSecret); + clone.Parameters.AddOptional(OidcConstants.TokenRequest.Scope, request.Scope); + + foreach (var resource in request.Resource) + { + clone.Parameters.AddRequired(OidcConstants.TokenRequest.Resource, resource, allowDuplicates: true); + } + + return await RequestTokenAsync(client, clone, cancellationToken); + } + + private static async Task RequestTokenAsync(HttpMessageInvoker client, ProtocolRequest request, + CancellationToken cancellationToken = default) + { + request.Prepare(); + request.Method = HttpMethod.Post; + + HttpResponseMessage response; + try + { + response = await client.SendAsync(request, cancellationToken); + } + catch (Exception ex) + { + return ProtocolResponse.FromException(ex); + } + + return await ProtocolResponse.FromHttpResponseAsync(response); + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Auth/AuthService.cs b/src/AeFinder.Cli.Core/Auth/AuthService.cs new file mode 100644 index 0000000..ba9e71f --- /dev/null +++ b/src/AeFinder.Cli.Core/Auth/AuthService.cs @@ -0,0 +1,30 @@ +using IdentityModel; +using Volo.Abp.DependencyInjection; +using Volo.Abp.IdentityModel; + +namespace AeFinder.Cli.Auth; + +public class AuthService : IAuthService, ITransientDependency +{ + private readonly IIdentityModelAuthenticationService _authenticationService; + + public AuthService(IIdentityModelAuthenticationService authenticationService) + { + _authenticationService = authenticationService; + } + + public async Task GetAccessTokenAsync(AeFinderNetwork network, string clientId, string clientSecret) + { + var configuration = new IdentityClientConfiguration( + CliConsts.AeFinderEndpoints[network].AuthEndpoint, + "AeFinder", + clientId, + clientSecret, + OidcConstants.GrantTypes.ClientCredentials, + requireHttps: false, + validateEndpoints: false + ); + + return await _authenticationService.GetAccessTokenAsync(configuration); + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Auth/IAuthService.cs b/src/AeFinder.Cli.Core/Auth/IAuthService.cs new file mode 100644 index 0000000..6a249aa --- /dev/null +++ b/src/AeFinder.Cli.Core/Auth/IAuthService.cs @@ -0,0 +1,6 @@ +namespace AeFinder.Cli.Auth; + +public interface IAuthService +{ + Task GetAccessTokenAsync(AeFinderNetwork network, string clientId, string clientSecret); +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/CliConsts.cs b/src/AeFinder.Cli.Core/CliConsts.cs new file mode 100644 index 0000000..fc13847 --- /dev/null +++ b/src/AeFinder.Cli.Core/CliConsts.cs @@ -0,0 +1,24 @@ +using System.Collections.Immutable; + +namespace AeFinder.Cli; + +public static class CliConsts +{ + public static readonly ImmutableDictionary AeFinderEndpoints = + ImmutableDictionary.CreateRange( + new[] + { + KeyValuePair.Create(AeFinderNetwork.MainNet, new AeFinderEndpoint + { + AuthEndpoint = "https://indexer-auth.aefinder.io/", + ApiEndpoint = "https://indexer-api.aefinder.io/" + }), + KeyValuePair.Create(AeFinderNetwork.TestNet, new AeFinderEndpoint + { + AuthEndpoint = "https://gcptest-indexer-auth.aefinder.io/", + ApiEndpoint = "https://gcptest-indexer-api.aefinder.io/" + }) + }); + + public const string HttpClientName = "AeFinderHttpClient"; +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/CliService.cs b/src/AeFinder.Cli.Core/CliService.cs new file mode 100644 index 0000000..1733d1d --- /dev/null +++ b/src/AeFinder.Cli.Core/CliService.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using AeFinder.Cli.Options; +using AeFinder.Cli.Services; +using CommandLine; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace AeFinder.Cli; + +public class CliService : ITransientDependency +{ + private readonly IDevelopmentTemplateAppService _developmentTemplateAppService; + private readonly IAppService _appService; + private readonly ILogger _logger; + + public CliService( + ILogger logger, IDevelopmentTemplateAppService developmentTemplateAppService, + IAppService appService) + { + _logger = logger; + _developmentTemplateAppService = developmentTemplateAppService; + _appService = appService; + } + + public async Task RunAsync(string[] args) + { + var types = LoadVerbs(); + + await Parser.Default.ParseArguments(args,types) + .WithParsedAsync(RunAsync); + } + + private static Type[] LoadVerbs() + { + return Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null).ToArray(); + } + + private async Task RunAsync(object obj) + { + var commonOptions = obj as CommonOptions; + _logger.LogInformation("Network : {Network}", commonOptions.Network); + _logger.LogInformation("AppId : {AppId}", commonOptions.AppId); + + switch (obj) + { + case InitAppOptions options: + await _developmentTemplateAppService.CreateProjectAsync(options); + break; + case DeployAppOptions options: + await _appService.DeployAsync(options); + break; + case UpdateAppOptions options: + await _appService.UpdateAsync(options); + break; + } + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Http/CliHttpClientFactory.cs b/src/AeFinder.Cli.Core/Http/CliHttpClientFactory.cs new file mode 100644 index 0000000..1098ada --- /dev/null +++ b/src/AeFinder.Cli.Core/Http/CliHttpClientFactory.cs @@ -0,0 +1,30 @@ +using IdentityModel.Client; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace AeFinder.Cli.Http; + +public class CliHttpClientFactory : ISingletonDependency +{ + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(2); + + private readonly IHttpClientFactory _clientFactory; + + public CliHttpClientFactory(IHttpClientFactory clientFactory) + { + _clientFactory = clientFactory; + } + + public HttpClient CreateClient(string accessToken = null, TimeSpan? timeout = null) + { + var httpClient = _clientFactory.CreateClient(CliConsts.HttpClientName); + httpClient.Timeout = timeout ?? DefaultTimeout; + + if (!accessToken.IsNullOrEmpty()) + { + httpClient.SetBearerToken(accessToken); + } + + return httpClient; + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Http/IRemoteServiceExceptionHandler.cs b/src/AeFinder.Cli.Core/Http/IRemoteServiceExceptionHandler.cs new file mode 100644 index 0000000..fb3b52a --- /dev/null +++ b/src/AeFinder.Cli.Core/Http/IRemoteServiceExceptionHandler.cs @@ -0,0 +1,8 @@ +namespace AeFinder.Cli; + +public interface IRemoteServiceExceptionHandler +{ + Task EnsureSuccessfulHttpResponseAsync(HttpResponseMessage responseMessage); + + Task GetAbpRemoteServiceErrorAsync(HttpResponseMessage responseMessage); +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Http/RemoteServiceExceptionHandler.cs b/src/AeFinder.Cli.Core/Http/RemoteServiceExceptionHandler.cs new file mode 100644 index 0000000..e126c11 --- /dev/null +++ b/src/AeFinder.Cli.Core/Http/RemoteServiceExceptionHandler.cs @@ -0,0 +1,97 @@ +using System.Text; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; +using Volo.Abp.Json; +using System.Text.Json; +using Volo.Abp; + +namespace AeFinder.Cli; + +public class RemoteServiceExceptionHandler : IRemoteServiceExceptionHandler, ITransientDependency +{ + private readonly IJsonSerializer _jsonSerializer; + + public RemoteServiceExceptionHandler(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + public async Task EnsureSuccessfulHttpResponseAsync(HttpResponseMessage responseMessage) + { + if (responseMessage == null) + { + return; + } + + if (responseMessage.IsSuccessStatusCode) + { + return; + } + + var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + + responseMessage.ReasonPhrase + "'. "; + + var remoteServiceErrorMessage = await GetAbpRemoteServiceErrorAsync(responseMessage); + if (remoteServiceErrorMessage != null) + { + exceptionMessage += remoteServiceErrorMessage; + } + + throw new UserFriendlyException(exceptionMessage); + } + + public async Task GetAbpRemoteServiceErrorAsync(HttpResponseMessage responseMessage) + { + RemoteServiceErrorResponse errorResult; + try + { + errorResult = _jsonSerializer.Deserialize + ( + await responseMessage.Content.ReadAsStringAsync() + ); + } + catch (JsonException) + { + return null; + } + + if (errorResult?.Error == null) + { + return null; + } + + var sbError = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(errorResult.Error.Code)) + { + sbError.Append("Code: " + errorResult.Error.Code); + } + + if (!string.IsNullOrWhiteSpace(errorResult.Error.Message)) + { + if (sbError.Length > 0) + { + sbError.Append(" - "); + } + + sbError.Append("Message: " + errorResult.Error.Message); + } + + if (errorResult.Error.ValidationErrors != null && errorResult.Error.ValidationErrors.Length > 0) + { + if (sbError.Length > 0) + { + sbError.Append(" - "); + } + + sbError.AppendLine("Validation Errors: "); + for (var i = 0; i < errorResult.Error.ValidationErrors.Length; i++) + { + var validationError = errorResult.Error.ValidationErrors[i]; + sbError.AppendLine("Validation error #" + i + ": " + validationError.Message + " - Members: " + + validationError.Members.JoinAsString(", ") + "."); + } + } + + return sbError.ToString(); + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Options/CommonOptions.cs b/src/AeFinder.Cli.Core/Options/CommonOptions.cs new file mode 100644 index 0000000..047ce03 --- /dev/null +++ b/src/AeFinder.Cli.Core/Options/CommonOptions.cs @@ -0,0 +1,15 @@ +using CommandLine; + +namespace AeFinder.Cli.Options; + +public class CommonOptions +{ + [Option(longName: "appid", Required = true, HelpText = "The appid of the AeFinder App.")] + public string AppId { get; set; } + + [Option(longName: "key", Required = true, HelpText = "The deploy key of the AeFinder App.")] + public string Key { get; set; } + + [Option(longName: "network", Required = false, Default = AeFinderNetwork.MainNet, HelpText = "The AeFinder network (MainNet or TestNet).")] + public AeFinderNetwork Network { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Options/DeployAppOptions.cs b/src/AeFinder.Cli.Core/Options/DeployAppOptions.cs new file mode 100644 index 0000000..c1d9ce5 --- /dev/null +++ b/src/AeFinder.Cli.Core/Options/DeployAppOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace AeFinder.Cli.Options; + +[Verb("deploy", HelpText = "Deploy AeFinder App.")] +public class DeployAppOptions : CommonOptions +{ + [Option(longName: "code", Required = true, HelpText = "The code file path of your AeFinder App.")] + public string Code { get; set; } + + [Option(longName: "manifest", Required = true, HelpText = "The manifest file path of your AeFinder App.")] + public string Manifest { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Options/InitAppOptions.cs b/src/AeFinder.Cli.Core/Options/InitAppOptions.cs new file mode 100644 index 0000000..a955788 --- /dev/null +++ b/src/AeFinder.Cli.Core/Options/InitAppOptions.cs @@ -0,0 +1,13 @@ +using CommandLine; + +namespace AeFinder.Cli.Options; + +[Verb("init", HelpText = "Init AeFinder App development project.")] +public class InitAppOptions : CommonOptions +{ + [Option(longName: "name", Required = true, HelpText = "The project name.")] + public string Name { get; set; } + + [Option(longName: "directory", Required = false, HelpText = "The project directory. (Default: Current directory)")] + public string Directory { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Options/UpdateAppOptions.cs b/src/AeFinder.Cli.Core/Options/UpdateAppOptions.cs new file mode 100644 index 0000000..abb7abe --- /dev/null +++ b/src/AeFinder.Cli.Core/Options/UpdateAppOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace AeFinder.Cli.Options; + +[Verb("update", HelpText = "Update AeFinder App.")] +public class UpdateAppOptions : CommonOptions +{ + [Option(longName: "code", Required = false, HelpText = "The code file path of your AeFinder App.")] + public string Code { get; set; } + + [Option(longName: "manifest", Required = false, HelpText = "The manifest file path of your AeFinder App.")] + public string Manifest { get; set; } + + [Option(longName: "version", Required = true, HelpText = "The version to update.")] + public string Version { get; set; } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Services/AppService.cs b/src/AeFinder.Cli.Core/Services/AppService.cs new file mode 100644 index 0000000..67ab87a --- /dev/null +++ b/src/AeFinder.Cli.Core/Services/AppService.cs @@ -0,0 +1,112 @@ +using System.Text; +using AeFinder.Cli.Auth; +using AeFinder.Cli.Http; +using AeFinder.Cli.Options; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; +using Volo.Abp.Threading; + +namespace AeFinder.Cli.Services; + +public class AppService : IAppService, ITransientDependency +{ + private readonly IAuthService _authService; + private readonly CliHttpClientFactory _cliHttpClientFactory; + private readonly ICancellationTokenProvider _cancellationTokenProvider; + private readonly IRemoteServiceExceptionHandler _remoteServiceExceptionHandler; + private readonly ILogger _logger; + + public AppService(IAuthService authService, + CliHttpClientFactory cliHttpClientFactory, ICancellationTokenProvider cancellationTokenProvider, + ILogger logger, IRemoteServiceExceptionHandler remoteServiceExceptionHandler) + { + _authService = authService; + _cliHttpClientFactory = cliHttpClientFactory; + _cancellationTokenProvider = cancellationTokenProvider; + _logger = logger; + _remoteServiceExceptionHandler = remoteServiceExceptionHandler; + } + + public async Task DeployAsync(DeployAppOptions options) + { + _logger.LogInformation("Deploying app..."); + + var token = await _authService.GetAccessTokenAsync(options.Network, options.AppId, options.Key); + var url = $"{CliConsts.AeFinderEndpoints[options.Network].ApiEndpoint}api/apps/subscriptions"; + var client = _cliHttpClientFactory.CreateClient(token); + + var formDataContent = new MultipartFormDataContent(); + formDataContent.Add(new StringContent(await File.ReadAllTextAsync(options.Manifest)), "Manifest"); + formDataContent.Add(new StreamContent(new MemoryStream(await File.ReadAllBytesAsync(options.Code))), "Code", "code.dll"); + + using var response = await client.PostAsync( + url, + formDataContent, + _cancellationTokenProvider.Token + ); + + await _remoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + var responseContent = await response.Content.ReadAsStringAsync(); + _logger.LogInformation("Deploy app successfully. Version: {Version}", responseContent); + } + + public async Task UpdateAsync(UpdateAppOptions options) + { + var token = await _authService.GetAccessTokenAsync(options.Network, options.AppId, options.Key); + + if (!options.Code.IsNullOrWhiteSpace()) + { + await UpdateCodeAsync(options, token); + } + + if (!options.Manifest.IsNullOrWhiteSpace()) + { + await UpdateManifestAsync(options, token); + } + } + + private async Task UpdateCodeAsync(UpdateAppOptions options, string token) + { + _logger.LogInformation("Updating app code..."); + + var url = + $"{CliConsts.AeFinderEndpoints[options.Network].ApiEndpoint}api/apps/subscriptions/code/{options.Version}"; + + var client = _cliHttpClientFactory.CreateClient(token); + + var formDataContent = new MultipartFormDataContent(); + formDataContent.Add(new StreamContent(new MemoryStream(await File.ReadAllBytesAsync(options.Code))), "Code", "code.dll"); + + using var response = await client.PutAsync( + url, + formDataContent, + _cancellationTokenProvider.Token + ); + + await _remoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + _logger.LogInformation("Update code successfully."); + } + + private async Task UpdateManifestAsync(UpdateAppOptions options, string token) + { + _logger.LogInformation("Updating app manifest..."); + + var url = + $"{CliConsts.AeFinderEndpoints[options.Network].ApiEndpoint}api/apps/subscriptions/manifest/{options.Version}"; + + var client = _cliHttpClientFactory.CreateClient(token); + + using var response = await client.PutAsync( + url, + new StringContent(await File.ReadAllTextAsync(options.Manifest), Encoding.UTF8, MimeTypes.Application.Json), + _cancellationTokenProvider.Token + ); + + await _remoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + _logger.LogInformation("Update manifest successfully."); + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Services/DevelopmentTemplateAppService.cs b/src/AeFinder.Cli.Core/Services/DevelopmentTemplateAppService.cs new file mode 100644 index 0000000..c696c41 --- /dev/null +++ b/src/AeFinder.Cli.Core/Services/DevelopmentTemplateAppService.cs @@ -0,0 +1,90 @@ +using System.Text; +using System.Text.RegularExpressions; +using AeFinder.Cli.Auth; +using AeFinder.Cli.Http; +using AeFinder.Cli.Options; +using AeFinder.Cli.Utils; +using Microsoft.Extensions.Logging; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; +using Volo.Abp.Json; +using Volo.Abp.Threading; + +namespace AeFinder.Cli.Services; + +public class DevelopmentTemplateAppService : IDevelopmentTemplateAppService, ITransientDependency +{ + private readonly IAuthService _authService; + private readonly IJsonSerializer _jsonSerializer; + private readonly CliHttpClientFactory _cliHttpClientFactory; + private readonly ICancellationTokenProvider _cancellationTokenProvider; + private readonly IRemoteServiceExceptionHandler _remoteServiceExceptionHandler; + private readonly ILogger _logger; + + public DevelopmentTemplateAppService(IAuthService authService, IJsonSerializer jsonSerializer, + CliHttpClientFactory cliHttpClientFactory, ICancellationTokenProvider cancellationTokenProvider, + ILogger logger, IRemoteServiceExceptionHandler remoteServiceExceptionHandler) + { + _authService = authService; + _jsonSerializer = jsonSerializer; + _cliHttpClientFactory = cliHttpClientFactory; + _cancellationTokenProvider = cancellationTokenProvider; + _logger = logger; + _remoteServiceExceptionHandler = remoteServiceExceptionHandler; + } + + public async Task CreateProjectAsync(InitAppOptions options) + { + _logger.LogInformation("Initialize app..."); + + CheckOptions(options); + + var token = await _authService.GetAccessTokenAsync(options.Network, options.AppId, options.Key); + + var postData = _jsonSerializer.Serialize(new + { + Name = options.Name + }); + var url = $"{CliConsts.AeFinderEndpoints[options.Network].ApiEndpoint}api/dev-template"; + + var client = _cliHttpClientFactory.CreateClient(token); + + using var response = await client.PostAsync( + url, + new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json), + _cancellationTokenProvider.Token + ); + + await _remoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response); + + if (options.Directory.IsNullOrWhiteSpace()) + { + options.Directory = Directory.GetCurrentDirectory(); + } + + var result = await response.Content.ReadAsStreamAsync(); + ZipHelper.UnZip(result, options.Directory); + + _logger.LogInformation("The AeFinder App: {App} is initialized successfully. Directory: {Directory}", options.Name, options.Directory); + } + + private static void CheckOptions(InitAppOptions options) + { + if (options.Name.Length is < 2 or > 20) + { + throw new UserFriendlyException("The name should be between 2 and 20 in length."); + } + + if (!ProjectNameRegex.IsValid(options.Name)) + { + throw new UserFriendlyException("The Name must begin with a letter and can only contain letters('A'-'Z', 'a'-'z'), numbers(0-9), and dots('.')."); + } + } +} + +public static partial class ProjectNameRegex { + [GeneratedRegex("^[A-Za-z][A-Za-z0-9.]+")] + public static partial Regex Regex(); + public static bool IsValid(string name) => Regex().IsMatch(name); +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Services/IAppService.cs b/src/AeFinder.Cli.Core/Services/IAppService.cs new file mode 100644 index 0000000..c61e491 --- /dev/null +++ b/src/AeFinder.Cli.Core/Services/IAppService.cs @@ -0,0 +1,9 @@ +using AeFinder.Cli.Options; + +namespace AeFinder.Cli.Services; + +public interface IAppService +{ + Task DeployAsync(DeployAppOptions options); + Task UpdateAsync(UpdateAppOptions options); +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Services/IDevelopmentTemplateAppService.cs b/src/AeFinder.Cli.Core/Services/IDevelopmentTemplateAppService.cs new file mode 100644 index 0000000..ee7ed99 --- /dev/null +++ b/src/AeFinder.Cli.Core/Services/IDevelopmentTemplateAppService.cs @@ -0,0 +1,8 @@ +using AeFinder.Cli.Options; + +namespace AeFinder.Cli.Services; + +public interface IDevelopmentTemplateAppService +{ + Task CreateProjectAsync(InitAppOptions options); +} \ No newline at end of file diff --git a/src/AeFinder.Cli.Core/Utils/ZipHelper.cs b/src/AeFinder.Cli.Core/Utils/ZipHelper.cs new file mode 100644 index 0000000..105d677 --- /dev/null +++ b/src/AeFinder.Cli.Core/Utils/ZipHelper.cs @@ -0,0 +1,19 @@ +using ICSharpCode.SharpZipLib.Zip; + +namespace AeFinder.Cli.Utils; + +public static class ZipHelper +{ + public static void ZipDirectory(string zipFileName, string sourceDirectory) + { + var zip = new FastZip(); + zip.CreateZip(zipFileName, sourceDirectory,true, string.Empty); + } + + public static void UnZip(Stream fileStream, string targetDirectory) + { + var zip = new FastZip(); + zip.ExtractZip(fileStream, targetDirectory, FastZip.Overwrite.Always, null, null, null, + false, true); + } +} \ No newline at end of file diff --git a/src/AeFinder.Cli/AeFinder.Cli.csproj b/src/AeFinder.Cli/AeFinder.Cli.csproj new file mode 100644 index 0000000..0f76a75 --- /dev/null +++ b/src/AeFinder.Cli/AeFinder.Cli.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + AeFinder.Cli + aefinder + true + true + + + + + + + + + + + + + diff --git a/src/AeFinder.Cli/AeFinderCliModule.cs b/src/AeFinder.Cli/AeFinderCliModule.cs new file mode 100644 index 0000000..802db03 --- /dev/null +++ b/src/AeFinder.Cli/AeFinderCliModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace AeFinder.Cli; + +[DependsOn( + typeof(AeFinderCliCoreModule), + typeof(AbpAutofacModule) +)] +public class AeFinderCliModule: AbpModule +{ + +} \ No newline at end of file diff --git a/src/AeFinder.Cli/Program.cs b/src/AeFinder.Cli/Program.cs new file mode 100644 index 0000000..a2f0116 --- /dev/null +++ b/src/AeFinder.Cli/Program.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using Volo.Abp; + +namespace AeFinder.Cli; + +public static class Program +{ + private static async Task Main(string[] args) + { + Console.OutputEncoding = System.Text.Encoding.UTF8; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) + .MinimumLevel.Override("Volo.Abp.IdentityModel", LogEventLevel.Information) +#if DEBUG + .MinimumLevel.Override("AeFinder.Cli", LogEventLevel.Debug) +#else + .MinimumLevel.Override("AeFinder.Cli", LogEventLevel.Information) +#endif + .Enrich.FromLogContext() + .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen) + .CreateLogger(); + + using var application = await AbpApplicationFactory.CreateAsync( + options => + { + options.UseAutofac(); + options.Services.AddLogging(c => c.AddSerilog()); + }); + await application.InitializeAsync(); + + await application.ServiceProvider + .GetRequiredService() + .RunAsync(args); + + await application.ShutdownAsync(); + + await Log.CloseAndFlushAsync(); + } +} \ No newline at end of file