├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── dotnet.yml │ └── dotnetcore-nuget.yml ├── .gitignore ├── .vscode └── settings.json ├── AbpDevTools.sln ├── LICENSE.txt ├── README.md ├── art ├── logo.svg ├── logo_128.png └── logo_512.png ├── images ├── abpdevbuild-interactive.gif ├── abpdevbuild.gif ├── abpdevenvapp.gif ├── abpdevlogs-clear.gif ├── abpdevlogs.gif ├── abpdevnotification.gif ├── abpdevprepare.gif ├── abpdevreplace.gif ├── abpdevrun-all.gif ├── abpdevrun-multiplesolutions.gif └── abpdevrun.gif ├── install.ps1 ├── pack.ps1 └── src └── AbpDevTools ├── AbpDevTools.csproj ├── Commands ├── AbpBundleCommand.cs ├── AbpBundleListCommand.cs ├── BuildCommand.cs ├── CleanCommand.cs ├── ConfigurationClearCommand.cs ├── ConfigurationCommand.cs ├── DatabaseDropCommand.cs ├── DisableNotificationsCommand.cs ├── EnableNotificationsCommand.cs ├── EnvironmentAppCommand.cs ├── EnvironmentAppStartCommand.cs ├── EnvironmentAppStopCommand.cs ├── EnvironmentCommand.cs ├── EnvironmentConfigurationCommand.cs ├── FindFileCommand.cs ├── LogsClearCommand.cs ├── LogsCommand.cs ├── MigrateCommand.cs ├── Migrations │ ├── AddMigrationCommand.cs │ ├── ClearMigrationsCommand.cs │ ├── MigrationsCommand.cs │ └── MigrationsCommandBase.cs ├── PrepareCommand.cs ├── ReplaceCommand.cs ├── RunCommand.cs ├── RunningProjectItem.cs ├── SwitchToEnvironmentCommand.cs ├── TestCommand.cs ├── ToolsCommand.cs └── UpdateCheckCommand.cs ├── Configuration ├── CleanConfiguration.cs ├── ConfigurationBase.cs ├── DictionaryConfigurationBase.cs ├── EnvironmentAppConfiguration.cs ├── EnvironmentConfiguration.cs ├── NotificationConfiguration.cs ├── ReplacementConfiguration.cs ├── ReplacementOption.cs ├── RunConfiguration.cs └── ToolsConfiguration.cs ├── DotnetDependencyResolver.cs ├── Environments ├── AppEnvironmentMapping.cs ├── IProcessEnvironmentManager.cs └── ProcessEnvironmentManager.cs ├── FileExplorer.cs ├── Global.usings.cs ├── LocalConfigurations ├── LocalConfiguration.cs └── LocalConfigurationManager.cs ├── Notifications ├── DefaultNotificationManager.cs ├── INotificationManager.cs ├── MacCatalystNotificationManager.cs └── WindowsNotificationManager.cs ├── Platform.cs ├── Program.cs ├── Properties └── launchSettings.json └── Services ├── EntityFrameworkCoreProjectsProvider.cs ├── RunnableProjectsProvider.cs ├── UpdateCheckResult.cs └── UpdateChecker.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 9.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | # - name: Test 28 | # run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore-nuget.yml: -------------------------------------------------------------------------------- 1 | name: Nuget Publish Pipeline 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 9.0.100 16 | - name: Restore 17 | run: dotnet restore 18 | - name: Pack 19 | run: dotnet pack ./src/AbpDevTools/AbpDevTools.csproj -c Release --include-symbols --include-source -o ./nupkg 20 | - name: Push 21 | run: dotnet nuget push './nupkg/*.symbols.nupkg' --source ${{secrets.NUGET_SOURCE}} --api-key ${{secrets.NUGET_KEY}} --skip-duplicate 22 | #continue-on-error: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "AbpDevTools.sln" 3 | } -------------------------------------------------------------------------------- /AbpDevTools.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33606.364 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{63A376BC-8AD1-46C6-B16F-F890103018DB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AbpDevTools", "src\AbpDevTools\AbpDevTools.csproj", "{C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8}.Release|Any CPU.Build.0 = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(NestedProjects) = preSolution 25 | {C58E2B8D-4310-4DA3-AD79-3B0555C6DFC8} = {63A376BC-8AD1-46C6-B16F-F890103018DB} 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {0798E145-DECF-44E8-9B64-2931A61A8E24} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 enisn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # AbpDevTools 6 | A set of tools to make development with ABP easier. It's a dotnet tool and accessed via `abpdev` **CLI** command. 7 | 8 | It helps the developer, build, run, replace, and manage logs of the projects. It makes running **multiple** solutions and projects easier. 9 | 10 | > Done with [Enisn.Templates.CLI](https://github.com/enisn/Enisn.Templates.CLI). You can build similar CLI tools with this template. 11 | 12 | AbpDevTools on Nuget 13 | 14 | # Installation 15 | 16 | - Install [AbpDevTools from NuGet](https://www.nuget.org/packages/AbpDevTools) as dotnet tool: 17 | ```bash 18 | dotnet tool update -g AbpDevTools 19 | ``` 20 | 21 | - Installation for a specific runtime other than the latest: 22 | 23 | ```bash 24 | dotnet tool update -g AbpDevTools --framework net8.0 25 | # or 26 | dotnet tool update -g AbpDevTools --framework net6.0 27 | ``` 28 | 29 | > This package is compiled for .NET 6.0, 7.0, 8.0 and 9.0. So you can install it for a specific runtime. If you don't specify a runtime, it'll install the latest version. 30 | 31 | ## Local Installation 32 | If you don't have access to the package source. You can install it from the source code by the following code: 33 | 34 | ```bash 35 | pwsh install.ps1 36 | ``` 37 | 38 | # Getting Started 39 | You can watch the **'Getting Started'** video for onboarding this tool: 40 | 41 | 42 | 43 | 44 | # Usage 45 | 46 | Execute `abpdev` command in the terminal and it'll show you the help message. 47 | 48 | ```bash 49 | abpdev --help 50 | ``` 51 | 52 | # Commands 53 | The following commands are available: 54 | 55 | ## abpdev build 56 | Builds all solutions/projects in the current directory recursively. _(Multiple solutions)_ 57 | 58 | ``` 59 | abpdev build [options] 60 | ``` 61 | 62 | ```bash 63 | abpdev build -h 64 | 65 | PARAMETERS 66 | workingdirectory Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory) 67 | OPTIONS 68 | -f|--build-files (Array) Names or part of names of projects or solutions will be built. 69 | -i|--interactive Interactive build file selection. Default: "False". 70 | -c|--configuration 71 | -h|--help Shows help text. 72 | ``` 73 | 74 | Convention: `*.sln` files are considered as solutions and `*.csproj` files are considered as projects. 75 | 76 | ![abpdev build](images/abpdevbuild.gif) 77 | 78 | ### Example commands 79 | 80 | - Run in a specific path 81 | ```bash 82 | abpdev build C:\Path\To\Projects 83 | ``` 84 | 85 | - Run in a specific configuration 86 | ```bash 87 | abpdev build C:\Path\To\Projects -c Release 88 | ``` 89 | 90 | 91 | - Run in a specific path with specific configuration and specific projects 92 | ```bash 93 | abpdev build C:\Path\To\Projects -c Release -f ProjectA.csproj ProjectB.csproj 94 | ``` 95 | 96 | - Run in interactive mode **(Select projects to build)** 97 | ```bash 98 | abpdev build -i 99 | ``` 100 | ![abpdev build interactive](images/abpdevbuild-interactive.gif) 101 | 102 | ## abpdev run 103 | Runs the solution in the current directory. _(Multiple solution, multiple applications including DbMigrator)_ 104 | 105 | ``` 106 | abpdev run [options] 107 | ``` 108 | 109 | ```bash 110 | PARAMETERS 111 | workingdirectory Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory) 112 | 113 | OPTIONS 114 | -w|--watch Watch mode Default: "False". 115 | --skip-migrate Skips migration and runs projects directly. Default: "False". 116 | -a|--all Projects to run will not be asked as prompt. All of them will run. Default: "False". 117 | --no-build Skipts build before running. Passes '--no-build' parameter to dotnet run. Default: "False". 118 | -i|--install-libs Runs 'abp install-libs' command while running the project simultaneously. Default: "False". 119 | -g|--graphBuild Uses /graphBuild while running the applications. So no need building before running. But it may cause some performance. Default: "False". 120 | -p|--projects (Array) Names or part of names of projects will be ran. 121 | -c|--configuration 122 | -e| --env Virtual Environment name. You can manage virtual environments by using 'abpdev env config' 123 | -h|--help Shows help text. 124 | ``` 125 | 126 | Convention: `*.csproj` files with specific names are considered as applications or dbmigrators. 127 | 128 | > _Use `abpdev run config` command to change project name conventions according to your requirements_ 129 | 130 | ![abpdev run](images/abpdevrun.gif) 131 | 132 | ### Example commands 133 | 134 | - Run multiple solutions 135 | ```bash 136 | abpdev run C:\Path\To\Top\Folder\Of\Solutions 137 | ``` 138 | ![abpdev run multiple solutions](images/abpdevrun-multiplesolutions.gif) 139 | 140 | - Run in a specific path 141 | ```bash 142 | abpdev run C:\Path\To\Projects 143 | ``` 144 | 145 | - Run in a specific configuration and specific path 146 | ```bash 147 | abpdev run C:\Path\To\Projects -c Release 148 | ``` 149 | 150 | - Run all projects instead prompt selection 151 | ```bash 152 | abpdev run -a 153 | ``` 154 | 155 | ![abpdev run all](images/abpdevrun-all.gif) 156 | 157 | - Skip migration and run projects directly 158 | ```bash 159 | abpdev run --skip-migrate 160 | ``` 161 | 162 | - Run in watch mode 163 | ```bash 164 | abpdev run -w 165 | ``` 166 | > Please note that we cannot print URL's because dotnet does give any output. 167 | 168 | ## Virtual Environments 169 | Virtual environments are used to run multiple solutions with different configurations. For example, you can run different solutions with different environments _(connectionstrings etc.)_. 170 | 171 | You can manage virtual environments by using `abpdev env config` command and use those pre-configured environments with other commands like: `abpdev run -e SqlServer`. This command will use the environment named SqlServer. You can set different connectionstrings for each environment. 172 | 173 | ```bash 174 | abpdev env config 175 | ``` 176 | 177 | > You'll see the following screen. You can add, edit, delete, and select virtual environments. 178 | > ```json 179 | > { 180 | > "SqlServer": { 181 | > "Variables": { 182 | > "ConnectionStrings__Default": "Server=localhost;Database={AppName}_{Today};User ID=SA;Password=12345678Aa;TrustServerCertificate=True" 183 | > } 184 | > }, 185 | > "MongoDB": { 186 | > "Variables": { 187 | > "ConnectionStrings__Default": "mongodb://localhost:27017/{AppName}_{Today}" 188 | > } 189 | > } 190 | >} 191 | > ``` 192 | > **{Today}** will be replaced with the current date. So you can run multiple solutions with different databases. 193 | > **{AppName}** will be replaced with the application name. So you can run multiple solutions with different databases. _When app name couldn't be detected, folder name will be used. 194 | 195 | 196 | ### Example commands 197 | 198 | - Run in a specific virtual environment 199 | ```bash 200 | abpdev run -e SqlServer 201 | ``` 202 | 203 | 204 | 205 | 206 | ## abpdev prepare 207 | Prepares the project for the first running on this machine. Automatically detects project dependencies, starts required environment apps (databases, message brokers), installs ABP libraries, and creates local configuration files. You can use the `abpdev.yml` configuration file to run the project with different environments without changing the `appsettings.json` files. You can modify the created `abpdev.yml` file to change or add new environment variables to run profile. 208 | 209 | ``` 210 | abpdev prepare [options] 211 | ``` 212 | 213 | ```bash 214 | abpdev prepare -h 215 | 216 | PARAMETERS 217 | workingdirectory Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory) 218 | 219 | OPTIONS 220 | --no-config Do not create local configuration file. (abpdev.yml) Default: "False". 221 | -h|--help Shows help text. 222 | ``` 223 | 224 | Convention: The command analyzes project dependencies to determine which environment apps are needed (SQL Server, MongoDB, Redis, etc.) and automatically configures the environment accordingly. 225 | 226 | ![abpdev prepare](images/abpdevprepare.gif) 227 | 228 | ### Example commands 229 | 230 | - Prepare project in current directory 231 | ```bash 232 | abpdev prepare 233 | ``` 234 | 235 | - Prepare project in a specific path 236 | ```bash 237 | abpdev prepare C:\Path\To\Projects 238 | ``` 239 | 240 | - Prepare without creating local configuration files 241 | ```bash 242 | abpdev prepare --no-config 243 | ``` 244 | 245 | This command performs the following operations: 246 | - **Dependency Analysis**: Scans projects for database and messaging dependencies 247 | - **Environment Apps**: Starts required Docker containers (SQL Server, MongoDB, Redis, etc.) 248 | - **Library Installation**: Runs `abp install-libs` to install client-side libraries 249 | - **Blazor Bundling**: Bundles Blazor WASM projects 250 | - **Configuration**: Creates `abpdev.yml` files with appropriate environment settings 251 | 252 | ## abpdev logs 253 | Finds given project under the current directory and shows logs of it. 254 | 255 | ![abpdev logs](images/abpdevlogs.gif) 256 | --- 257 | ![abpdev logs clear](images/abpdevlogs-clear.gif) 258 | 259 | 260 | ```bash 261 | abpdev logs [options] 262 | abpdev logs [command] [...] 263 | 264 | PARAMETERS 265 | projectname Determines the project to open logs of it. 266 | 267 | OPTIONS 268 | -p|--path Working directory of the command. Probably solution directory. Default: . (CurrentDirectory) 269 | -i|--interactive Options will be asked as prompt when this option used. Default: "False". 270 | -h|--help Shows help text. 271 | 272 | COMMANDS 273 | clear 274 | ``` 275 | 276 | ### Example commands 277 | 278 | - Show logs of the **.Web** project 279 | ```bash 280 | abpdev logs Web 281 | ``` 282 | 283 | - Clear logs of the **.Web** project 284 | ```bash 285 | abpdev logs clear -p Web 286 | ``` 287 | 288 | - Clear logs without approval 289 | ```bash 290 | abpdev logs clear -p Web -f 291 | ``` 292 | 293 | ## abpdev replace 294 | Replaces specified text in files under the current directory recursively. Mostly used to replace connection strings in `appsettings.json` files. But it can be used for any other purposes. 295 | 296 | ```bash 297 | USAGE 298 | abpdev replace [options] 299 | abpdev replace [command] [...] 300 | 301 | DESCRIPTION 302 | Runs file replacement according to configuration. 303 | 304 | PARAMETERS 305 | replacementconfigname If you execute single option from config, you can pass the name or pass 'all' to execute all of them 306 | 307 | OPTIONS 308 | -p|--path Working directory of the command. Probably solution directory. Default: . (CurrentDirectory) 309 | -i|--interactive Interactive Mode. It'll ask prompt to pick one config. Default: "False". 310 | -h|--help Shows help text. 311 | 312 | COMMANDS 313 | config Allows managing replacement configuration. Subcommands: config clear. 314 | ``` 315 | 316 | ![abpdev replace](images/abpdevreplace.gif) 317 | 318 | 319 | > Use `abpdev replace config` command to change file name conventions according to your requirements. 320 | > _You'll see something like that by default: 321 | > ```json 322 | > { 323 | > "ConnectionStrings": { 324 | > "FilePattern": "appsettings.json", 325 | > "Find": "Trusted_Connection=True;", 326 | > "Replace": "User ID=SA;Password=12345678Aa;" 327 | > } 328 | > } 329 | > ``` 330 | 331 | ### Example commands 332 | 333 | - Replace connection strings in `appsettings.json` files 334 | ```bash 335 | abpdev replace ConnectionStrings 336 | ``` 337 | 338 | - Run all the configurations at once 339 | ```bash 340 | abpdev replace all 341 | ``` 342 | 343 | ## Enable Notifications 344 | You can enable notifications to get notified when a build or run process is completed. You can enable it by using `abpdev enable-notifications` command and disable it by using `abpdev disable-notifications` command. 345 | 346 | > _It only works on **Windows** and **MacOS**. Linux is not supported yet._ 347 | 348 | ```bash 349 | abpdev enable-notifications 350 | abpdev disable-notifications 351 | ``` 352 | 353 | It'll send a notification when a **migration**, **build** or **run** process is completed. 354 | 355 | ![abpdev notifications](images/abpdevnotification.gif) 356 | 357 | 358 | ## Environment Apps 359 | You can easily run commonly used environment apps like **SQL Server**, **PostgreSQL**, **Redis**, **MySQL**, **MongoDB** and **RabbitMQ** by using `abpdev envapp start` command. 360 | 361 | ```bash 362 | abpdev envapp [command] [options] 363 | 364 | abpdev envapp start [options] 365 | abpdev envapp stop [options] 366 | ``` 367 | 368 | > You can change the default running commands by using `abpdev envapp config` command. 369 | 370 | ![abpdev envapp](images/abpdevenvapp.gif) 371 | 372 | Available app names by **default**: 373 | ```bash 374 | - sqlserver 375 | - sqlserver-edge 376 | - postgresql 377 | - mysql 378 | - mongodb 379 | - redis 380 | - rabbitmq 381 | ``` 382 | 383 | _You can extend the list or change environments of apps by using `abpdev envapp config` command._ 384 | 385 | ### Example commands 386 | 387 | - Start SQL Server with custom SA password 388 | ```bash 389 | abpdev envapp start sqlserver -p myPassw0rd 390 | ``` 391 | -------------------------------------------------------------------------------- /art/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Created with Fabric.js 5.3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /art/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/art/logo_128.png -------------------------------------------------------------------------------- /art/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/art/logo_512.png -------------------------------------------------------------------------------- /images/abpdevbuild-interactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevbuild-interactive.gif -------------------------------------------------------------------------------- /images/abpdevbuild.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevbuild.gif -------------------------------------------------------------------------------- /images/abpdevenvapp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevenvapp.gif -------------------------------------------------------------------------------- /images/abpdevlogs-clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevlogs-clear.gif -------------------------------------------------------------------------------- /images/abpdevlogs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevlogs.gif -------------------------------------------------------------------------------- /images/abpdevnotification.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevnotification.gif -------------------------------------------------------------------------------- /images/abpdevprepare.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevprepare.gif -------------------------------------------------------------------------------- /images/abpdevreplace.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevreplace.gif -------------------------------------------------------------------------------- /images/abpdevrun-all.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevrun-all.gif -------------------------------------------------------------------------------- /images/abpdevrun-multiplesolutions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevrun-multiplesolutions.gif -------------------------------------------------------------------------------- /images/abpdevrun.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enisn/AbpDevTools/c26f328ce3474c08d839045ccfa991fdf7b24eae/images/abpdevrun.gif -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | pwsh ./pack.ps1 2 | dotnet tool update -g AbpDevTools --add-source ./nupkg --prerelease -------------------------------------------------------------------------------- /pack.ps1: -------------------------------------------------------------------------------- 1 | dotnet pack ./src/AbpDevTools/AbpDevTools.csproj -c Release --include-symbols --include-source -o ./nupkg -------------------------------------------------------------------------------- /src/AbpDevTools/AbpDevTools.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | 1.9.2 6 | enable 7 | true 8 | logo_128.png 9 | https://github.com/enisn/AbpDevTools 10 | LICENSE.txt 11 | enable 12 | true 13 | abpdev 14 | ../../nupkg 15 | 16 | 17 | 18 | net6.0;net7.0;net8.0;net9.0 19 | 20 | 21 | net9.0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/AbpBundleCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | using System.Diagnostics; 5 | using System.Text; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("bundle", Description = "Runs 'abp bundle' command for each Blazor WASM projects recursively.")] 10 | public class AbpBundleCommand : ICommand 11 | { 12 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 13 | public string? WorkingDirectory { get; set; } 14 | 15 | [CommandOption("graphBuild", 'g', Description = "Graph builds project before running 'abp bundle'.")] 16 | public bool GraphBuild { get; set; } 17 | 18 | protected IConsole? console; 19 | protected AbpBundleListCommand listCommand; 20 | 21 | protected ToolOption Tools { get; } 22 | 23 | public AbpBundleCommand(AbpBundleListCommand listCommand, ToolsConfiguration toolsConfiguration) 24 | { 25 | this.listCommand = listCommand; 26 | Tools = toolsConfiguration.GetOptions(); 27 | } 28 | 29 | public async ValueTask ExecuteAsync(IConsole console) 30 | { 31 | this.console = console; 32 | if (string.IsNullOrEmpty(WorkingDirectory)) 33 | { 34 | WorkingDirectory = Directory.GetCurrentDirectory(); 35 | } 36 | listCommand.WorkingDirectory = WorkingDirectory; 37 | 38 | console.RegisterCancellationHandler().Register(() => 39 | { 40 | console.Output.WriteLine("Abp bundle cancelled."); 41 | throw new OperationCanceledException("Abp bundle cancelled."); 42 | }); 43 | 44 | var wasmCsprojs = await AnsiConsole.Status() 45 | .StartAsync("Looking for projects", async ctx => 46 | { 47 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 48 | 49 | await Task.Yield(); 50 | 51 | return listCommand.GetWasmProjects().ToArray(); 52 | }); 53 | 54 | if (!wasmCsprojs.Any()) 55 | { 56 | await console.Output.WriteLineAsync("No Blazor WASM projects found. No files to bundle."); 57 | 58 | return; 59 | } 60 | 61 | AnsiConsole.MarkupLine($"[green]{wasmCsprojs.Length}[/] blazor wasm projects found."); 62 | 63 | foreach (var csproj in wasmCsprojs) 64 | { 65 | if (GraphBuild) 66 | { 67 | var index = Array.IndexOf(wasmCsprojs, csproj) + 1; 68 | var compiled = await AnsiConsole.Status().StartAsync($"[grey]{index/wasmCsprojs.Length} Building {csproj.Name}...[/]", async (ctx) => 69 | { 70 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 71 | 72 | var startInfo = new ProcessStartInfo("dotnet", $"build /graphBuild") 73 | { 74 | WorkingDirectory = Path.GetDirectoryName(csproj.FullName)!, 75 | }; 76 | startInfo.RedirectStandardOutput = true; 77 | using var process = Process.Start(startInfo)!; 78 | await process.WaitForExitAsync(); 79 | 80 | if (process.ExitCode == 0) 81 | { 82 | AnsiConsole.MarkupLine($"[green]Completed[/][grey] Building {csproj.Name}[/]"); 83 | return true; 84 | } 85 | else 86 | { 87 | AnsiConsole.MarkupLine($"[red]Couldn't compile[/] {csproj.Name}"); 88 | return false; 89 | } 90 | }); 91 | 92 | if (!compiled) 93 | { 94 | continue; 95 | } 96 | } 97 | 98 | await AnsiConsole.Status().StartAsync($"Running 'abp bundle' for {csproj.Name}...", async ctx => 99 | { 100 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 101 | 102 | var startInfo = new ProcessStartInfo(Tools["abp"], $"bundle -wd {Path.GetDirectoryName(csproj.FullName)}"); 103 | startInfo.RedirectStandardOutput = true; 104 | using var process = Process.Start(startInfo)!; 105 | process.BeginOutputReadLine(); 106 | await process.WaitForExitAsync(); 107 | 108 | if (process.ExitCode == 0) 109 | { 110 | AnsiConsole.MarkupLine($"[green]Success[/] while running 'abp bundle' for {csproj.Name}."); 111 | } 112 | else 113 | { 114 | AnsiConsole.MarkupLine($"[red]Error[/] while running 'abp bundle' for {csproj.Name}."); 115 | } 116 | }); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/AbpBundleListCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CliFx.Infrastructure; 3 | using System.Xml; 4 | using Spectre.Console; 5 | 6 | [Command("bundle list", Description = "List projects that needs to run 'abp bundle'.")] 7 | public class AbpBundleListCommand : ICommand 8 | { 9 | [CommandParameter(0, IsRequired = false, Description = "Working directory for the command. Probably project or solution directory path goes here. Default: . (Current Directory)")] 10 | public string? WorkingDirectory { get; set; } 11 | 12 | public async ValueTask ExecuteAsync(IConsole console) 13 | { 14 | if (string.IsNullOrEmpty(WorkingDirectory)) 15 | { 16 | WorkingDirectory = Directory.GetCurrentDirectory(); 17 | } 18 | 19 | var wasmCsprojs = await AnsiConsole.Status() 20 | .StartAsync("Searching for Blazor WASM projects...", async ctx => 21 | { 22 | ctx.Spinner(Spinner.Known.Dots); 23 | var wasmCsprojs = GetWasmProjects(); 24 | foreach (var csproj in wasmCsprojs) 25 | { 26 | AnsiConsole.MarkupLine($"- .{Path.DirectorySeparatorChar}{Path.GetRelativePath(WorkingDirectory!, csproj.DirectoryName ?? string.Empty)}"); 27 | } 28 | return wasmCsprojs; 29 | }); 30 | 31 | if (!wasmCsprojs.Any()) 32 | { 33 | await console.Output.WriteLineAsync("No Blazor WASM projects found. No files to bundle."); 34 | return; 35 | } 36 | } 37 | 38 | public IEnumerable GetWasmProjects(){ 39 | return Directory.EnumerateFiles(WorkingDirectory!, "*.csproj", SearchOption.AllDirectories) 40 | .Where(IsCsprojBlazorWasm) 41 | .Select(x => new FileInfo(x)); 42 | } 43 | 44 | private static bool IsCsprojBlazorWasm(string file) 45 | { 46 | using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read); 47 | using var reader = XmlReader.Create(fileStream, new XmlReaderSettings 48 | { 49 | DtdProcessing = DtdProcessing.Ignore, 50 | IgnoreWhitespace = true 51 | }); 52 | 53 | try 54 | { 55 | // Look for the Project element 56 | while (reader.Read()) 57 | { 58 | if (reader.NodeType == XmlNodeType.Element && reader.Name == "Project") 59 | { 60 | var sdk = reader.GetAttribute("Sdk"); 61 | return sdk == "Microsoft.NET.Sdk.BlazorWebAssembly"; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | catch (XmlException) 68 | { 69 | return false; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/BuildCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Notifications; 3 | using CliFx.Infrastructure; 4 | using Spectre.Console; 5 | using System; 6 | using System.Diagnostics; 7 | 8 | namespace AbpDevTools.Commands; 9 | 10 | [Command("build", Description = "Shortcut for dotnet build /graphBuild")] 11 | public class BuildCommand : ICommand 12 | { 13 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 14 | public string? WorkingDirectory { get; set; } 15 | 16 | [CommandOption("build-files", 'f', Description = "(Array) Names or part of names of projects or solutions will be built.")] 17 | public string[]? BuildFiles { get; set; } 18 | 19 | [CommandOption("interactive", 'i', Description = "Interactive build file selection.")] 20 | public bool Interactive { get; set; } 21 | 22 | [CommandOption("configuration", 'c')] 23 | public string? Configuration { get; set; } 24 | 25 | Process? runningProcess; 26 | protected readonly INotificationManager notificationManager; 27 | protected readonly ToolsConfiguration toolsConfiguration; 28 | 29 | public BuildCommand(INotificationManager notificationManager, ToolsConfiguration toolsConfiguration) 30 | { 31 | this.notificationManager = notificationManager; 32 | this.toolsConfiguration = toolsConfiguration; 33 | } 34 | 35 | public async ValueTask ExecuteAsync(IConsole console) 36 | { 37 | if (string.IsNullOrEmpty(WorkingDirectory)) 38 | { 39 | WorkingDirectory = Directory.GetCurrentDirectory(); 40 | } 41 | 42 | var cancellationToken = console.RegisterCancellationHandler(); 43 | var buildFiles = await FindBuildFilesAsync("*.sln", "solution"); 44 | 45 | if (buildFiles.Length == 0) 46 | { 47 | await console.Output.WriteLineAsync("No .sln files found. Looking for .csproj files."); 48 | 49 | buildFiles = await FindBuildFilesAsync("*.csproj", "csproj"); 50 | } 51 | 52 | if (buildFiles.Length == 0) 53 | { 54 | await console.Output.WriteLineAsync("No .csproj files found. No files to build."); 55 | 56 | return; 57 | } 58 | 59 | var successfulCount = await AnsiConsole.Status().StartAsync("Starting build...", async ctx => 60 | { 61 | int completed = 0; 62 | for (int i = 0; i < buildFiles.Length; i++) 63 | { 64 | var buildFile = buildFiles[i]; 65 | var progressRatio = $"[yellow]{i + 1}/{buildFiles.Length}[/]"; 66 | ctx.Status($"{progressRatio} - [bold]Building[/] {buildFile.FullName}"); 67 | 68 | var commandSuffix = string.Empty; 69 | 70 | if (!string.IsNullOrEmpty(Configuration)) 71 | { 72 | commandSuffix += $" --configuration {Configuration}"; 73 | } 74 | 75 | var tools = toolsConfiguration.GetOptions(); 76 | runningProcess = Process.Start(new ProcessStartInfo(tools["dotnet"], "build /graphBuild" + commandSuffix) 77 | { 78 | WorkingDirectory = Path.GetDirectoryName(buildFile.FullName), 79 | UseShellExecute = false, 80 | RedirectStandardOutput = true, 81 | RedirectStandardError = true, 82 | })!; 83 | 84 | // equivalent of WaitForExit 85 | var _output = await runningProcess.StandardOutput.ReadToEndAsync(); 86 | await runningProcess.WaitForExitAsync(); 87 | 88 | if (runningProcess.ExitCode == 0) 89 | { 90 | completed++; 91 | AnsiConsole.MarkupLine($"{progressRatio} - [green]completed[/] [bold]Building[/] [silver]{buildFile.Name}[/]"); 92 | } 93 | else 94 | { 95 | AnsiConsole.MarkupLine($"{progressRatio} - [red]failed [bold]Building[/] {buildFile.Name} Exit Code: {runningProcess.ExitCode}[/]"); 96 | AnsiConsole.MarkupLine($"[grey]{_output}[/]"); 97 | } 98 | 99 | if (cancellationToken.IsCancellationRequested) 100 | { 101 | break; 102 | } 103 | 104 | runningProcess.Kill(entireProcessTree: true); 105 | } 106 | 107 | return completed; 108 | }); 109 | 110 | if (buildFiles.Length == 1) 111 | { 112 | await notificationManager.SendAsync("Build "+ (successfulCount > 0 ? "Completed!" : "Failed!"), $"{buildFiles[0].Name} has been built."); 113 | } 114 | else 115 | { 116 | await notificationManager.SendAsync("Build Done!", $"{successfulCount} of {buildFiles.Length} projects have been built in '{WorkingDirectory}' folder."); 117 | } 118 | 119 | cancellationToken.Register(KillRunningProcesses); 120 | } 121 | 122 | private async Task FindBuildFilesAsync(string pattern, string? nameOfPattern = null) 123 | { 124 | nameOfPattern ??= "build"; 125 | 126 | var files = await AnsiConsole.Status() 127 | .StartAsync($"Looking for {nameOfPattern} files ({pattern})", async ctx => 128 | { 129 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 130 | 131 | await Task.Yield(); 132 | 133 | var query = Directory.EnumerateFiles(WorkingDirectory!, pattern, SearchOption.AllDirectories); 134 | 135 | if (BuildFiles?.Length > 0) 136 | { 137 | query = query.Where(x => BuildFiles.Any(y => x.Contains(y, StringComparison.InvariantCultureIgnoreCase))); 138 | } 139 | 140 | var fileInfos = query 141 | .Select(x => new FileInfo(x)) 142 | .ToArray(); 143 | 144 | AnsiConsole.MarkupLine($"[green]{fileInfos.Length}[/] {pattern.Replace('*', '\0')} files found."); 145 | 146 | return fileInfos; 147 | }); 148 | 149 | if (Interactive && files.Length > 1) 150 | { 151 | var choosed = AnsiConsole.Prompt( 152 | new MultiSelectionPrompt() 153 | .Title("Choose files to be built:") 154 | .NotRequired() // Not required to have a favorite fruit 155 | .PageSize(12) 156 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 157 | .MoreChoicesText("[grey](Move up and down to reveal more files)[/]") 158 | .InstructionsText( 159 | "[grey](Press [mediumpurple2][/] to toggle a file, " + 160 | "[green][/] to accept)[/]") 161 | .AddChoices(files.Select(s => s.DirectoryName!.Replace(WorkingDirectory!, ".")))); 162 | 163 | files = files.Where(x => choosed.Contains(x.FullName)).ToArray(); 164 | } 165 | 166 | return files; 167 | } 168 | 169 | protected void KillRunningProcesses() 170 | { 171 | runningProcess?.Kill(entireProcessTree: true); 172 | 173 | runningProcess?.WaitForExit(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/CleanCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | 5 | namespace AbpDevTools.Commands; 6 | 7 | [Command("clean", Description = "Cleans 'bin', 'obj' and 'node_modules' folders recursively.")] 8 | public class CleanCommand : ICommand 9 | { 10 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 11 | public string? WorkingDirectory { get; set; } 12 | 13 | private readonly CleanConfiguration cleanConfiguration; 14 | 15 | public CleanCommand(CleanConfiguration cleanConfiguration) 16 | { 17 | this.cleanConfiguration = cleanConfiguration; 18 | } 19 | 20 | public async ValueTask ExecuteAsync(IConsole console) 21 | { 22 | if (string.IsNullOrEmpty(WorkingDirectory)) 23 | { 24 | WorkingDirectory = Directory.GetCurrentDirectory(); 25 | } 26 | 27 | var foldersToDelete = cleanConfiguration.GetOptions() 28 | .Folders.Select(x => Path.DirectorySeparatorChar + x) 29 | .ToArray(); 30 | 31 | await AnsiConsole.Status() 32 | .StartAsync("Looking for directories...", async ctx => 33 | { 34 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 35 | 36 | await Task.Yield(); 37 | 38 | var directories = Directory.EnumerateDirectories(WorkingDirectory!, string.Empty, SearchOption.AllDirectories) 39 | .Where(x => foldersToDelete.Any(a => x.EndsWith(a))); 40 | 41 | foreach (var directory in directories) 42 | { 43 | ctx.Status($"Deleting {directory}..."); 44 | Directory.Delete(directory, true); 45 | } 46 | }); 47 | 48 | console.Output.WriteLine("Cleaned successfully."); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/ConfigurationClearCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | 4 | namespace AbpDevTools.Commands; 5 | 6 | public abstract class ConfigurationClearCommandBase : ICommand 7 | { 8 | [CommandOption("force", 'f')] 9 | public bool Force { get; set; } 10 | 11 | public virtual ValueTask ExecuteAsync(IConsole console) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | } 16 | 17 | public abstract class ConfigurationClearCommandBase : ConfigurationClearCommandBase 18 | where TConfiguration : IConfigurationBase 19 | { 20 | 21 | private readonly TConfiguration configuration; 22 | 23 | public ConfigurationClearCommandBase(TConfiguration configuration) 24 | { 25 | this.configuration = configuration; 26 | } 27 | 28 | public override async ValueTask ExecuteAsync(IConsole console) 29 | { 30 | if (!Force) 31 | { 32 | await console 33 | .Output 34 | .WriteAsync($"Are you sure to remove existing configuration at path {configuration.FilePath}?\nY/N?"); 35 | 36 | var confirm = await console.Input.ReadLineAsync(); 37 | if (!confirm!.Equals("Y", StringComparison.InvariantCultureIgnoreCase)) 38 | { 39 | return; 40 | } 41 | } 42 | 43 | File.Delete(configuration.FilePath); 44 | } 45 | } 46 | 47 | [Command("config clear", Description = "Clears the current configuration.")] 48 | public class ConfigurationClearCommand : ICommand 49 | { 50 | [CommandOption("force", 'f')] 51 | public bool Force { get; set; } 52 | 53 | protected readonly ConfigurationClearCommandBase[] configurationClearCommands; 54 | 55 | public ConfigurationClearCommand( 56 | ReplacementConfigClearCommand replacementConfigClearCommand, 57 | EnvironmentAppConfigClearCommand environmentAppConfigClearCommand, 58 | RunConfigClearCommand runConfigClearCommand, 59 | CleanConfigClearCommand cleanConfigClearCommand, 60 | ToolsConfigClearCommand toolsConfigClearCommand) 61 | { 62 | configurationClearCommands = new ConfigurationClearCommandBase[] 63 | { 64 | replacementConfigClearCommand, 65 | environmentAppConfigClearCommand, 66 | runConfigClearCommand, 67 | cleanConfigClearCommand, 68 | toolsConfigClearCommand, 69 | }; 70 | } 71 | 72 | public async ValueTask ExecuteAsync(IConsole console) 73 | { 74 | foreach (var command in configurationClearCommands) 75 | { 76 | command.Force = Force; 77 | await command.ExecuteAsync(console); 78 | } 79 | } 80 | } 81 | 82 | [Command("replace config clear")] 83 | [RegisterTransient] 84 | public class ReplacementConfigClearCommand : ConfigurationClearCommandBase 85 | { 86 | public ReplacementConfigClearCommand(ReplacementConfiguration configuration) : base(configuration) 87 | { 88 | } 89 | } 90 | 91 | [Command("envapp config clear")] 92 | public class EnvironmentAppConfigClearCommand : ConfigurationClearCommandBase 93 | { 94 | public EnvironmentAppConfigClearCommand(EnvironmentAppConfiguration configuration) : base(configuration) 95 | { 96 | } 97 | } 98 | 99 | [Command("run config clear")] 100 | public class RunConfigClearCommand : ConfigurationClearCommandBase 101 | { 102 | public RunConfigClearCommand(RunConfiguration configuration) : base(configuration) 103 | { 104 | } 105 | } 106 | 107 | [Command("clean config clear")] 108 | public class CleanConfigClearCommand : ConfigurationClearCommandBase 109 | { 110 | public CleanConfigClearCommand(CleanConfiguration configuration) : base(configuration) 111 | { 112 | } 113 | } 114 | [Command("tools config clear")] 115 | public class ToolsConfigClearCommand : ConfigurationClearCommandBase 116 | { 117 | public ToolsConfigClearCommand(ToolsConfiguration configuration) : base(configuration) 118 | { 119 | } 120 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/ConfigurationCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Exceptions; 3 | using CliFx.Infrastructure; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("config")] 10 | public class ConfigCommand : ICommand 11 | { 12 | public async ValueTask ExecuteAsync(IConsole console) 13 | { 14 | await console.Output.WriteLineAsync("Available commands:\n"); 15 | await console.Output.WriteLineAsync("-abpdev replace config"); 16 | await console.Output.WriteLineAsync("-abpdev envapp config"); 17 | await console.Output.WriteLineAsync("-abpdev run config"); 18 | await console.Output.WriteLineAsync("-abpdev clean config"); 19 | await console.Output.WriteLineAsync("-abpdev tools config"); 20 | await console.Output.WriteLineAsync("-abpdev config clear | Resets all the configurations to defaults."); 21 | } 22 | } 23 | 24 | public abstract class ConfigurationBaseCommand : ICommand 25 | where TConfiguration : IConfigurationBase 26 | { 27 | public TConfiguration Configuration { get; } 28 | 29 | public ConfigurationBaseCommand(TConfiguration configuration) 30 | { 31 | Configuration = configuration; 32 | } 33 | 34 | public virtual ValueTask ExecuteAsync(IConsole console) 35 | { 36 | console.Output.WriteLine("Opening file " + Configuration.FilePath); 37 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 38 | { 39 | Process.Start(new ProcessStartInfo("explorer", Configuration.FilePath)); 40 | } 41 | else 42 | { 43 | Process.Start(new ProcessStartInfo("open", $"\"{Configuration.FilePath}\"")); 44 | } 45 | return ValueTask.CompletedTask; 46 | } 47 | } 48 | 49 | [Command("replace config", Description = "Allows managing replacement configuration.")] 50 | public class ReplaceConfigurationCommand : ConfigurationBaseCommand 51 | { 52 | public ReplaceConfigurationCommand(ReplacementConfiguration configuration) : base(configuration) 53 | { 54 | } 55 | 56 | public override ValueTask ExecuteAsync(IConsole console) 57 | { 58 | Configuration.GetOptions(); 59 | return base.ExecuteAsync(console); 60 | } 61 | } 62 | 63 | [Command("envapp config", Description = "Allows managing replacement configuration.")] 64 | public class EnvironmentAppConfigurationCommand : ConfigurationBaseCommand 65 | { 66 | public EnvironmentAppConfigurationCommand(EnvironmentAppConfiguration configuration) : base(configuration) 67 | { 68 | } 69 | 70 | public override ValueTask ExecuteAsync(IConsole console) 71 | { 72 | Configuration.GetOptions(); 73 | return base.ExecuteAsync(console); 74 | } 75 | } 76 | 77 | [Command("run config")] [Obsolete] 78 | public class RunConfigurationCommand : ConfigurationBaseCommand 79 | { 80 | public RunConfigurationCommand(RunConfiguration configuration) : base(configuration) 81 | { 82 | } 83 | 84 | public override ValueTask ExecuteAsync(IConsole console) 85 | { 86 | Configuration.GetOptions(); 87 | 88 | // This command is deprecated. 89 | // TODO: Remove this command in the future. 90 | throw new CommandException("This command is deprecated. Use \"abpdev run\" directly instead."); 91 | } 92 | } 93 | 94 | [Command("clean config")] 95 | public class CleanConfigurationCommand : ConfigurationBaseCommand 96 | { 97 | public CleanConfigurationCommand(CleanConfiguration configuration) : base(configuration) 98 | { 99 | } 100 | 101 | public override ValueTask ExecuteAsync(IConsole console) 102 | { 103 | Configuration.GetOptions(); 104 | return base.ExecuteAsync(console); 105 | } 106 | } 107 | 108 | [Command("tools config")] 109 | public class ToolsConfigurationCommand : ConfigurationBaseCommand 110 | { 111 | public ToolsConfigurationCommand(ToolsConfiguration configuration) : base(configuration) 112 | { 113 | } 114 | 115 | public override ValueTask ExecuteAsync(IConsole console) 116 | { 117 | Configuration.GetOptions(); 118 | return base.ExecuteAsync(console); 119 | } 120 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/DatabaseDropCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using AbpDevTools.Configuration; 3 | using AbpDevTools.Notifications; 4 | using CliFx.Infrastructure; 5 | using Spectre.Console; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("database-drop", Description = "Drops all databases in the working directory")] 10 | public class DatabaseDropCommand : ICommand 11 | { 12 | [CommandParameter(0, IsRequired = false, Description = "Working directory to search for EntityFramework projects. Default: . (Current Directory)")] 13 | public string? WorkingDirectory { get; set; } 14 | 15 | [CommandOption("force", 'f')] 16 | public bool Force { get; set; } 17 | 18 | protected readonly INotificationManager notificationManager; 19 | protected readonly ToolsConfiguration toolsConfiguration; 20 | 21 | public DatabaseDropCommand(INotificationManager notificationManager, ToolsConfiguration toolsConfiguration) 22 | { 23 | this.notificationManager = notificationManager; 24 | this.toolsConfiguration = toolsConfiguration; 25 | } 26 | 27 | public async ValueTask ExecuteAsync(IConsole console) 28 | { 29 | if (string.IsNullOrEmpty(WorkingDirectory)) 30 | { 31 | WorkingDirectory = Directory.GetCurrentDirectory(); 32 | } 33 | 34 | var efCoreProjects = await GetEfCoreProjectsAsync(); 35 | 36 | var cancellationToken = console.RegisterCancellationHandler(); 37 | 38 | var projectCount = efCoreProjects.Length; 39 | if (projectCount == 0) 40 | { 41 | await console.Output.WriteLineAsync("Could not find any EntityFrameworkCore project in the working directory..."); 42 | return; 43 | } 44 | 45 | AnsiConsole.MarkupLine($"[green]{projectCount}[/] EntityFrameworkCore project(s) found in the directory. Trying to find and drop databases..."); 46 | 47 | var forcePostfix = Force ? " --force" : string.Empty; 48 | 49 | for (var i = 0; i < projectCount; i++) 50 | { 51 | var efCoreProject = efCoreProjects[i]; 52 | 53 | AnsiConsole.MarkupLine($"[blue]## Project{(i + 1)} - {efCoreProject.Name.Replace(".csproj", string.Empty)}[/]"); 54 | 55 | var tools = toolsConfiguration.GetOptions(); 56 | var startInfo = new ProcessStartInfo(tools["dotnet"], $"ef database drop{forcePostfix}") 57 | { 58 | WorkingDirectory = efCoreProject.DirectoryName!, 59 | RedirectStandardOutput = true, 60 | CreateNoWindow = false 61 | }; 62 | 63 | var process = Process.Start(startInfo)!; 64 | 65 | process.OutputDataReceived += async (sender, args) => 66 | { 67 | if (args?.Data != null) 68 | { 69 | await console.Output.WriteLineAsync("* " + args.Data); 70 | } 71 | }; 72 | 73 | process.BeginOutputReadLine(); 74 | 75 | await process.WaitForExitAsync(cancellationToken); 76 | } 77 | 78 | if (!cancellationToken.IsCancellationRequested) 79 | { 80 | await notificationManager.SendAsync("Dropped database(s)", $"Dropped all databases in {WorkingDirectory}"); 81 | } 82 | } 83 | 84 | private async Task GetEfCoreProjectsAsync() 85 | { 86 | return await AnsiConsole.Status() 87 | .StartAsync("Searching EntityFrameworkCore projects...", ctx => 88 | { 89 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 90 | 91 | var efCoreProjects = Directory.EnumerateFiles(WorkingDirectory!, "*.csproj", SearchOption.AllDirectories) 92 | .Where(x => x.EndsWith("EntityFrameworkCore.csproj")) 93 | .Select(x => new FileInfo(x)) 94 | .ToArray(); 95 | 96 | return Task.FromResult(efCoreProjects); 97 | }); 98 | } 99 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/DisableNotificationsCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Exceptions; 3 | using CliFx.Infrastructure; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("disable-notifications")] 10 | public class DisableNotificationsCommand : ICommand 11 | { 12 | [CommandOption("uninstall", 'u', Description = "Uninstalls the 'BurntToast' powershell module.")] 13 | public bool UninstallBurntToast { get; set; } 14 | 15 | protected readonly NotificationConfiguration notificationConfiguration; 16 | 17 | public DisableNotificationsCommand(NotificationConfiguration notificationConfiguration) 18 | { 19 | this.notificationConfiguration = notificationConfiguration; 20 | } 21 | 22 | public async ValueTask ExecuteAsync(IConsole console) 23 | { 24 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 25 | { 26 | if (UninstallBurntToast) 27 | { 28 | var process = Process.Start("powershell", "-Command Uninstall-Module -Name BurntToast"); 29 | 30 | console.RegisterCancellationHandler().Register(() => process.Kill(entireProcessTree: true)); 31 | 32 | await process.WaitForExitAsync(); 33 | } 34 | } 35 | 36 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 37 | { 38 | var options = notificationConfiguration.GetOptions(); 39 | options.Enabled = false; 40 | notificationConfiguration.SetOptions(options); 41 | 42 | return; 43 | } 44 | 45 | throw new CommandException($"This operation isn't supported on {RuntimeInformation.OSDescription} currently. :("); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnableNotificationsCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Notifications; 3 | using CliFx.Exceptions; 4 | using CliFx.Infrastructure; 5 | using System.Diagnostics; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace AbpDevTools.Commands; 9 | 10 | [Command("enable-notifications")] 11 | public class EnableNotificationsCommand : ICommand 12 | { 13 | protected INotificationManager notificationManager; 14 | protected ToolsConfiguration toolsConfiguration; 15 | protected NotificationConfiguration notificationConfiguration; 16 | 17 | public EnableNotificationsCommand(INotificationManager notificationManager, ToolsConfiguration toolsConfiguration, NotificationConfiguration notificationConfiguration) 18 | { 19 | this.notificationManager = notificationManager; 20 | this.toolsConfiguration = toolsConfiguration; 21 | this.notificationConfiguration = notificationConfiguration; 22 | } 23 | 24 | public async ValueTask ExecuteAsync(IConsole console) 25 | { 26 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 27 | { 28 | if (!PowerShellExistsInWindows()) 29 | { 30 | throw new CommandException($"PowerShell is not installed in your system. Please install it and try again."); 31 | } 32 | 33 | var tools = toolsConfiguration.GetOptions(); 34 | var process = Process.Start(tools["powershell"], "-Command Install-Module -Name BurntToast"); 35 | 36 | console.RegisterCancellationHandler().Register(() => process.Kill(entireProcessTree: true)); 37 | 38 | await process.WaitForExitAsync(); 39 | 40 | var options = notificationConfiguration.GetOptions(); 41 | options.Enabled = true; 42 | notificationConfiguration.SetOptions(options); 43 | 44 | await notificationManager.SendAsync("Notifications Enabled", "Notifications will be displayed like this."); 45 | 46 | return; 47 | } 48 | 49 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 50 | { 51 | var options = notificationConfiguration.GetOptions(); 52 | options.Enabled = true; 53 | notificationConfiguration.SetOptions(options); 54 | 55 | await notificationManager.SendAsync("Notifications Enabled", "Notifications will be displayed like this."); 56 | 57 | return; 58 | } 59 | 60 | throw new CommandException($"This operation isn't supported on {RuntimeInformation.OSDescription} currently. :("); 61 | } 62 | 63 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "")] 64 | public bool PowerShellExistsInWindows() 65 | { 66 | string regval = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1", "Install", null).ToString(); 67 | return regval.Equals("1"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnvironmentAppCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | 4 | namespace AbpDevTools.Commands; 5 | 6 | [Command("envapp", Description = "Environment apps that required while development.")] 7 | public class EnvironmentAppCommand : ICommand 8 | { 9 | private readonly EnvironmentAppConfiguration environmentAppConfiguration; 10 | 11 | public EnvironmentAppCommand(EnvironmentAppConfiguration environmentAppConfiguration) 12 | { 13 | this.environmentAppConfiguration = environmentAppConfiguration; 14 | } 15 | 16 | public ValueTask ExecuteAsync(IConsole console) 17 | { 18 | var options = environmentAppConfiguration.GetOptions(); 19 | 20 | console.Output.WriteLine("Available env apps:\n - " + string.Join("\n - ", options.Keys)); 21 | 22 | console.Output.WriteLine("\nRunning env app: \n envapp start \n envapp start redis"); 23 | console.Output.WriteLine("\nStopping env app: \n envapp stop \n envapp stop redis"); 24 | console.Output.WriteLine("\nEditing env apps: \n envapp config"); 25 | 26 | return ValueTask.CompletedTask; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnvironmentAppStartCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Exceptions; 3 | using CliFx.Infrastructure; 4 | using System.Diagnostics; 5 | 6 | namespace AbpDevTools.Commands; 7 | [Command("envapp start", Description = "Deploys infrastructural tools to docker. Such as Redis, RabbitMQ, SqlServer etc.")] 8 | public class EnvironmentAppStartCommand : ICommand 9 | { 10 | [CommandParameter(0, IsRequired = false, Description = "Name of the app.")] 11 | public string[] AppNames { get; set; } = Array.Empty(); 12 | 13 | [CommandOption("password", 'p', Description = "Default password for sql images when applicable. Default: 12345678Aa")] 14 | public string DefaultPassword { get; set; } = "12345678Aa"; 15 | 16 | protected IConsole? console; 17 | protected Dictionary configurations; 18 | 19 | public EnvironmentAppStartCommand(EnvironmentAppConfiguration environmentAppConfiguration) 20 | { 21 | configurations = environmentAppConfiguration.GetOptions(); 22 | } 23 | 24 | public async ValueTask ExecuteAsync(IConsole console) 25 | { 26 | this.console = console; 27 | 28 | if (AppNames == null || AppNames.Length == 0) 29 | { 30 | console.Output.WriteLine("You must specify an app to run.\n" + 31 | "envapp start \n" + 32 | "Available app names:\n - " + string.Join("\n - ", configurations.Keys)); 33 | 34 | return; 35 | } 36 | 37 | foreach (var appName in AppNames) 38 | { 39 | await StartAppAsync(appName); 40 | } 41 | } 42 | 43 | protected async Task StartAppAsync(string appName) 44 | { 45 | if (string.IsNullOrEmpty(appName)) 46 | { 47 | await console!.Output.WriteAsync("App Name can't be null or empty."); 48 | return; 49 | } 50 | 51 | if (!configurations.TryGetValue(appName, out var option)) 52 | { 53 | throw new CommandException($"ToolName '{appName}' couldn't be recognized. Try one of them: \n - " + string.Join("\n - ", configurations.Keys)); 54 | } 55 | 56 | if (string.IsNullOrEmpty(DefaultPassword)) 57 | { 58 | DefaultPassword = "12345678Aa"; 59 | } 60 | 61 | await RunCommandAsync(option.StartCmd.Replace("Passw0rd", DefaultPassword)); 62 | } 63 | 64 | protected async Task RunCommandAsync(string command) 65 | { 66 | var commands = command.Split(';'); 67 | 68 | // Legacy support for old commands: 69 | if (commands.Length == 1 && commands[0].Contains(" || ")) 70 | { 71 | commands = commands[0].Split(" || "); 72 | } 73 | 74 | foreach (var c in commands) 75 | { 76 | var fileName = c[..c.IndexOf(' ')]; 77 | 78 | var process = Process.Start(fileName, c[c.IndexOf(' ')..]); 79 | await process.WaitForExitAsync(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnvironmentAppStopCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Exceptions; 3 | using CliFx.Infrastructure; 4 | using System.Diagnostics; 5 | 6 | namespace AbpDevTools.Commands; 7 | 8 | [Command("envapp stop", Description = "Stops previously deployed environment app.")] 9 | public class EnvironmentAppStopCommand : ICommand 10 | { 11 | private readonly EnvironmentAppConfiguration environmentAppConfiguration; 12 | 13 | [CommandParameter(0, IsRequired = false , Description = "Name of the app.")] 14 | public string? AppName { get; set; } 15 | 16 | public EnvironmentAppStopCommand(EnvironmentAppConfiguration environmentAppConfiguration) 17 | { 18 | this.environmentAppConfiguration = environmentAppConfiguration; 19 | } 20 | public async ValueTask ExecuteAsync(IConsole console) 21 | { 22 | var configurations = environmentAppConfiguration.GetOptions(); 23 | 24 | if (string.IsNullOrEmpty(AppName)) 25 | { 26 | console.Output.WriteLine("You must specify an app to run.\n" + 27 | "envapp start \n" + 28 | "Available app names:\n - all\n" + string.Join("\n - ", configurations.Keys)); 29 | return; 30 | } 31 | 32 | var cancellationToken = console.RegisterCancellationHandler(); 33 | 34 | if (AppName.Equals("all", StringComparison.InvariantCultureIgnoreCase)) 35 | { 36 | foreach (var config in configurations) 37 | { 38 | await console.Output.WriteLineAsync($"Stopping {config.Key}..."); 39 | 40 | await StopWithCmdAsync(config.Value, cancellationToken); 41 | 42 | await console.Output.WriteLineAsync($"Stopped {config.Key}."); 43 | } 44 | 45 | return; 46 | } 47 | 48 | if (!configurations.TryGetValue(AppName, out var option)) 49 | { 50 | throw new CommandException("App name couldn't be recognized. Try one of them: \n" + string.Join("\n - ", configurations.Keys)); 51 | } 52 | 53 | if (option.StopCmd.StartsWith("docker ")) 54 | { 55 | await StopWithCmdAsync(option, cancellationToken); 56 | } 57 | else 58 | { 59 | throw new CommandException($"Only docker apps supported currently. Your command can't be executed. \n'{option.StartCmd}'\n"); 60 | } 61 | } 62 | 63 | private async Task StopWithCmdAsync(EnvironmentToolOption option, CancellationToken cancellationToken) 64 | { 65 | var commands = option.StopCmd.Split(';'); 66 | foreach (var command in commands) 67 | { 68 | var process = Process.Start( 69 | "docker", 70 | command.Replace("docker ", string.Empty)); 71 | 72 | await process.WaitForExitAsync(cancellationToken); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnvironmentCommand.cs: -------------------------------------------------------------------------------- 1 | using CliFx.Infrastructure; 2 | 3 | namespace AbpDevTools.Commands; 4 | 5 | [Command("env", Description = "Virtual Environment. You can create virtual envionments and run your applications on pre-built virtual environments without changing any files on your computer.")] 6 | public class EnvironmentCommand : ICommand 7 | { 8 | public async ValueTask ExecuteAsync(IConsole console) 9 | { 10 | await console.Output.WriteLineAsync("-----------------------------------------------------------"); 11 | await console.Output.WriteLineAsync("AbpDev Environment provides you to build virtual environemnts managed by \n\t'abpdev env config'"); 12 | await console.Output.WriteLineAsync("\n\nUsage:\nIt's not used standalone. You should use created environments with other commands like:"); 13 | await console.Output.WriteLineAsync("\t'abpdev run --env sqlserver'"); 14 | await console.Output.WriteLineAsync("-----------------------------------------------------------"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/EnvironmentConfigurationCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | 4 | namespace AbpDevTools.Commands; 5 | 6 | [Command("env config")] 7 | public class EnvironmentConfigurationCommand : ConfigurationBaseCommand 8 | { 9 | public EnvironmentConfigurationCommand(EnvironmentConfiguration configuration) : base(configuration) 10 | { 11 | } 12 | 13 | public override ValueTask ExecuteAsync(IConsole console) 14 | { 15 | Configuration.GetOptions(); 16 | return base.ExecuteAsync(console); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/FindFileCommand.cs: -------------------------------------------------------------------------------- 1 | using CliFx.Infrastructure; 2 | 3 | namespace AbpDevTools.Commands; 4 | 5 | [Command("find-file", Description = "Finds the specified text in the solution.")] 6 | public class FindFileCommand : ICommand 7 | { 8 | protected FileExplorer FileExplorer { get; } 9 | 10 | [CommandOption("ascendant", 'a', Description = "Determined searching direction as 'Ascendant' or 'Descendants'.")] 11 | public bool Ascendant { get; set; } = false; 12 | 13 | [CommandParameter(0, Description = "Text to search.")] 14 | public string SearchTerm { get; set; } = string.Empty; 15 | 16 | [CommandParameter(1, Description = "Directory to search", IsRequired = false)] 17 | public string WorkingDirectory { get; set; } = string.Empty; 18 | 19 | public FindFileCommand(FileExplorer fileExplorer) 20 | { 21 | FileExplorer = fileExplorer; 22 | } 23 | 24 | public async ValueTask ExecuteAsync(IConsole console) 25 | { 26 | if (string.IsNullOrWhiteSpace(WorkingDirectory)) 27 | { 28 | WorkingDirectory = Directory.GetCurrentDirectory(); 29 | } 30 | 31 | foreach (var file in Find()) 32 | { 33 | await console.Output.WriteLineAsync(file); 34 | } 35 | } 36 | 37 | IEnumerable Find() 38 | { 39 | if (Ascendant) 40 | { 41 | return FileExplorer.FindAscendants(WorkingDirectory, SearchTerm); 42 | } 43 | else 44 | { 45 | return FileExplorer.FindDescendants(WorkingDirectory, SearchTerm); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/LogsClearCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Services; 3 | using CliFx.Infrastructure; 4 | using Spectre.Console; 5 | using System; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("logs clear")] 10 | public class LogsClearCommand : ICommand 11 | { 12 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 13 | public string? WorkingDirectory { get; set; } 14 | 15 | [CommandOption("project", 'p', Description = "Determines the project to open logs of it.")] 16 | public string? ProjectName { get; set; } 17 | 18 | [CommandOption("interactive", 'i', Description = "Options will be asked as prompt when this option used.")] 19 | public bool Interactive { get; set; } 20 | 21 | [CommandOption("force", 'f')] 22 | public bool Force { get; set; } 23 | 24 | protected IConsole? console; 25 | protected readonly RunnableProjectsProvider runnableProjectsProvider; 26 | 27 | public LogsClearCommand(RunnableProjectsProvider runnableProjectsProvider) 28 | { 29 | this.runnableProjectsProvider = runnableProjectsProvider; 30 | } 31 | 32 | public async ValueTask ExecuteAsync(IConsole console) 33 | { 34 | this.console = console; 35 | if (string.IsNullOrEmpty(WorkingDirectory)) 36 | { 37 | WorkingDirectory = Directory.GetCurrentDirectory(); 38 | } 39 | 40 | var csprojs = runnableProjectsProvider.GetRunnableProjects(WorkingDirectory); 41 | 42 | if (string.IsNullOrEmpty(ProjectName)) 43 | { 44 | if (Interactive) 45 | { 46 | await console.Output.WriteLineAsync($"\n"); 47 | ProjectName = AnsiConsole.Prompt( 48 | new SelectionPrompt() 49 | .Title("Choose a [blueviolet]project[/] to open logs?") 50 | .PageSize(12) 51 | .HighlightStyle(new Style(foreground: Color.BlueViolet)) 52 | .MoreChoicesText("[grey](Move up and down to reveal more rules)[/]") 53 | .AddChoices(csprojs.Select(s => s.Name))); 54 | } 55 | else 56 | { 57 | await console.Output.WriteLineAsync("You have to pass a project name.\n"); 58 | await console.Output.WriteLineAsync("\n\tUsage:"); 59 | await console.Output.WriteLineAsync("\tlogs -p "); 60 | await console.Output.WriteLineAsync("\nExample:\n\n\t" + 61 | "abpdev logs Web.csproj"); 62 | return; 63 | } 64 | } 65 | 66 | if (ProjectName.Equals("all", StringComparison.InvariantCultureIgnoreCase)) 67 | { 68 | foreach (var csproj in csprojs) 69 | { 70 | await DeleteCsprojLogsAsync(csproj); 71 | } 72 | 73 | return; 74 | } 75 | 76 | var selectedCsproj = csprojs.FirstOrDefault(x => x.FullName.Contains(ProjectName)); 77 | 78 | if (selectedCsproj == null) 79 | { 80 | await console.Output.WriteLineAsync($"No project found with the name '{ProjectName}'"); 81 | return; 82 | } 83 | 84 | await DeleteCsprojLogsAsync(selectedCsproj); 85 | } 86 | 87 | protected async Task DeleteCsprojLogsAsync(FileInfo csproj) 88 | { 89 | var dir = Path.GetDirectoryName(csproj.FullName)!; 90 | var logsDir = Path.Combine(dir, "Logs"); 91 | if (Directory.Exists(logsDir)) 92 | { 93 | var filePath = Path.Combine(logsDir, "logs.txt"); 94 | if (File.Exists(filePath)) 95 | { 96 | if (!Force && !AnsiConsole.Confirm($"{filePath} will be deleted. Are you sure?")) 97 | { 98 | return; 99 | } 100 | 101 | File.Delete(filePath); 102 | await console!.Output.WriteLineAsync($"{filePath} deleted."); 103 | return; 104 | } 105 | } 106 | 107 | await console!.Output.WriteLineAsync($"No logs found for {csproj.Name}"); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/LogsCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Services; 3 | using CliFx.Infrastructure; 4 | using Spectre.Console; 5 | 6 | namespace AbpDevTools.Commands; 7 | 8 | [Command("logs")] 9 | public class LogsCommand : ICommand 10 | { 11 | [CommandParameter(0, Description = "Determines the project to open logs of it.", IsRequired = false)] 12 | public string? ProjectName { get; set; } 13 | 14 | [CommandOption("path", 'p', Description = "Working directory of the command. Probably solution directory. Default: . (CurrentDirectory) ")] 15 | public string? WorkingDirectory { get; set; } 16 | 17 | [CommandOption("interactive", 'i', Description = "Options will be asked as prompt when this option used.")] 18 | public bool Interactive { get; set; } 19 | 20 | protected readonly RunnableProjectsProvider runnableProjectsProvider; 21 | protected readonly Platform platform; 22 | 23 | public LogsCommand(RunnableProjectsProvider runnableProjectsProvider, Platform platform) 24 | { 25 | this.runnableProjectsProvider = runnableProjectsProvider; 26 | this.platform = platform; 27 | } 28 | 29 | public async ValueTask ExecuteAsync(IConsole console) 30 | { 31 | if (string.IsNullOrEmpty(WorkingDirectory)) 32 | { 33 | WorkingDirectory = Directory.GetCurrentDirectory(); 34 | } 35 | 36 | var csprojs = await AnsiConsole.Status() 37 | .StartAsync("Looking for projects...", async ctx => 38 | { 39 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 40 | 41 | await Task.Yield(); 42 | 43 | var projects = runnableProjectsProvider.GetRunnableProjects(WorkingDirectory); 44 | 45 | AnsiConsole.MarkupLine($"[green]{projects.Length}[/] project files found."); 46 | 47 | return projects; 48 | }); 49 | 50 | if (string.IsNullOrEmpty(ProjectName)) 51 | { 52 | if (Interactive) 53 | { 54 | await console.Output.WriteLineAsync($"\n"); 55 | ProjectName = AnsiConsole.Prompt( 56 | new SelectionPrompt() 57 | .Title("Choose a [mediumpurple2]project[/] to open logs?") 58 | .PageSize(12) 59 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 60 | .MoreChoicesText("[grey](Move up and down to reveal more rules)[/]") 61 | .AddChoices(csprojs.Select(s => s.Name))); 62 | } 63 | else 64 | { 65 | await console.Output.WriteLineAsync("You have to pass a project name.\n"); 66 | await console.Output.WriteLineAsync("\n\tUsage:"); 67 | await console.Output.WriteLineAsync("\tlogs -p "); 68 | await console.Output.WriteLineAsync("\nAvailable project names:\n\n\t - " + 69 | string.Join("\n\t - ", csprojs.Select(x => x.Name.Split(Path.DirectorySeparatorChar).Last()))); 70 | return; 71 | } 72 | } 73 | 74 | var selectedCsproj = csprojs.FirstOrDefault(x => x.FullName.Contains(ProjectName)); 75 | 76 | if (selectedCsproj == null) 77 | { 78 | await console.Output.WriteLineAsync($"No project found with the name '{ProjectName}'"); 79 | return; 80 | } 81 | 82 | var dir = Path.GetDirectoryName(selectedCsproj.FullName)!; 83 | var logsDir = Path.Combine(dir, "Logs"); 84 | if (Directory.Exists(logsDir)) 85 | { 86 | var filePath = Path.Combine(logsDir, "logs.txt"); 87 | if (File.Exists(filePath)) 88 | { 89 | platform.Open(filePath); 90 | } 91 | else 92 | { 93 | platform.Open(logsDir); 94 | } 95 | } 96 | else 97 | { 98 | await console.Output.WriteLineAsync("No logs folder found for project.\nOpening project folder..."); 99 | 100 | platform.Open(dir); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/MigrateCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Environments; 3 | using AbpDevTools.LocalConfigurations; 4 | using AbpDevTools.Notifications; 5 | using AbpDevTools.Services; 6 | using CliFx.Infrastructure; 7 | using Spectre.Console; 8 | using System.Diagnostics; 9 | using System.Text; 10 | 11 | namespace AbpDevTools.Commands; 12 | 13 | [Command("migrate", Description = "Runs all .DbMigrator projects in folder recursively.")] 14 | public class MigrateCommand : ICommand 15 | { 16 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 17 | public string? WorkingDirectory { get; set; } 18 | 19 | [CommandOption("no-build", Description = "Skipts build before running. Passes '--no-build' parameter to dotnet run.")] 20 | public bool NoBuild { get; set; } 21 | 22 | [CommandOption("env", 'e', Description = "Uses the virtual environment for this process. Use 'abpdev env config' command to see/manage environments.")] 23 | public string? EnvironmentName { get; set; } 24 | 25 | protected readonly List runningProjects = new(); 26 | 27 | protected IConsole? console; 28 | 29 | protected readonly INotificationManager notificationManager; 30 | protected readonly IProcessEnvironmentManager environmentManager; 31 | protected readonly ToolsConfiguration toolsConfiguration; 32 | protected readonly LocalConfigurationManager localConfigurationManager; 33 | protected readonly RunnableProjectsProvider runnableProjectsProvider; 34 | 35 | public MigrateCommand(INotificationManager notificationManager, 36 | IProcessEnvironmentManager environmentManager, 37 | ToolsConfiguration toolsConfiguration, 38 | LocalConfigurationManager localConfigurationManager, 39 | RunnableProjectsProvider runnableProjectsProvider) 40 | { 41 | this.notificationManager = notificationManager; 42 | this.environmentManager = environmentManager; 43 | this.toolsConfiguration = toolsConfiguration; 44 | this.localConfigurationManager = localConfigurationManager; 45 | this.runnableProjectsProvider = runnableProjectsProvider; 46 | } 47 | 48 | public async ValueTask ExecuteAsync(IConsole console) 49 | { 50 | this.console = console; 51 | if (string.IsNullOrEmpty(WorkingDirectory)) 52 | { 53 | WorkingDirectory = Directory.GetCurrentDirectory(); 54 | } 55 | 56 | var dbMigrators = Directory.EnumerateFiles(WorkingDirectory, "*.csproj", SearchOption.AllDirectories) 57 | .Where(IsDbMigrator) 58 | .Select(x => new FileInfo(x)) 59 | .ToList(); 60 | 61 | var cancellationToken = console.RegisterCancellationHandler(); 62 | 63 | if (dbMigrators.Count == 0) 64 | { 65 | await console.Output.WriteLineAsync($"No migrator(s) found in this folder. Migration not applied."); 66 | await RunParameterMigrationFallbackAsync(); 67 | return; 68 | } 69 | 70 | await console.Output.WriteLineAsync($"{dbMigrators.Count} db migrator(s) found."); 71 | 72 | var commandPostFix = NoBuild ? " --no-build" : string.Empty; 73 | 74 | foreach (var dbMigrator in dbMigrators) 75 | { 76 | var tools = toolsConfiguration.GetOptions(); 77 | var startInfo = new ProcessStartInfo(tools["dotnet"], $"run --project \"{dbMigrator.FullName}\"" + commandPostFix) 78 | { 79 | WorkingDirectory = Path.GetDirectoryName(dbMigrator.FullName), 80 | RedirectStandardOutput = true, 81 | }; 82 | 83 | localConfigurationManager.ApplyLocalEnvironmentForProcess(dbMigrator.FullName, startInfo); 84 | 85 | if (!string.IsNullOrEmpty(EnvironmentName)) 86 | { 87 | environmentManager.SetEnvironmentForProcess(EnvironmentName, startInfo); 88 | } 89 | 90 | var process = Process.Start(startInfo)!; 91 | 92 | runningProjects.Add(new RunningProjectItem 93 | { 94 | Name = dbMigrator.Name, 95 | Process = process, 96 | Status = "Running..." 97 | }); 98 | } 99 | 100 | await console.Output.WriteAsync("Waiting for db migrators to finish..."); 101 | cancellationToken.Register(KillRunningProcesses); 102 | 103 | await RenderStatusAsync(); 104 | 105 | if (!cancellationToken.IsCancellationRequested) 106 | { 107 | await console.Output.WriteLineAsync("Migrations finished."); 108 | await notificationManager.SendAsync("Migration Completed", $"Complated migrations in {WorkingDirectory}"); 109 | } 110 | 111 | KillRunningProcesses(); 112 | } 113 | 114 | protected async Task RunParameterMigrationFallbackAsync() 115 | { 116 | if (!AnsiConsole.Confirm("Do you want to run any of projects in this folder with '--migrate-database' parameter?")) 117 | { 118 | return; 119 | } 120 | 121 | FileInfo[] csprojs = await AnsiConsole.Status() 122 | .StartAsync("Looking for projects", async ctx => 123 | { 124 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 125 | 126 | await Task.Yield(); 127 | 128 | return runnableProjectsProvider.GetRunnableProjects(WorkingDirectory); 129 | }); 130 | 131 | if (csprojs.Length <= 0) 132 | { 133 | await console.Output.WriteLineAsync("No project found to run."); 134 | return; 135 | } 136 | 137 | if(csprojs.Length == 1) 138 | { 139 | await console.Output.WriteLineAsync("Only one project found. Running it with '--migrate-database' parameter."); 140 | await RunProjectWithMigrateDatabaseAsync(csprojs[0]); 141 | } 142 | else{ 143 | 144 | var selectedProject = AnsiConsole.Prompt( 145 | new SelectionPrompt() 146 | .Title("Select a project to run with '--migrate-database' parameter") 147 | .PageSize(10) 148 | .AddChoices(csprojs) 149 | ); 150 | 151 | await RunProjectWithMigrateDatabaseAsync(selectedProject); 152 | } 153 | 154 | await RenderStatusAsync(); 155 | } 156 | 157 | protected Task RunProjectWithMigrateDatabaseAsync(FileInfo project) 158 | { 159 | var tools = toolsConfiguration.GetOptions(); 160 | var startInfo = new ProcessStartInfo(tools["dotnet"], $"run --project \"{project.FullName}\" -- --migrate-database") 161 | { 162 | WorkingDirectory = Path.GetDirectoryName(project.FullName), 163 | RedirectStandardOutput = true, 164 | }; 165 | 166 | localConfigurationManager.ApplyLocalEnvironmentForProcess(project.FullName, startInfo); 167 | 168 | if (!string.IsNullOrEmpty(EnvironmentName)) 169 | { 170 | environmentManager.SetEnvironmentForProcess(EnvironmentName, startInfo); 171 | } 172 | 173 | var process = Process.Start(startInfo)!; 174 | 175 | runningProjects.Add(new RunningProjectItem 176 | { 177 | Name = project.Name, 178 | Process = process, 179 | Status = "Running..." 180 | }); 181 | 182 | return Task.CompletedTask; 183 | } 184 | 185 | private bool IsDbMigrator(string file) 186 | { 187 | if (!file.EndsWith("Migrator.csproj", StringComparison.InvariantCultureIgnoreCase)) 188 | { 189 | return false; 190 | } 191 | 192 | using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read); 193 | using var streamReader = new StreamReader(fileStream, Encoding.UTF8, true); 194 | 195 | while (!streamReader.EndOfStream) 196 | { 197 | var line = streamReader.ReadLine(); 198 | 199 | if (line == null) 200 | { 201 | continue; 202 | } 203 | 204 | if (line.Contains("Exe")) 205 | { 206 | return true; 207 | } 208 | 209 | if (line.Contains("")) 210 | { 211 | break; 212 | } 213 | } 214 | 215 | return false; 216 | } 217 | 218 | private async Task RenderStatusAsync() 219 | { 220 | var table = new Table().Border(TableBorder.Ascii); 221 | 222 | AnsiConsole.WriteLine(Environment.NewLine); 223 | await AnsiConsole.Live(table) 224 | .StartAsync(async ctx => 225 | { 226 | table.AddColumn("Project"); 227 | table.AddColumn("Status"); 228 | 229 | UpdateTable(table); 230 | ctx.UpdateTarget(table); 231 | 232 | foreach (var runningProject in runningProjects) 233 | { 234 | runningProject.Process!.OutputDataReceived += (sender, args) => 235 | { 236 | if (args?.Data != null && args.Data.Length < 90) 237 | { 238 | runningProject.Status = args.Data[args.Data.IndexOf(']')..].Replace('[', '\0').Replace(']', '\0'); 239 | UpdateTable(table); 240 | ctx.UpdateTarget(table); 241 | } 242 | }; 243 | runningProject.Process.BeginOutputReadLine(); 244 | } 245 | 246 | await Task.WhenAll(runningProjects.Select(x => x.Process!.WaitForExitAsync())); 247 | }); 248 | } 249 | 250 | private void UpdateTable(Table table) 251 | { 252 | table.Rows.Clear(); 253 | foreach (var runningProject in runningProjects) 254 | { 255 | table.AddRow( 256 | runningProject.Name!, 257 | runningProject.Status!); 258 | } 259 | } 260 | 261 | protected void KillRunningProcesses() 262 | { 263 | console!.Output.WriteLine($"- Killing running {runningProjects.Count} processes..."); 264 | foreach (var project in runningProjects) 265 | { 266 | project.Process?.Kill(entireProcessTree: true); 267 | 268 | project.Process?.WaitForExit(); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/Migrations/AddMigrationCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Services; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | using System.Diagnostics; 5 | 6 | namespace AbpDevTools.Commands.Migrations; 7 | 8 | [Command("migrations add", Description = "Adds migration with specified name in EntityFrameworkCore project(s). Used to add bulk migrations to multiple project at the same time.")] 9 | public class AddMigrationCommand : MigrationsCommandBase, ICommand 10 | { 11 | [CommandOption("name", 'n', Description = "Name of the migration.")] 12 | public string Name { get; set; } = "Initial"; 13 | 14 | public List RunningProgresses { get; } = new(); 15 | 16 | 17 | public AddMigrationCommand(EntityFrameworkCoreProjectsProvider entityFrameworkCoreProjectsProvider) : base(entityFrameworkCoreProjectsProvider) 18 | { 19 | } 20 | 21 | public override async ValueTask ExecuteAsync(IConsole console) 22 | { 23 | await base.ExecuteAsync(console); 24 | 25 | var cancellationToken = console.RegisterCancellationHandler(); 26 | 27 | var projectFiles = await ChooseProjectsAsync(); 28 | 29 | if (projectFiles.Length == 0) 30 | { 31 | await console.Output.WriteLineAsync("No EF Core projects found. No migrations to add."); 32 | return; 33 | } 34 | 35 | foreach (var project in projectFiles) 36 | { 37 | var arguments = $"migrations add {Name} --project {project.FullName}"; 38 | var process = new Process 39 | { 40 | StartInfo = new ProcessStartInfo("dotnet-ef", arguments) 41 | { 42 | WorkingDirectory = WorkingDirectory, 43 | RedirectStandardOutput = true, 44 | RedirectStandardError = true, 45 | } 46 | }; 47 | 48 | var projectName = Path.GetFileNameWithoutExtension(project.Name); 49 | RunningProgresses.Add(new RunningProgressItem(process!, projectName, "Running...")); 50 | } 51 | 52 | cancellationToken.Register(KillAllProcesses); 53 | await RenderProgressesAsync(cancellationToken); 54 | } 55 | 56 | private async Task RenderProgressesAsync(CancellationToken cancellationToken) 57 | { 58 | var table = new Table().Border(TableBorder.Ascii) 59 | .AddColumn("Project") 60 | .AddColumn("Status") 61 | .AddColumn("Result"); 62 | 63 | await AnsiConsole.Live(table).StartAsync(async ctx => 64 | { 65 | while (!cancellationToken.IsCancellationRequested) 66 | { 67 | RenderProgresses(table); 68 | await Task.Delay(500, cancellationToken); 69 | 70 | ctx.Refresh(); 71 | 72 | if (RunningProgresses.All(p => !p.IsRunning)) 73 | { 74 | break; 75 | } 76 | } 77 | 78 | await Task.WhenAll(RunningProgresses.Select(p => p.Process.WaitForExitAsync())); 79 | 80 | RenderProgresses(table); 81 | ctx.Refresh(); 82 | }); 83 | } 84 | 85 | private async void RenderProgresses(Table table) 86 | { 87 | table.Rows.Clear(); 88 | foreach (var progress in RunningProgresses) 89 | { 90 | if (progress.Process.HasExited) 91 | { 92 | progress.Status = progress.ExitCode == 0 ? "Completed!" 93 | : $"Failed! ( Exit Code: {progress.ExitCode})"; 94 | } 95 | 96 | table.AddRow(progress.Name, progress.Status, progress.LastLine); 97 | } 98 | } 99 | 100 | protected void KillAllProcesses() 101 | { 102 | foreach (var progress in RunningProgresses) 103 | { 104 | if (progress.IsRunning) 105 | { 106 | progress.Process.Kill(entireProcessTree: true); 107 | } 108 | } 109 | } 110 | } 111 | 112 | public class RunningProgressItem 113 | { 114 | public RunningProgressItem(Process process, string name, string initialStatus) 115 | { 116 | Process = process; 117 | Name = name; 118 | Status = initialStatus; 119 | process.OutputDataReceived += OutputReceived; 120 | process.Start(); 121 | process.BeginOutputReadLine(); 122 | } 123 | 124 | private void OutputReceived(object sender, DataReceivedEventArgs e) 125 | { 126 | if (e.Data != null) 127 | { 128 | Output += e.Data + Environment.NewLine; 129 | LastLine = e.Data; 130 | } 131 | } 132 | 133 | public string Name { get; set; } 134 | 135 | public Process Process { get; set; } 136 | 137 | public string Status { get; set; } = string.Empty; 138 | 139 | public string Output { get; set; } = string.Empty; 140 | 141 | public string LastLine { get; set; } = string.Empty; 142 | 143 | public int ExitCode => Process.HasExited ? Process.ExitCode : 0; 144 | 145 | public bool IsRunning => Process.HasExited == false; 146 | } 147 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/Migrations/ClearMigrationsCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Services; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace AbpDevTools.Commands.Migrations; 11 | 12 | [Command("migrations clear", Description = "Clears 'Migrations' folders from EntityFrameworkCore projects.")] 13 | public class ClearMigrationsCommand : MigrationsCommandBase, ICommand 14 | { 15 | public ClearMigrationsCommand(EntityFrameworkCoreProjectsProvider entityFrameworkCoreProjectsProvider) : base(entityFrameworkCoreProjectsProvider) 16 | { 17 | } 18 | 19 | public override async ValueTask ExecuteAsync(IConsole console) 20 | { 21 | await base.ExecuteAsync(console); 22 | 23 | var projectFiles = await ChooseProjectsAsync(); 24 | 25 | if (projectFiles.Length == 0) 26 | { 27 | await console.Output.WriteLineAsync("No EF Core projects found. No migrations to add."); 28 | return; 29 | } 30 | 31 | await AnsiConsole.Status().StartAsync("Clearing migrations...", async ctx => 32 | { 33 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 34 | 35 | foreach (var project in projectFiles) 36 | { 37 | var migrationsFolder = Path.Combine(Path.GetDirectoryName(project.FullName)!, "Migrations"); 38 | if (Directory.Exists(migrationsFolder)) 39 | { 40 | Directory.Delete(migrationsFolder, true); 41 | AnsiConsole.MarkupLine($"[green]Cleared[/] migrations of [bold]{Path.GetFileNameWithoutExtension(project.Name)}[/]"); 42 | } 43 | else 44 | { 45 | AnsiConsole.MarkupLine($"No migrations found in [bold]{Path.GetFileNameWithoutExtension(project.Name)}[/]"); 46 | } 47 | } 48 | }); 49 | 50 | await console.Output.WriteLineAsync("Migrations cleared."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/Migrations/MigrationsCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CliFx.Infrastructure; 3 | 4 | namespace AbpDevTools.Commands.Migrations; 5 | 6 | [Command("migrations", Description = "Manages EntityFrameworkCore migrations in multiple projects.")] 7 | public class MigrationsCommand : ICommand 8 | { 9 | public async ValueTask ExecuteAsync(IConsole console) 10 | { 11 | await console.Output.WriteLineAsync("Specify a subcommand.Available subcommands:\n\n"); 12 | 13 | await console.Output.WriteLineAsync("\tabpdev add"); 14 | await console.Output.WriteLineAsync("\tabpdev clear"); 15 | 16 | await console.Output.WriteLineAsync("\n\nExample:"); 17 | await console.Output.WriteLineAsync("\tabpdev migrations add --name Initial"); 18 | await console.Output.WriteLineAsync("\tabpdev migrations clear"); 19 | 20 | await console.Output.WriteLineAsync("\n\nGet Help:"); 21 | await console.Output.WriteLineAsync("\tabpdev migrations --help"); 22 | await console.Output.WriteLineAsync("\tabpdev migrations add --help"); 23 | 24 | return; 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/Migrations/MigrationsCommandBase.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Services; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | 5 | namespace AbpDevTools.Commands.Migrations; 6 | public abstract class MigrationsCommandBase : ICommand 7 | { 8 | [CommandOption("all", 'a', Description = "Run the command for all the EF Core projects.")] 9 | public bool RunAll { get; set; } 10 | 11 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 12 | public string? WorkingDirectory { get; set; } 13 | 14 | [CommandOption("projects", 'p', Description = "(Array) Names or part of names of projects will be ran.")] 15 | public string[] Projects { get; set; } = Array.Empty(); 16 | 17 | protected readonly EntityFrameworkCoreProjectsProvider entityFrameworkCoreProjectsProvider; 18 | 19 | protected MigrationsCommandBase(EntityFrameworkCoreProjectsProvider entityFrameworkCoreProjectsProvider) 20 | { 21 | this.entityFrameworkCoreProjectsProvider = entityFrameworkCoreProjectsProvider; 22 | } 23 | 24 | public virtual ValueTask ExecuteAsync(IConsole console) 25 | { 26 | if (string.IsNullOrEmpty(WorkingDirectory)) 27 | { 28 | WorkingDirectory = Directory.GetCurrentDirectory(); 29 | } 30 | 31 | return default; 32 | } 33 | 34 | protected async Task ChooseProjectsAsync() 35 | { 36 | var projectFiles = GetEfCoreProjects(); 37 | 38 | if (projectFiles.Length == 0) 39 | { 40 | return Array.Empty(); 41 | } 42 | 43 | if (Projects.Length > 0) 44 | { 45 | projectFiles = projectFiles.Where(pf => Projects.Any(a => pf.FullName.Contains(a))).ToArray(); 46 | } 47 | else if (!RunAll && projectFiles.Length > 0) 48 | { 49 | var chosenProjects = AnsiConsole.Prompt(new MultiSelectionPrompt() 50 | .Title("Choose project to create migrations.") 51 | .Required(true) 52 | .PageSize(12) 53 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 54 | .MoreChoicesText("[grey](Move up and down to reveal more projects)[/]") 55 | .InstructionsText( 56 | "[grey](Press [mediumpurple2][/] to toggle a project, " + 57 | "[green][/] to accept)[/]") 58 | .AddChoices(projectFiles 59 | .Select(p => Path.GetDirectoryName(p.FullName.Replace(WorkingDirectory, string.Empty)).Trim('\\')) 60 | .ToArray()) 61 | ); 62 | 63 | projectFiles = projectFiles.Where(p => chosenProjects.Any(cp => p.FullName.Contains(cp))).ToArray(); 64 | } 65 | 66 | return projectFiles; 67 | } 68 | 69 | FileInfo[] GetEfCoreProjects() 70 | { 71 | return entityFrameworkCoreProjectsProvider.GetEfCoreProjects(WorkingDirectory!); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/ReplaceCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace AbpDevTools.Commands; 7 | 8 | [Command("replace", Description = "Runs file replacement according to configuration.")] 9 | public class ReplaceCommand : ICommand 10 | { 11 | [CommandOption("path", 'p', Description = "Working directory of the command. Probably solution directory. Default: . (CurrentDirectory) ")] 12 | public string? WorkingDirectory { get; set; } 13 | 14 | [CommandParameter(0, IsRequired = false, Description = "If you execute single option from config, you can pass the name or pass 'all' to execute all of them")] 15 | public string? ReplacementConfigName { get; set; } 16 | 17 | [CommandOption("interactive", 'i', Description = "Interactive Mode. It'll ask prompt to pick one config.")] 18 | public bool InteractiveMode { get; set; } 19 | 20 | private readonly ReplacementConfiguration replacementConfiguration; 21 | 22 | public ReplaceCommand(ReplacementConfiguration replacementConfiguration) 23 | { 24 | this.replacementConfiguration = replacementConfiguration; 25 | } 26 | 27 | public async ValueTask ExecuteAsync(IConsole console) 28 | { 29 | WorkingDirectory ??= Directory.GetCurrentDirectory(); 30 | 31 | var options = replacementConfiguration.GetOptions(); 32 | 33 | if (string.IsNullOrEmpty(ReplacementConfigName)) 34 | { 35 | if (InteractiveMode) 36 | { 37 | await console.Output.WriteLineAsync($"\n"); 38 | ReplacementConfigName = AnsiConsole.Prompt( 39 | new SelectionPrompt() 40 | .Title("Choose a [mediumpurple2]rule[/] to execute?") 41 | .PageSize(12) 42 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 43 | .MoreChoicesText("[grey](Move up and down to reveal more rules)[/]") 44 | .AddChoices(options.Keys)); 45 | } 46 | else 47 | { 48 | console.Output.WriteLine("You should specify a execution rule name.\n"); 49 | console.Output.WriteLine("\tUse 'replace ' to execute a rule"); 50 | console.Output.WriteLine("\tUse 'replace all' to execute a rules"); 51 | console.Output.WriteLine("\tUse 'replace config' to manage rules.\n\n"); 52 | console.Output.WriteLine("Available execution rules:\n\n\t - " + string.Join("\n\t - ", options.Keys)); 53 | return; 54 | } 55 | } 56 | 57 | if (ReplacementConfigName.Equals("all", StringComparison.InvariantCultureIgnoreCase)) 58 | { 59 | foreach (var item in options) 60 | { 61 | await ExecuteConfigAsync(item.Key, item.Value); 62 | } 63 | 64 | return; 65 | } 66 | 67 | if (!string.IsNullOrEmpty(ReplacementConfigName)) 68 | { 69 | if (!options.TryGetValue(ReplacementConfigName, out var option)) 70 | { 71 | console.ForegroundColor = ConsoleColor.Red; 72 | await console.Error.WriteLineAsync($"No replacement config found with name '{ReplacementConfigName}'"); 73 | console.ResetColor(); 74 | 75 | await console.Output.WriteLineAsync("Available configurations: " + string.Join(',', options.Keys)); 76 | await console.Output.WriteLineAsync("Check existing configurations with 'abpdev config' command."); 77 | return; 78 | } 79 | await ExecuteConfigAsync(ReplacementConfigName, option); 80 | return; 81 | } 82 | } 83 | 84 | protected virtual async ValueTask ExecuteConfigAsync(string configurationName, ReplacementOption option) 85 | { 86 | await AnsiConsole.Status() 87 | .StartAsync($"Executing...", async ctx => 88 | { 89 | AnsiConsole.MarkupLine($"Executing [blue]'{configurationName}'[/] replacement configuration..."); 90 | 91 | await Task.Yield(); 92 | 93 | ctx.Status($"[blue]{option.FilePattern}[/] file pattern executing."); 94 | var files = Directory.EnumerateFiles(WorkingDirectory!, "*.*", SearchOption.AllDirectories) 95 | .Where(x => Regex.IsMatch(x, option.FilePattern)) 96 | .ToList(); 97 | 98 | ctx.Status($"[green]{files.Count}[/] file(s) found with pattern."); 99 | 100 | int affectedFileCount = 0; 101 | foreach (var file in files) 102 | { 103 | var text = File.ReadAllText(file); 104 | 105 | if (text.Contains(option.Find)) 106 | { 107 | File.WriteAllText(file, text.Replace(option.Find, option.Replace)); 108 | ctx.Status($"{file} updated."); 109 | affectedFileCount++; 110 | } 111 | } 112 | AnsiConsole.MarkupLine($"Totally [green]{affectedFileCount}[/] files updated."); 113 | }); 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/RunCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Environments; 3 | using AbpDevTools.LocalConfigurations; 4 | using AbpDevTools.Notifications; 5 | using AbpDevTools.Services; 6 | using CliFx.Infrastructure; 7 | using Spectre.Console; 8 | using System.Diagnostics; 9 | 10 | namespace AbpDevTools.Commands; 11 | 12 | [Command("run", Description = "Run all the required applications")] 13 | public partial class RunCommand : ICommand 14 | { 15 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run build. Probably project or solution directory path goes here. Default: . (Current Directory)")] 16 | public string? WorkingDirectory { get; set; } 17 | 18 | [CommandOption("watch", 'w', Description = "Watch mode")] 19 | public bool Watch { get; set; } 20 | 21 | [CommandOption("skip-migrate", Description = "Skips migration and runs projects directly.")] 22 | public bool SkipMigration { get; set; } 23 | 24 | [CommandOption("all", 'a', Description = "Projects to run will not be asked as prompt. All of them will run.")] 25 | public bool RunAll { get; set; } 26 | 27 | [CommandOption("no-build", Description = "Skips build before running. Passes '--no-build' parameter to dotnet run.")] 28 | public bool NoBuild { get; set; } 29 | 30 | [CommandOption("install-libs", 'i', Description = "Runs 'abp install-libs' command while running the project simultaneously.")] 31 | public bool InstallLibs { get; set; } 32 | 33 | [CommandOption("graphBuild", 'g', Description = "Uses /graphBuild while running the applications. So no need building before running. But it may cause some performance.")] 34 | public bool GraphBuild { get; set; } 35 | 36 | [CommandOption("projects", 'p', Description = "(Array) Names or part of names of projects will be ran.")] 37 | public string[] Projects { get; set; } = Array.Empty(); 38 | 39 | [CommandOption("configuration", 'c')] 40 | public string? Configuration { get; set; } 41 | 42 | [CommandOption("env", 'e', Description = "Uses the virtual environment for this process. Use 'abpdev env config' command to see/manage environments.")] 43 | public string? EnvironmentName { get; set; } 44 | 45 | [CommandOption("retry", 'r', Description = "Retries running again when application exits.")] 46 | public bool Retry { get; set; } 47 | 48 | [CommandOption("verbose", 'v', Description = "Shows verbose output from the projects.")] 49 | public bool Verbose { get; set; } 50 | 51 | [CommandOption("yml", Description = "Path to the yml file to be used for running the project.")] 52 | public string? YmlPath { get; set; } 53 | 54 | protected IConsole? console; 55 | 56 | protected readonly List runningProjects = new(); 57 | 58 | protected readonly INotificationManager notificationManager; 59 | protected readonly MigrateCommand migrateCommand; 60 | protected readonly IProcessEnvironmentManager environmentManager; 61 | protected readonly UpdateCheckCommand updateCheckCommand; 62 | protected readonly RunnableProjectsProvider runnableProjectsProvider; 63 | protected readonly ToolsConfiguration toolsConfiguration; 64 | protected readonly FileExplorer fileExplorer; 65 | private readonly LocalConfigurationManager localConfigurationManager; 66 | 67 | public RunCommand( 68 | INotificationManager notificationManager, 69 | MigrateCommand migrateCommand, 70 | IProcessEnvironmentManager environmentManager, 71 | UpdateCheckCommand updateCheckCommand, 72 | RunnableProjectsProvider runnableProjectsProvider, 73 | ToolsConfiguration toolsConfiguration, 74 | FileExplorer fileExplorer, 75 | LocalConfigurationManager localConfigurationManager) 76 | { 77 | this.notificationManager = notificationManager; 78 | this.migrateCommand = migrateCommand; 79 | this.environmentManager = environmentManager; 80 | this.updateCheckCommand = updateCheckCommand; 81 | this.runnableProjectsProvider = runnableProjectsProvider; 82 | this.toolsConfiguration = toolsConfiguration; 83 | this.fileExplorer = fileExplorer; 84 | this.localConfigurationManager = localConfigurationManager; 85 | } 86 | 87 | public async ValueTask ExecuteAsync(IConsole console) 88 | { 89 | this.console = console; 90 | if (string.IsNullOrEmpty(WorkingDirectory)) 91 | { 92 | WorkingDirectory = Directory.GetCurrentDirectory(); 93 | } 94 | 95 | if (string.IsNullOrEmpty(YmlPath)) 96 | { 97 | YmlPath = Path.Combine(WorkingDirectory, "abpdev.yml"); 98 | } 99 | 100 | var cancellationToken = console.RegisterCancellationHandler(); 101 | 102 | if (localConfigurationManager.TryLoad(YmlPath!, out var localRootConfig, FileSearchDirection.OnlyCurrent)) 103 | { 104 | console.Output.WriteLine($"Loaded YAML configuration from '{YmlPath}' with environment '{localRootConfig?.Environment?.Name ?? "Default"}'."); 105 | } 106 | 107 | FileInfo[] csprojs = await AnsiConsole.Status() 108 | .StartAsync("Looking for projects", async ctx => 109 | { 110 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 111 | 112 | await Task.Yield(); 113 | 114 | return runnableProjectsProvider.GetRunnableProjects(WorkingDirectory); 115 | }); 116 | 117 | await console.Output.WriteLineAsync($"{csprojs.Length} csproj file(s) found."); 118 | 119 | if (!SkipMigration && localRootConfig?.Run?.SkipMigrate != true) 120 | { 121 | migrateCommand.WorkingDirectory = this.WorkingDirectory; 122 | migrateCommand.NoBuild = this.NoBuild; 123 | migrateCommand.EnvironmentName = this.EnvironmentName; 124 | 125 | await migrateCommand.ExecuteAsync(console); 126 | } 127 | 128 | await console.Output.WriteLineAsync("Starting projects..."); 129 | 130 | var projectFiles = csprojs.Where(x => !x.Name.Contains(".DbMigrator")).ToArray(); 131 | 132 | if (!RunAll && projectFiles.Length > 1) 133 | { 134 | await console.Output.WriteLineAsync($"\n"); 135 | 136 | ApplyLocalProjects(localRootConfig); 137 | 138 | if (Projects.Length == 0) 139 | { 140 | var choosedProjects = AnsiConsole.Prompt( 141 | new MultiSelectionPrompt() 142 | .Title("Choose [mediumpurple2]projects[/] to run.") 143 | .Required(true) 144 | .PageSize(12) 145 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 146 | .MoreChoicesText("[grey](Move up and down to reveal more projects)[/]") 147 | .InstructionsText( 148 | "[grey](Press [mediumpurple2][/] to toggle a project, " + 149 | "[green][/] to accept)[/]") 150 | .AddChoices(projectFiles.Select(s => s.Name))); 151 | 152 | projectFiles = projectFiles.Where(x => choosedProjects.Contains(x.Name)).ToArray(); 153 | } 154 | else 155 | { 156 | projectFiles = projectFiles.Where(x => Projects.Any(y => x.FullName.Contains(y, StringComparison.InvariantCultureIgnoreCase))).ToArray(); 157 | } 158 | } 159 | 160 | foreach (var csproj in projectFiles) 161 | { 162 | localConfigurationManager.TryLoad(csproj.FullName, out var localConfiguration); 163 | 164 | var commandPrefix = BuildCommandPrefix(localConfiguration?.Run?.Watch); 165 | var commandSuffix = BuildCommandSuffix( 166 | localConfiguration?.Run?.NoBuild, 167 | localConfiguration?.Run?.GraphBuild, 168 | localConfiguration?.Run?.Configuration); 169 | 170 | var tools = toolsConfiguration.GetOptions(); 171 | var startInfo = new ProcessStartInfo(tools["dotnet"], commandPrefix + $"run --project \"{csproj.FullName}\"" + commandSuffix) 172 | { 173 | WorkingDirectory = Path.GetDirectoryName(csproj.FullName), 174 | UseShellExecute = false, 175 | RedirectStandardOutput = true, 176 | RedirectStandardError = true, 177 | }; 178 | 179 | localConfigurationManager.ApplyLocalEnvironmentForProcess(csproj.FullName, startInfo, localConfiguration); 180 | 181 | if (!string.IsNullOrEmpty(EnvironmentName)) 182 | { 183 | environmentManager.SetEnvironmentForProcess(EnvironmentName, startInfo); 184 | } 185 | 186 | runningProjects.Add( 187 | new RunningCsProjItem( 188 | csproj.Name, 189 | Process.Start(startInfo)!, 190 | verbose: Verbose 191 | ) 192 | ); 193 | 194 | if (InstallLibs) 195 | { 196 | var wwwRootLibs = Path.Combine(Path.GetDirectoryName(csproj.FullName)!, "wwwroot/libs"); 197 | if (!Directory.Exists(wwwRootLibs)) 198 | { 199 | Directory.CreateDirectory(wwwRootLibs); 200 | } 201 | 202 | if (!Directory.EnumerateFiles(wwwRootLibs).Any()) 203 | { 204 | File.WriteAllText(Path.Combine(wwwRootLibs, "abplibs.installing"), string.Empty); 205 | } 206 | 207 | var installLibsRunninItem = new RunningInstallLibsItem( 208 | csproj.Name.Replace(".csproj", " install-libs"), 209 | Process.Start(new ProcessStartInfo(tools["abp"], "install-libs") 210 | { 211 | WorkingDirectory = Path.GetDirectoryName(csproj.FullName), 212 | UseShellExecute = false, 213 | RedirectStandardOutput = true, 214 | })! 215 | ); 216 | 217 | runningProjects.Add(installLibsRunninItem); 218 | } 219 | } 220 | 221 | cancellationToken.Register(KillRunningProcesses); 222 | 223 | await RenderProcesses(cancellationToken); 224 | 225 | await updateCheckCommand.SoftCheckAsync(console); 226 | } 227 | 228 | private void ApplyLocalProjects(LocalConfiguration? localConfiguration) 229 | { 230 | if(localConfiguration is not null) 231 | { 232 | if (Projects.Length == 0 && localConfiguration?.Run?.Projects.Length > 0) 233 | { 234 | Projects = localConfiguration.Run.Projects; 235 | } 236 | } 237 | } 238 | 239 | private string BuildCommandSuffix(bool? noBuild = null, bool? graphBuild = null, string? configuration = null) 240 | { 241 | var commandSuffix = (NoBuild || noBuild == true) ? " --no-build" : string.Empty; 242 | 243 | if (GraphBuild || graphBuild == true) 244 | { 245 | commandSuffix += " /graphBuild"; 246 | } 247 | 248 | if (configuration != null) 249 | { 250 | commandSuffix += $" --configuration {configuration}"; 251 | } 252 | else if (!string.IsNullOrEmpty(Configuration)) 253 | { 254 | commandSuffix += $" --configuration {Configuration}"; 255 | } 256 | 257 | return commandSuffix; 258 | } 259 | 260 | private string BuildCommandPrefix(bool? watchOverride) 261 | { 262 | if (watchOverride is not null) 263 | { 264 | return watchOverride.Value ? "watch " : string.Empty; 265 | } 266 | return Watch ? "watch " : string.Empty; 267 | } 268 | 269 | private async Task RenderProcesses(CancellationToken cancellationToken) 270 | { 271 | var table = new Table().Ascii2Border(); 272 | 273 | await AnsiConsole.Live(table) 274 | .StartAsync(async ctx => 275 | { 276 | table.AddColumn("Project").AddColumn("Status"); 277 | 278 | foreach (var project in runningProjects) 279 | { 280 | table.AddRow(project.Name!, project.Status!); 281 | } 282 | ctx.Refresh(); 283 | 284 | while (!cancellationToken.IsCancellationRequested) 285 | { 286 | #if DEBUG 287 | await Task.Delay(100); 288 | #else 289 | await Task.Delay(500); 290 | #endif 291 | table.Rows.Clear(); 292 | 293 | foreach (var project in runningProjects) 294 | { 295 | if (project.IsCompleted) 296 | { 297 | table.AddRow(project.Name!, $"[green]*[/] {project.Status}"); 298 | } 299 | else 300 | { 301 | if (project.Process!.HasExited && !project.Queued) 302 | { 303 | project.Status = $"[red]*[/] Exited({project.Process.ExitCode})"; 304 | 305 | if (Retry) 306 | { 307 | project.Status = $"[orange1]*[/] Exited({project.Process.ExitCode})"; 308 | 309 | _ = RestartProject(project, cancellationToken); // fire and forget 310 | } 311 | } 312 | table.AddRow(project.Name!, project.Status!); 313 | } 314 | } 315 | 316 | ctx.Refresh(); 317 | } 318 | }); 319 | } 320 | 321 | private static async Task RestartProject(RunningProjectItem project, CancellationToken cancellationToken = default) 322 | { 323 | project.Queued = true; 324 | await Task.Delay(3100, cancellationToken); 325 | 326 | if (cancellationToken.IsCancellationRequested) 327 | { 328 | return; 329 | } 330 | 331 | project.Status = $"[orange1]*[/] Exited({project.Process!.ExitCode}) (Retrying...)"; 332 | project.Process = Process.Start(project.Process!.StartInfo)!; 333 | project.StartReadingOutput(); 334 | } 335 | 336 | protected void KillRunningProcesses() 337 | { 338 | console!.Output.WriteLine($"- Killing running {runningProjects.Count} processes..."); 339 | foreach (var project in runningProjects) 340 | { 341 | project.Process?.Kill(entireProcessTree: true); 342 | 343 | project.Process?.WaitForExit(); 344 | } 345 | } 346 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/RunningProjectItem.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace AbpDevTools.Commands; 4 | 5 | public class RunningProjectItem 6 | { 7 | public string? Name { get; set; } 8 | public Process? Process { get; set; } 9 | public virtual string? Status { get; set; } 10 | public virtual bool IsCompleted { get; set; } 11 | public virtual bool Queued { get; set; } 12 | public bool Verbose { get; set; } 13 | 14 | public virtual void StartReadingOutput() 15 | { 16 | } 17 | } 18 | 19 | public class RunningCsProjItem : RunningProjectItem 20 | { 21 | public RunningCsProjItem(string name, Process process, string? status = null, bool verbose = false) 22 | { 23 | this.Name = name; 24 | this.Process = process; 25 | this.Status = status ?? "Building..."; 26 | this.Verbose = verbose; 27 | StartReadingOutput(); 28 | } 29 | 30 | public override void StartReadingOutput() 31 | { 32 | Queued = false; 33 | Process!.OutputDataReceived -= OutputReceived; 34 | Process!.OutputDataReceived += OutputReceived; 35 | Process!.BeginOutputReadLine(); 36 | } 37 | 38 | protected virtual void OutputReceived(object sender, DataReceivedEventArgs args) 39 | { 40 | if (!IsCompleted && Verbose) 41 | { 42 | Status = args.Data?.Replace("[", string.Empty).Replace("]", string.Empty) ?? string.Empty; 43 | } 44 | 45 | if (args.Data != null && args.Data.Contains("Now listening on: ")) 46 | { 47 | Status = args.Data[args.Data.IndexOf("Now listening on: ")..]; 48 | Process?.CancelOutputRead(); 49 | IsCompleted = true; 50 | } 51 | 52 | if (args.Data != null && 53 | args.Data.Contains("dotnet watch ") && 54 | args.Data.Contains(" Started")) 55 | { 56 | Status = args.Data; 57 | Process?.CancelOutputRead(); 58 | IsCompleted = true; 59 | } 60 | 61 | if (DateTime.Now - Process?.StartTime > TimeSpan.FromMinutes(5)) 62 | { 63 | Status = "Stale"; 64 | Process!.OutputDataReceived -= OutputReceived; 65 | Process.CancelOutputRead(); 66 | } 67 | } 68 | } 69 | 70 | public class RunningInstallLibsItem : RunningProjectItem 71 | { 72 | public RunningInstallLibsItem(string name, Process process, string? status = null) 73 | { 74 | this.Name = name; 75 | this.Process = process; 76 | this.Status = status ?? "Installing..."; 77 | StartReadingOutput(); 78 | } 79 | 80 | public override void StartReadingOutput() 81 | { 82 | Process!.OutputDataReceived -= OutputReceived; 83 | Process!.OutputDataReceived += OutputReceived; 84 | Process!.BeginOutputReadLine(); 85 | } 86 | 87 | protected virtual void OutputReceived(object sender, DataReceivedEventArgs args) 88 | { 89 | if (args.Data != null && args.Data.Contains("Done in")) 90 | { 91 | Status = "Completed."; 92 | Process!.CancelOutputRead(); 93 | IsCompleted = true; 94 | } 95 | 96 | if (DateTime.Now - Process!.StartTime > TimeSpan.FromMinutes(5)) 97 | { 98 | Status = "Stale"; 99 | Process!.OutputDataReceived -= OutputReceived; 100 | Process!.CancelOutputRead(); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/SwitchToEnvironmentCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AbpDevTools.Environments; 3 | using CliFx.Infrastructure; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("switch-to-env", Description = "Switches to the specified environment.")] 10 | public class SwitchToEnvironmentCommand : ICommand 11 | { 12 | protected readonly IProcessEnvironmentManager processEnvironmentManager; 13 | protected readonly ToolsConfiguration toolsConfiguration; 14 | 15 | [CommandParameter(0, IsRequired = true, Description = "Virtual Environment name to switch in this process")] 16 | public string? EnvironmentName { get; set; } 17 | 18 | public SwitchToEnvironmentCommand(IProcessEnvironmentManager processEnvironmentManager, ToolsConfiguration toolsConfiguration) 19 | { 20 | this.processEnvironmentManager = processEnvironmentManager; 21 | this.toolsConfiguration = toolsConfiguration; 22 | } 23 | 24 | public ValueTask ExecuteAsync(IConsole console) 25 | { 26 | if (string.IsNullOrEmpty(EnvironmentName)) 27 | { 28 | throw new ArgumentException("Environment name can not be null or empty."); 29 | } 30 | 31 | var startInfo = GetStartInfo(); 32 | 33 | processEnvironmentManager.SetEnvironmentForProcess(EnvironmentName, startInfo); 34 | 35 | var process = Process.Start(startInfo)!; 36 | console.Output.WriteAsync($"Switched to {EnvironmentName} on the process (PID: {process.Id} - {process.ProcessName})."); 37 | process.WaitForExit(); 38 | 39 | return default; 40 | } 41 | 42 | private ProcessStartInfo GetStartInfo() 43 | { 44 | var terminal = toolsConfiguration.GetOptions()["terminal"]; 45 | 46 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 47 | { 48 | return new ProcessStartInfo(terminal); 49 | } 50 | else 51 | { 52 | return new ProcessStartInfo("open", $". -a {terminal}"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/TestCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | using System.Diagnostics; 5 | 6 | namespace AbpDevTools.Commands; 7 | 8 | [Command("test", Description = "runs 'dotnet test' command recursively.")] 9 | public class TestCommand : ICommand 10 | { 11 | [CommandParameter(0, IsRequired = false, Description = "Working directory to run test. Probably project or solution directory path goes here. Default: . (Current Directory)")] 12 | public string? WorkingDirectory { get; set; } 13 | 14 | [CommandOption("files", 'f', Description = "(Array) Names or part of names of solutions will be tested.")] 15 | public string[]? TestFiles { get; set; } 16 | 17 | [CommandOption("interactive", 'i', Description = "Interactive test solution selection.")] 18 | public bool Interactive { get; set; } 19 | 20 | [CommandOption("configuration", 'c')] 21 | public string? Configuration { get; set; } 22 | 23 | [CommandOption("no-build", Description = "Skips build before running. Passes '--no-build' parameter to dotnet test.")] 24 | public bool NoBuild { get; set; } 25 | 26 | protected IConsole? console; 27 | protected Process? runningProcess; 28 | protected readonly ToolsConfiguration toolsConfiguration; 29 | 30 | public TestCommand(ToolsConfiguration toolsConfiguration) 31 | { 32 | this.toolsConfiguration = toolsConfiguration; 33 | } 34 | 35 | public async ValueTask ExecuteAsync(IConsole console) 36 | { 37 | this.console = console; 38 | if (string.IsNullOrEmpty(WorkingDirectory)) 39 | { 40 | WorkingDirectory = Directory.GetCurrentDirectory(); 41 | } 42 | var cancellationToken = console.RegisterCancellationHandler(); 43 | 44 | cancellationToken.Register(() => 45 | { 46 | AnsiConsole.MarkupLine("[red]AbpDev Test cancelled by the user.[/]"); 47 | console.Output.WriteLine("Killing process with id " + runningProcess?.Id); 48 | runningProcess?.Kill(true); 49 | }); 50 | 51 | var buildFiles = await FindBuildFilesAsync("*.sln", "solution"); 52 | 53 | if (buildFiles.Length == 0) 54 | { 55 | await console.Output.WriteLineAsync("No .sln files found. Looking for .csproj files."); 56 | return; 57 | } 58 | 59 | var successfulCount = await AnsiConsole.Status().StartAsync("Starting tests...", async ctx => 60 | { 61 | int completed = 0; 62 | for (int i = 0; i < buildFiles.Length; i++) 63 | { 64 | var buildFile = buildFiles[i]; 65 | 66 | var commandSuffix = NoBuild ? " --no-build" : string.Empty; 67 | if (!string.IsNullOrEmpty(Configuration)) 68 | { 69 | commandSuffix += $" --configuration {Configuration}"; 70 | } 71 | 72 | var tools = toolsConfiguration.GetOptions(); 73 | var startInfo = new ProcessStartInfo(tools["dotnet"], $"test {buildFile.FullName}{commandSuffix}"); 74 | startInfo.RedirectStandardOutput = true; 75 | startInfo.WorkingDirectory = WorkingDirectory; 76 | 77 | runningProcess = Process.Start(startInfo); 78 | ctx.Status($"Running tests for {buildFile.Name}."); 79 | runningProcess!.OutputDataReceived += (s, e) => 80 | { 81 | if (e.Data != null) 82 | { 83 | AnsiConsole.MarkupLine($"[grey]{e.Data}[/]"); 84 | } 85 | }; 86 | runningProcess!.BeginOutputReadLine(); 87 | 88 | await runningProcess.WaitForExitAsync(cancellationToken); 89 | 90 | if (runningProcess.ExitCode == 0) 91 | { 92 | completed++; 93 | } 94 | } 95 | 96 | return completed; 97 | }); 98 | } 99 | 100 | private async Task FindBuildFilesAsync(string pattern, string? nameOfPattern = null) 101 | { 102 | nameOfPattern ??= "solution"; 103 | 104 | var files = await AnsiConsole.Status() 105 | .StartAsync($"Looking for {nameOfPattern} files ({pattern})", async ctx => 106 | { 107 | ctx.Spinner(Spinner.Known.SimpleDotsScrolling); 108 | 109 | await Task.Yield(); 110 | 111 | var query = Directory.EnumerateFiles(WorkingDirectory!, pattern, SearchOption.AllDirectories); 112 | 113 | if (TestFiles?.Length > 0) 114 | { 115 | query = query.Where(x => TestFiles.Any(y => x.Contains(y, StringComparison.InvariantCultureIgnoreCase))); 116 | } 117 | 118 | var fileInfo = query 119 | .Select(x => new FileInfo(x)) 120 | .ToArray(); 121 | 122 | AnsiConsole.MarkupLine($"[green]{fileInfo.Length}[/] {pattern.Replace('*', '\0')} files found."); 123 | 124 | return fileInfo; 125 | }); 126 | 127 | if (Interactive && files.Length > 1) 128 | { 129 | var choosed = AnsiConsole.Prompt( 130 | new MultiSelectionPrompt() 131 | .Title("Choose files to be tested:") 132 | .NotRequired() // Not required to have a favorite fruit 133 | .PageSize(12) 134 | .HighlightStyle(new Style(foreground: Color.MediumPurple2)) 135 | .MoreChoicesText("[grey](Move up and down to reveal more files)[/]") 136 | .InstructionsText( 137 | "[grey](Press [mediumpurple2][/] to toggle a file, " + 138 | "[green][/] to accept)[/]") 139 | .AddChoices(files.Select(s => s.FullName))); 140 | 141 | files = files.Where(x => choosed.Contains(x.FullName)).ToArray(); 142 | } 143 | 144 | return files; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/ToolsCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Infrastructure; 3 | using Spectre.Console; 4 | 5 | namespace AbpDevTools.Commands; 6 | 7 | [Command("tools")] 8 | public class ToolsCommand : ICommand 9 | { 10 | protected readonly ToolsConfiguration toolsConfiguration; 11 | 12 | public ToolsCommand(ToolsConfiguration toolsConfiguration) 13 | { 14 | this.toolsConfiguration = toolsConfiguration; 15 | } 16 | 17 | public ValueTask ExecuteAsync(IConsole console) 18 | { 19 | var tools = toolsConfiguration.GetOptions(); 20 | 21 | console.Output.WriteLine("Available tools:\n"); 22 | 23 | var table = new Table(); 24 | 25 | table.AddColumn("Tool"); 26 | table.AddColumn("Path"); 27 | 28 | foreach (var tool in tools) 29 | { 30 | table.AddRow(tool.Key, tool.Value); 31 | } 32 | 33 | AnsiConsole.Write(table); 34 | 35 | console.Output.WriteLine("\nYou can change tools with the 'abpdev tools config' command.\n"); 36 | return ValueTask.CompletedTask; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AbpDevTools/Commands/UpdateCheckCommand.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Notifications; 2 | using AbpDevTools.Services; 3 | using CliFx.Infrastructure; 4 | using Spectre.Console; 5 | using System.Net.Http.Json; 6 | 7 | namespace AbpDevTools.Commands; 8 | 9 | [Command("update", Description = "Checks for updates")] 10 | public class UpdateCheckCommand : ICommand 11 | { 12 | public bool Force { get; set; } = true; 13 | 14 | public bool Silent { get; set; } 15 | 16 | protected readonly UpdateChecker updateChecker; 17 | protected readonly INotificationManager notificationManager; 18 | 19 | public UpdateCheckCommand(UpdateChecker updateChecker, INotificationManager notificationManager) 20 | { 21 | this.updateChecker = updateChecker; 22 | this.notificationManager = notificationManager; 23 | } 24 | 25 | public ValueTask SoftCheckAsync(IConsole console) 26 | { 27 | Force = false; 28 | Silent = true; 29 | return ExecuteAsync(console); 30 | } 31 | 32 | public async ValueTask ExecuteAsync(IConsole console) 33 | { 34 | var command = "dotnet tool update -g AbpDevTools"; 35 | 36 | if (Force) 37 | { 38 | console.Output.WriteLine($"Checking for updates..."); 39 | } 40 | 41 | var result = await updateChecker.CheckAsync(force: Force); 42 | 43 | if (Force) 44 | { 45 | console.Output.WriteLine($"Current version: {result.CurrentVersion}"); 46 | } 47 | 48 | if (result.UpdateAvailable) 49 | { 50 | await notificationManager.SendAsync( 51 | "AbpDevTools Update available!", 52 | $"Run '{command}' to update. A newer version {result.LatestVersion} available for AbpDevTools"); 53 | 54 | AnsiConsole.Markup($"[yellow]A newer version {result.LatestVersion} available.[/]\n"); 55 | 56 | AnsiConsole.Markup($"Run '[black on yellow]{command}[/]' to update."); 57 | } 58 | else 59 | { 60 | if (!Silent) 61 | { 62 | AnsiConsole.Markup($"[green]Your tool is up to date![/]\n"); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/CleanConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace AbpDevTools.Configuration; 5 | 6 | [RegisterTransient] 7 | public class CleanConfiguration : ConfigurationBase 8 | { 9 | public CleanConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 10 | { 11 | } 12 | 13 | public override string FileName => "clean-configuration"; 14 | 15 | protected override CleanOptions GetDefaults() 16 | { 17 | return new(); 18 | } 19 | } 20 | 21 | public class CleanOptions 22 | { 23 | public string[] Folders { get; set; } = new[] 24 | { 25 | "bin", 26 | "obj", 27 | "node_modules", 28 | }; 29 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/ConfigurationBase.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace AbpDevTools.Configuration; 5 | 6 | public interface IConfigurationBase 7 | { 8 | string FolderPath { get; } 9 | 10 | string FilePath { get; } 11 | } 12 | 13 | public abstract class ConfigurationBase : IConfigurationBase 14 | where T : class 15 | { 16 | private readonly IDeserializer _yamlDeserializer; 17 | private readonly ISerializer _yamlSerializer; 18 | 19 | protected ConfigurationBase(IDeserializer yamlDeserializer, ISerializer yamlSerializer) 20 | { 21 | _yamlDeserializer = yamlDeserializer; 22 | _yamlSerializer = yamlSerializer; 23 | } 24 | 25 | public string FolderPath => Path.Combine( 26 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 27 | "abpdev"); 28 | 29 | public string FilePath => Path.Combine( 30 | FolderPath, 31 | FileName + ".yml"); 32 | 33 | public virtual string FileName => GetType().Name + ".yml"; 34 | 35 | protected virtual string LegacyJsonFilePath => Path.Combine( 36 | FolderPath, 37 | FileName + ".json"); 38 | 39 | public virtual T GetOptions() 40 | { 41 | if (!Directory.Exists(FolderPath)) 42 | Directory.CreateDirectory(FolderPath); 43 | 44 | var options = GetDefaults(); 45 | 46 | // Check for legacy JSON file and migrate if needed 47 | if (File.Exists(LegacyJsonFilePath)) 48 | { 49 | options = MigrateFromJson(); 50 | } 51 | else if (File.Exists(FilePath)) 52 | { 53 | options = ReadOptions(); 54 | } 55 | else 56 | { 57 | SaveOptions(options); 58 | } 59 | 60 | return options; 61 | } 62 | 63 | protected virtual T ReadOptions() 64 | { 65 | var ymlContent = File.ReadAllText(FilePath); 66 | return _yamlDeserializer.Deserialize(ymlContent); 67 | } 68 | 69 | protected virtual void SaveOptions(T options) 70 | { 71 | var yaml = _yamlSerializer.Serialize(options); 72 | File.WriteAllText(FilePath, yaml); 73 | } 74 | 75 | private T MigrateFromJson() 76 | { 77 | var jsonContent = File.ReadAllText(LegacyJsonFilePath); 78 | var options = JsonSerializer.Deserialize(jsonContent)!; 79 | 80 | // Save as YAML 81 | SaveOptions(options); 82 | 83 | // Delete old JSON file 84 | File.Delete(LegacyJsonFilePath); 85 | 86 | return options; 87 | } 88 | 89 | protected abstract T GetDefaults(); 90 | } 91 | -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/DictionaryConfigurationBase.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace AbpDevTools.Configuration; 5 | 6 | public abstract class DictionaryConfigurationBase : ConfigurationBase> 7 | where T : class 8 | { 9 | protected DictionaryConfigurationBase(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 10 | { 11 | } 12 | 13 | protected virtual bool PreserveExistingValues => true; 14 | 15 | public override Dictionary GetOptions() 16 | { 17 | if (!Directory.Exists(FolderPath)) 18 | Directory.CreateDirectory(FolderPath); 19 | 20 | var options = GetDefaults()!; 21 | var shouldWrite = !File.Exists(FilePath); 22 | 23 | if (File.Exists(FilePath)) 24 | { 25 | var existingOptions = base.GetOptions(); 26 | 27 | if (PreserveExistingValues) 28 | { 29 | foreach (var defaultOption in options) 30 | { 31 | if (!existingOptions.ContainsKey(defaultOption.Key)) 32 | { 33 | existingOptions[defaultOption.Key] = defaultOption.Value; 34 | shouldWrite = true; 35 | } 36 | } 37 | options = existingOptions; 38 | } 39 | else 40 | { 41 | // When not preserving, just add missing defaults to existing options 42 | options = GetDefaults()!; 43 | shouldWrite = true; 44 | } 45 | } 46 | 47 | if (shouldWrite) 48 | { 49 | SaveOptions(options); 50 | } 51 | 52 | return options; 53 | } 54 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/EnvironmentAppConfiguration.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace AbpDevTools.Configuration; 4 | 5 | [RegisterTransient] 6 | public class EnvironmentAppConfiguration : DictionaryConfigurationBase 7 | { 8 | public const string SqlServer = "sqlserver"; 9 | public const string SqlServerEdge = "sqlserver-edge"; 10 | public const string PostgreSql = "postgresql"; 11 | public const string MySql = "mysql"; 12 | public const string MongoDb = "mongodb"; 13 | public const string Redis = "redis"; 14 | public const string RabbitMq = "rabbitmq"; 15 | 16 | public EnvironmentAppConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 17 | { 18 | } 19 | 20 | public override string FileName => "environment-tools"; 21 | 22 | protected override Dictionary GetDefaults() 23 | { 24 | return new Dictionary 25 | { 26 | { SqlServer, new EnvironmentToolOption("docker start tmp-sqlserver;docker run --name tmp-sqlserver --restart unless-stopped -e \"ACCEPT_EULA=Y\" -e \"SA_PASSWORD=Passw0rd\" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-CU8-ubuntu", "docker kill tmp-sqlserver;docker rm tmp-sqlserver") }, 27 | { SqlServerEdge, new EnvironmentToolOption("docker start tmp-sqlserver-edge;docker run --name tmp-sqlserver-edge --restart unless-stopped -d --cap-add SYS_PTRACE -e \"ACCEPT_EULA=1\" -e \"MSSQL_SA_PASSWORD=Passw0rd\" -p 1433:1433 mcr.microsoft.com/azure-sql-edge", "docker kill tmp-sqlserver-edge;docker rm tmp-sqlserver-edge") }, 28 | { PostgreSql, new EnvironmentToolOption("docker start tmp-postgres;docker run --name tmp-postgres --restart unless-stopped -e POSTGRES_PASSWORD=Passw0rd -p 5432:5432 -d postgres", "docker kill tmp-posgres;docker rm tmp-posgres") }, 29 | { MySql, new EnvironmentToolOption("docker start tmp-mysql;docker run --name tmp-mysql --restart unless-stopped -e \"MYSQL_ROOT_PASSWORD=Passw0rd\" -p 3306:3306 --platform linux/x86_64 -d mysql:5.7", "docker kill tmp-mysql;docker rm tmp-mysql" )}, 30 | { MongoDb, new EnvironmentToolOption("docker start tmp-mongo;docker run --name tmp-mongo --restart unless-stopped -p 27017:27017 -d mongo:latest","docker kill tmp-mongo;docker rm tmp-mongo")}, 31 | { Redis, new EnvironmentToolOption("docker start tmp-redis;docker run --name tmp-redis -p 6379:6379 -d --restart unless-stopped redis", "docker kill tmp-redis;docker rm tmp-redis") }, 32 | { RabbitMq, new EnvironmentToolOption("docker start tmp-rabbitmq;docker run --name tmp-rabbitmq -d --restart unless-stopped -p 15672:15672 -p 5672:5672 rabbitmq:3-management", "docker kill tmp-rabbitmq;docker rm tmp-rabbitmq") } 33 | }; 34 | } 35 | } 36 | 37 | public class EnvironmentToolOption 38 | { 39 | public EnvironmentToolOption() 40 | { 41 | } 42 | 43 | public EnvironmentToolOption(string startCmd, string stopCmd) 44 | { 45 | StartCmd = startCmd; 46 | StopCmd = stopCmd; 47 | } 48 | 49 | public string StartCmd { get; set; } 50 | public string StopCmd { get; set; } 51 | } 52 | -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/EnvironmentConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace AbpDevTools.Configuration; 5 | 6 | [RegisterTransient] 7 | public class EnvironmentConfiguration : DictionaryConfigurationBase 8 | { 9 | public const string SqlServer = "SqlServer"; 10 | public const string PostgreSql = "PostgreSql"; 11 | public const string MySql = "MySql"; 12 | public const string MongoDb = "MongoDb"; 13 | 14 | public EnvironmentConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 15 | { 16 | } 17 | 18 | protected override Dictionary GetDefaults() 19 | { 20 | return new Dictionary 21 | { 22 | { 23 | SqlServer, new EnvironmentOption 24 | { 25 | Variables = new Dictionary 26 | { 27 | { "ConnectionStrings__Default", "Server=localhost;Database={AppName}_{Today};User ID=SA;Password=12345678Aa;TrustServerCertificate=True" } 28 | } 29 | } 30 | }, 31 | { 32 | MongoDb, new EnvironmentOption 33 | { 34 | Variables = new Dictionary 35 | { 36 | { "ConnectionStrings__Default", "mongodb://localhost:27017/{AppName}_{Today}" } 37 | } 38 | } 39 | }, 40 | { 41 | PostgreSql, new EnvironmentOption 42 | { 43 | Variables = new Dictionary 44 | { 45 | { "ConnectionStrings__Default", "Server=localhost;Port=5432;Database={AppName}_{Today};User Id=postgres;Password=12345678Aa;" } 46 | } 47 | } 48 | }, 49 | { 50 | MySql, new EnvironmentOption 51 | { 52 | Variables = new Dictionary 53 | { 54 | { "ConnectionStrings__Default", "Server=localhost;Port=3306;Database={AppName}_{Today};User Id=root;Password=12345678Aa;" } 55 | } 56 | } 57 | } 58 | }; 59 | } 60 | } 61 | 62 | public class EnvironmentOption 63 | { 64 | public Dictionary Variables { get; set; } 65 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/NotificationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace AbpDevTools.Configuration; 5 | 6 | [RegisterTransient] 7 | public class NotificationConfiguration : ConfigurationBase 8 | { 9 | public NotificationConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) 10 | : base(yamlDeserializer, yamlSerializer) 11 | { 12 | } 13 | 14 | public override string FileName => "notifications"; 15 | 16 | public void SetOptions(NotificationOption options) 17 | { 18 | File.WriteAllText(FilePath, JsonSerializer.Serialize(options, new JsonSerializerOptions 19 | { 20 | WriteIndented = true 21 | })); 22 | } 23 | 24 | protected override NotificationOption GetDefaults() => new(); 25 | } 26 | 27 | public class NotificationOption 28 | { 29 | public bool Enabled { get; set; } 30 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/ReplacementConfiguration.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace AbpDevTools.Configuration; 4 | 5 | [RegisterTransient] 6 | public class ReplacementConfiguration : ConfigurationBase> 7 | { 8 | public ReplacementConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 9 | { 10 | } 11 | 12 | public override string FileName => "replacements"; 13 | 14 | protected override string LegacyJsonFilePath => Path.Combine(FolderPath, "replacements.json"); 15 | 16 | protected override Dictionary GetDefaults() 17 | { 18 | return new Dictionary 19 | { 20 | { 21 | "ConnectionStrings", new ReplacementOption 22 | { 23 | FilePattern = "appsettings.json", 24 | Find = "Trusted_Connection=True;", 25 | Replace = "User ID=SA;Password=12345678Aa;" 26 | } 27 | }, 28 | { 29 | "LocalDb", new ReplacementOption 30 | { 31 | FilePattern = "appsettings.json", 32 | Find = "Server=(LocalDb)\\\\MSSQLLocalDB;", 33 | Replace = "Server=localhost;" 34 | } 35 | } 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/ReplacementOption.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools.Configuration; 2 | public class ReplacementOption 3 | { 4 | public string FilePattern { get; set; } 5 | public string Find { get; set; } 6 | public string Replace { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/RunConfiguration.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace AbpDevTools.Configuration; 4 | 5 | [RegisterTransient] 6 | [Obsolete] 7 | public class RunConfiguration : ConfigurationBase 8 | { 9 | public RunConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 10 | { 11 | } 12 | 13 | public override string FileName => "run-configuration"; 14 | 15 | [Obsolete] 16 | protected override RunOptions GetDefaults() 17 | { 18 | return new RunOptions 19 | { 20 | RunnableProjects = new[] 21 | { 22 | ".HttpApi.Host", 23 | ".HttpApi.HostWithIds", 24 | ".AuthServer", 25 | ".IdentityServer", 26 | ".Web", 27 | ".Web.Host", 28 | ".Web.Public", 29 | ".Mvc", 30 | ".Mvc.Host", 31 | ".Blazor", 32 | ".Blazor.Host", 33 | ".Blazor.Server", 34 | ".Blazor.Server.Host", 35 | ".Blazor.Server.Tiered", 36 | ".Unified", 37 | ".PublicWeb", 38 | ".PublicWebGateway", 39 | ".WebGateway" 40 | } 41 | }; 42 | } 43 | 44 | [Obsolete] 45 | public override RunOptions GetOptions() 46 | { 47 | return base.GetOptions(); 48 | } 49 | 50 | public void CleanObsolete() 51 | { 52 | if(File.Exists(FilePath)) 53 | { 54 | File.Delete(FilePath); 55 | } 56 | } 57 | } 58 | 59 | [Obsolete] 60 | public class RunOptions 61 | { 62 | public string[] RunnableProjects { get; set; } = Array.Empty(); 63 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Configuration/ToolsConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Text.Json; 3 | using YamlDotNet.Serialization; 4 | 5 | namespace AbpDevTools.Configuration; 6 | 7 | [RegisterTransient] 8 | public class ToolsConfiguration : ConfigurationBase 9 | { 10 | public ToolsConfiguration(IDeserializer yamlDeserializer, ISerializer yamlSerializer) : base(yamlDeserializer, yamlSerializer) 11 | { 12 | } 13 | 14 | public override string FileName => "tools-configuration"; 15 | 16 | public override ToolOption GetOptions() 17 | { 18 | if (!Directory.Exists(FolderPath)) 19 | { 20 | Directory.CreateDirectory(FolderPath); 21 | } 22 | 23 | var _defaults = GetDefaults(); 24 | var shouldSave = true; 25 | 26 | if (File.Exists(FilePath)) 27 | { 28 | var options = ReadOptions()!; 29 | 30 | shouldSave = Merge(options, _defaults); 31 | } 32 | 33 | if(shouldSave) 34 | { 35 | SaveOptions(_defaults); 36 | } 37 | 38 | return _defaults; 39 | } 40 | 41 | protected override ToolOption GetDefaults() 42 | { 43 | var _defaults = new ToolOption 44 | { 45 | { "powershell", "pwsh"}, 46 | { "dotnet", "dotnet" }, 47 | { "abp", "abp" }, 48 | }; 49 | 50 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 51 | { 52 | _defaults["open"] = "explorer"; 53 | _defaults["terminal"] = "wt"; 54 | } 55 | else 56 | { 57 | _defaults["open"] = "open"; 58 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 59 | { 60 | _defaults["osascript"] = "osascript"; 61 | _defaults["terminal"] = "terminal"; 62 | } 63 | } 64 | 65 | return _defaults; 66 | } 67 | 68 | private static bool Merge(Dictionary options, Dictionary defaults) 69 | { 70 | var changed = false; 71 | foreach (var (key, value) in defaults) 72 | { 73 | if (!options.ContainsKey(key)) 74 | { 75 | options[key] = value; 76 | changed = true; 77 | } 78 | } 79 | 80 | return changed; 81 | } 82 | } 83 | 84 | public class ToolOption : Dictionary 85 | { 86 | } 87 | -------------------------------------------------------------------------------- /src/AbpDevTools/DotnetDependencyResolver.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using CliFx.Exceptions; 3 | using Spectre.Console; 4 | using System.Diagnostics; 5 | using System.Text.Json; 6 | 7 | namespace AbpDevTools; 8 | 9 | [RegisterTransient] 10 | public class DotnetDependencyResolver 11 | { 12 | protected ToolOption Tools { get; } 13 | 14 | public DotnetDependencyResolver(ToolsConfiguration toolsConfiguration) 15 | { 16 | Tools = toolsConfiguration.GetOptions(); 17 | } 18 | 19 | public async Task CheckSingleDependencyAsync(string projectPath, string assemblyName, CancellationToken cancellationToken) 20 | { 21 | await RestoreProjectAsync(projectPath); 22 | return await IsPackageDependencyAsync(projectPath, assemblyName, cancellationToken); 23 | } 24 | 25 | private async Task IsPackageDependencyAsync(string projectPath, string assemblyName, CancellationToken cancellationToken) 26 | { 27 | var startInfo = new ProcessStartInfo 28 | { 29 | FileName = Tools["dotnet"], 30 | Arguments = $"nuget why \"{projectPath}\" \"{assemblyName}\"", 31 | RedirectStandardOutput = true, 32 | RedirectStandardError = true, 33 | UseShellExecute = false, 34 | CreateNoWindow = true 35 | }; 36 | 37 | using var process = new Process { StartInfo = startInfo }; 38 | var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 39 | cts.CancelAfter(TimeSpan.FromSeconds(30)); // 30-second timeout 40 | 41 | try 42 | { 43 | process.Start(); 44 | 45 | var outputTask = process.StandardOutput.ReadToEndAsync(); 46 | var errorTask = process.StandardError.ReadToEndAsync(); 47 | 48 | // Wait for the process to exit or for the cancellation token to be triggered 49 | await Task.WhenAny(process.WaitForExitAsync(cts.Token), Task.Delay(Timeout.Infinite, cts.Token)); 50 | 51 | if (!process.HasExited) 52 | { 53 | process.Kill(entireProcessTree: true); 54 | throw new TimeoutException($"'dotnet nuget why' command timed out for assembly '{assemblyName}' in project '{projectPath}'."); 55 | } 56 | 57 | var exitCode = process.ExitCode; 58 | var output = await outputTask; 59 | var error = await errorTask; 60 | 61 | if (exitCode != 0) 62 | { 63 | AnsiConsole.WriteLine($"Error executing 'dotnet nuget why': {error}"); 64 | return false; 65 | } 66 | 67 | // Return false if the output indicates no dependency 68 | if (output.Contains("does not have a dependency on", StringComparison.OrdinalIgnoreCase)) 69 | { 70 | return false; 71 | } 72 | 73 | // Only return true if there's actual dependency information 74 | return !string.IsNullOrWhiteSpace(output); 75 | } 76 | catch (OperationCanceledException) 77 | { 78 | if (!process.HasExited) 79 | { 80 | process.Kill(entireProcessTree: true); 81 | } 82 | AnsiConsole.WriteLine($"'dotnet nuget why' command was cancelled for assembly '{assemblyName}' in project '{projectPath}'."); 83 | return false; 84 | } 85 | catch (Exception ex) 86 | { 87 | AnsiConsole.WriteLine($"Unexpected error executing 'dotnet nuget why': {ex.Message}"); 88 | return false; 89 | } 90 | } 91 | 92 | public async Task> GetProjectDependenciesAsync(string projectPath) 93 | { 94 | var packages = new HashSet(); 95 | 96 | await RestoreProjectAsync(projectPath); 97 | 98 | // Get NuGet package references 99 | var listPackagesResult = await ExecuteDotnetListPackagesAsync(projectPath); 100 | var packageReferences = ParsePackageList(listPackagesResult); 101 | packages.UnionWith(packageReferences); 102 | 103 | // Get project references 104 | var listReferencesResult = await ExecuteDotnetListReferenceAsync(projectPath); 105 | var projectReferences = ParseProjectReferences(listReferencesResult); 106 | packages.UnionWith(projectReferences); 107 | 108 | return packages; 109 | } 110 | 111 | private async Task ExecuteDotnetListPackagesAsync(string projectPath) 112 | { 113 | var startInfo = new ProcessStartInfo 114 | { 115 | FileName = Tools["dotnet"], 116 | Arguments = $"list {projectPath} package --format json", 117 | RedirectStandardOutput = true, 118 | RedirectStandardError = true, 119 | UseShellExecute = false, 120 | CreateNoWindow = true 121 | }; 122 | 123 | using var process = Process.Start(startInfo) 124 | ?? throw new CommandException("Failed to start 'dotnet list package' process."); 125 | 126 | // Asynchronously read output and error 127 | var outputTask = process.StandardOutput.ReadToEndAsync(); 128 | var errorTask = process.StandardError.ReadToEndAsync(); 129 | 130 | // Wait for exit with timeout 131 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); 132 | try 133 | { 134 | process.WaitForExit(30000); // 30 seconds 135 | 136 | if (!process.HasExited) 137 | { 138 | process.Kill(entireProcessTree: true); 139 | throw new TimeoutException($"'dotnet list package' command timed out for project '{projectPath}'."); 140 | } 141 | 142 | if (process.ExitCode != 0) 143 | { 144 | var error = await errorTask; 145 | throw new CommandException($"'dotnet list package' failed with exit code {process.ExitCode}. Error: {error}"); 146 | } 147 | 148 | return await outputTask; 149 | } 150 | catch (Exception ex) when (!(ex is CommandException)) 151 | { 152 | process.Kill(entireProcessTree: true); 153 | throw new CommandException($"Error executing 'dotnet list package': {ex.Message}"); 154 | } 155 | } 156 | 157 | private HashSet ParsePackageList(string jsonOutput) 158 | { 159 | var packages = new HashSet(); 160 | 161 | try 162 | { 163 | using var doc = JsonDocument.Parse(jsonOutput); 164 | var projects = doc.RootElement.GetProperty("projects"); 165 | 166 | foreach (var project in projects.EnumerateArray()) 167 | { 168 | if (project.TryGetProperty("frameworks", out var frameworks)) 169 | { 170 | foreach (var framework in frameworks.EnumerateArray()) 171 | { 172 | if (framework.TryGetProperty("topLevelPackages", out var topLevelPackages)) 173 | { 174 | foreach (var package in topLevelPackages.EnumerateArray()) 175 | { 176 | var id = package.GetProperty("id").GetString(); 177 | if (!string.IsNullOrEmpty(id)) 178 | { 179 | packages.Add(id); 180 | } 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | catch (JsonException ex) 188 | { 189 | throw new CommandException("Failed to parse package list output.\n\n" + ex.Message); 190 | } 191 | 192 | return packages; 193 | } 194 | 195 | private async Task ExecuteDotnetListReferenceAsync(string projectPath) 196 | { 197 | var startInfo = new ProcessStartInfo 198 | { 199 | FileName = Tools["dotnet"], 200 | Arguments = $"list {projectPath} reference", 201 | RedirectStandardOutput = true, 202 | RedirectStandardError = true, 203 | UseShellExecute = false, 204 | CreateNoWindow = true 205 | }; 206 | 207 | using var process = Process.Start(startInfo) 208 | ?? throw new CommandException("Failed to start 'dotnet list reference' process."); 209 | 210 | // Asynchronously read output and error 211 | var outputTask = process.StandardOutput.ReadToEndAsync(); 212 | var errorTask = process.StandardError.ReadToEndAsync(); 213 | 214 | // Wait for exit with timeout 215 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); 216 | try 217 | { 218 | process.WaitForExit(30000); // 30 seconds 219 | 220 | if (!process.HasExited) 221 | { 222 | process.Kill(entireProcessTree: true); 223 | throw new TimeoutException($"'dotnet list reference' command timed out for project '{projectPath}'."); 224 | } 225 | 226 | if (process.ExitCode != 0) 227 | { 228 | var error = await errorTask; 229 | throw new CommandException($"'dotnet list reference' failed with exit code {process.ExitCode}. Error: {error}"); 230 | } 231 | 232 | return await outputTask; 233 | } 234 | catch (Exception ex) when (!(ex is CommandException)) 235 | { 236 | process.Kill(entireProcessTree: true); 237 | throw new CommandException($"Error executing 'dotnet list reference': {ex.Message}"); 238 | } 239 | } 240 | 241 | private HashSet ParseProjectReferences(string output) 242 | { 243 | var references = new HashSet(); 244 | 245 | var lines = output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); 246 | foreach (var line in lines) 247 | { 248 | if (string.IsNullOrWhiteSpace(line)) continue; 249 | 250 | var projectPath = line.Trim(); 251 | var projectName = Path.GetFileNameWithoutExtension(projectPath); 252 | references.Add(projectName); 253 | } 254 | 255 | return references; 256 | } 257 | 258 | private async Task RestoreProjectAsync(string projectPath) 259 | { 260 | var friendlyProjectName = Path.GetFileNameWithoutExtension(projectPath); 261 | if(!ShouldRestoreProject(projectPath)) 262 | { 263 | return; 264 | } 265 | 266 | AnsiConsole.WriteLine($"{Emoji.Known.RecyclingSymbol} Restoring {friendlyProjectName}..."); 267 | 268 | var restoreStartInfo = new ProcessStartInfo 269 | { 270 | FileName = Tools["dotnet"], 271 | Arguments = $"restore \"{projectPath}\"", 272 | RedirectStandardOutput = true, 273 | RedirectStandardError = true, 274 | UseShellExecute = false, 275 | CreateNoWindow = true 276 | }; 277 | 278 | using var restoreProcess = Process.Start(restoreStartInfo) 279 | ?? throw new CommandException("Failed to start 'dotnet restore' process."); 280 | 281 | // Asynchronously read output and error 282 | var outputTask = restoreProcess.StandardOutput.ReadToEndAsync(); 283 | var errorTask = restoreProcess.StandardError.ReadToEndAsync(); 284 | 285 | // Wait for exit with timeout 286 | var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); // 2-minute timeout 287 | try 288 | { 289 | restoreProcess.WaitForExit(120000); // 120 seconds 290 | 291 | if (!restoreProcess.HasExited) 292 | { 293 | restoreProcess.Kill(entireProcessTree: true); 294 | throw new TimeoutException($"'dotnet restore' command timed out for project '{friendlyProjectName}'."); 295 | } 296 | 297 | if (restoreProcess.ExitCode != 0) 298 | { 299 | var error = await errorTask; 300 | throw new CommandException($"'dotnet restore' failed with exit code {restoreProcess.ExitCode}. Error: {error}"); 301 | } 302 | } 303 | catch (Exception ex) when (!(ex is CommandException)) 304 | { 305 | restoreProcess.Kill(entireProcessTree: true); 306 | throw new CommandException($"Error executing 'dotnet restore': {ex.Message}"); 307 | } 308 | } 309 | 310 | private bool ShouldRestoreProject(string projectPath) 311 | { 312 | var projectDirectory = Path.GetDirectoryName(projectPath) 313 | ?? throw new CommandException($"Could not get directory for project: {projectPath}"); 314 | 315 | var binPath = Path.Combine(projectDirectory, "bin"); 316 | var objPath = Path.Combine(projectDirectory, "obj"); 317 | 318 | if (!Directory.Exists(binPath) && !Directory.Exists(objPath)) 319 | { 320 | return true; 321 | } 322 | 323 | var assetsFile = Path.Combine(objPath, "project.assets.json"); 324 | if (!File.Exists(assetsFile)) 325 | { 326 | return true; 327 | } 328 | 329 | var projectFileInfo = new FileInfo(projectPath); 330 | var assetsFileInfo = new FileInfo(assetsFile); 331 | 332 | return projectFileInfo.LastWriteTimeUtc > assetsFileInfo.LastWriteTimeUtc; 333 | } 334 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Environments/AppEnvironmentMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AbpDevTools.Configuration; 3 | 4 | namespace AbpDevTools.Environments; 5 | 6 | public class AppEnvironmentMapping 7 | { 8 | public string EnvironmentName { get; set; } = string.Empty; 9 | public string AppName { get; set; } = string.Empty; 10 | 11 | public static Dictionary Default { get; } = new() 12 | { 13 | { 14 | "Volo.Abp.EntityFrameworkCore.SqlServer", 15 | new AppEnvironmentMapping 16 | { 17 | AppName = EnvironmentAppConfiguration.SqlServerEdge, 18 | EnvironmentName = EnvironmentConfiguration.SqlServer 19 | } 20 | }, 21 | { 22 | "Volo.Abp.EntityFrameworkCore.MySQL", 23 | new AppEnvironmentMapping 24 | { 25 | AppName = EnvironmentAppConfiguration.MySql, 26 | EnvironmentName = EnvironmentConfiguration.MySql 27 | } 28 | }, 29 | { 30 | "Volo.Abp.EntityFrameworkCore.PostgreSql", 31 | new AppEnvironmentMapping 32 | { 33 | AppName = EnvironmentAppConfiguration.PostgreSql, 34 | EnvironmentName = EnvironmentConfiguration.PostgreSql 35 | } 36 | }, 37 | { 38 | "Volo.Abp.Caching.StackExchangeRedis", 39 | new AppEnvironmentMapping 40 | { 41 | AppName = EnvironmentAppConfiguration.Redis 42 | } 43 | }, 44 | { 45 | "Volo.Abp.EventBus.RabbitMQ", 46 | new AppEnvironmentMapping 47 | { 48 | AppName = EnvironmentAppConfiguration.RabbitMq 49 | } 50 | }, 51 | { 52 | "Volo.Abp.MongoDB", 53 | new AppEnvironmentMapping 54 | { 55 | AppName = EnvironmentAppConfiguration.MongoDb 56 | } 57 | } 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/AbpDevTools/Environments/IProcessEnvironmentManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace AbpDevTools.Environments; 4 | public interface IProcessEnvironmentManager 5 | { 6 | void SetEnvironment(string environment, string directory); 7 | void SetEnvironmentForProcess(string environment, ProcessStartInfo process); 8 | void SetEnvironmentVariablesForProcess(ProcessStartInfo process, Dictionary variables); 9 | } 10 | -------------------------------------------------------------------------------- /src/AbpDevTools/Environments/ProcessEnvironmentManager.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using AutoRegisterInject; 3 | using CliFx.Exceptions; 4 | using System.Diagnostics; 5 | using System.Text.RegularExpressions; 6 | using Unidecode.NET; 7 | 8 | namespace AbpDevTools.Environments; 9 | 10 | [RegisterTransient] 11 | public class ProcessEnvironmentManager : IProcessEnvironmentManager 12 | { 13 | private static Dictionary> replacements = new Dictionary>() 14 | { 15 | { "{Today}", (_) => DateTime.Today.ToString("yyyyMMdd") }, 16 | { "{AppName}", FindAppName } 17 | }; 18 | 19 | private readonly EnvironmentConfiguration environmentConfiguration; 20 | 21 | public ProcessEnvironmentManager(EnvironmentConfiguration environmentConfiguration) 22 | { 23 | this.environmentConfiguration = environmentConfiguration; 24 | } 25 | 26 | public void SetEnvironment(string environment, string directory) 27 | { 28 | var options = environmentConfiguration.GetOptions(); 29 | 30 | if (!options.TryGetValue(environment, out var env)) 31 | { 32 | var environments = string.Join('\n', options.Keys.Select(x => "\t- " + x)); 33 | throw new CommandException("Environment not found! Check environments by 'abpdev env config' command.\nAvailable environments:\n" + environments); 34 | } 35 | 36 | foreach (var variable in env.Variables) 37 | { 38 | Environment.SetEnvironmentVariable(variable.Key, PrepareValue(variable.Value, directory), EnvironmentVariableTarget.Process); 39 | } 40 | } 41 | 42 | public void SetEnvironmentForProcess(string environment, ProcessStartInfo process) 43 | { 44 | var options = environmentConfiguration.GetOptions(); 45 | 46 | if (!options.TryGetValue(environment, out var env)) 47 | { 48 | var environments = string.Join('\n', options.Keys.Select(x => "\t- " + x)); 49 | throw new CommandException("Environment not found! Check environments by 'abpdev env config' command.\nAvailable environments:\n" + environments); 50 | } 51 | 52 | SetEnvironmentVariablesForProcess(process, env.Variables); 53 | } 54 | 55 | public void SetEnvironmentVariablesForProcess(ProcessStartInfo process, Dictionary variables) 56 | { 57 | foreach (var variable in variables) 58 | { 59 | 60 | process.EnvironmentVariables[variable.Key] = PrepareValue(variable.Value, process.WorkingDirectory); 61 | } 62 | } 63 | 64 | protected virtual string PrepareValue(string value, string? directory = null) 65 | { 66 | var finalResult = value; 67 | foreach (var item in replacements) 68 | { 69 | finalResult = finalResult.Replace(item.Key, item.Value(directory)); 70 | } 71 | 72 | return finalResult; 73 | } 74 | 75 | private static string FindAppName(string? directory) 76 | { 77 | var dir = directory; 78 | 79 | if (string.IsNullOrEmpty(dir)) 80 | { 81 | dir = Directory.GetCurrentDirectory(); 82 | } 83 | 84 | var folderName = new DirectoryInfo(dir).Name; 85 | if (folderName.Contains(".")) 86 | { 87 | var appName = folderName.Split('.').First(); 88 | 89 | dir = dir.Replace(folderName, appName); 90 | } 91 | 92 | var normalized = Regex.Replace(dir.Unidecode().ToLowerInvariant(), @"[^a-z0-9]", string.Empty); 93 | 94 | var result = TruncateStart(normalized, 116); 95 | 96 | return result; 97 | } 98 | 99 | private static string TruncateStart(string value, int maximumLength) 100 | { 101 | if (value.Length <= maximumLength) 102 | { 103 | return value; 104 | } 105 | 106 | return value[^maximumLength..]; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/AbpDevTools/FileExplorer.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools; 2 | 3 | [RegisterTransient] 4 | public class FileExplorer 5 | { 6 | public IEnumerable FindDescendants(string path, string pattern) 7 | { 8 | return Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories); 9 | } 10 | 11 | public IEnumerable FindDescendants(string path, string pattern, string[] excludeFolders) 12 | { 13 | return Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories) 14 | .Where(x => !excludeFolders.Any(x.Contains)); 15 | } 16 | 17 | public IEnumerable FindAscendants(string path, string pattern) 18 | { 19 | foreach (var item in Directory.EnumerateFiles(path, pattern, SearchOption.TopDirectoryOnly)) 20 | { 21 | yield return item; 22 | } 23 | 24 | var upLevelPath = Path.GetFullPath(Path.Combine(path, "..")); 25 | 26 | var root = Path.GetPathRoot(path); 27 | 28 | if (upLevelPath != root) 29 | { 30 | foreach (var item in FindAscendants(upLevelPath, pattern)) 31 | { 32 | yield return item; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AbpDevTools/Global.usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using CliFx; 3 | global using CliFx.Attributes; 4 | -------------------------------------------------------------------------------- /src/AbpDevTools/LocalConfigurations/LocalConfiguration.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | 3 | namespace AbpDevTools.LocalConfigurations; 4 | public class LocalConfiguration 5 | { 6 | public LocalRunOption? Run { get; set; } 7 | public LocalEnvironmentOption? Environment { get; set; } 8 | 9 | public class LocalEnvironmentOption : EnvironmentOption 10 | { 11 | public string? Name { get; set; } 12 | } 13 | 14 | public class LocalRunOption 15 | { 16 | public bool Watch { get; set; } 17 | public bool NoBuild { get; set; } 18 | public bool GraphBuild { get; set; } 19 | public string? Configuration { get; set; } 20 | public bool SkipMigrate { get; set; } 21 | public string[] Projects { get; set; } = Array.Empty(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AbpDevTools/LocalConfigurations/LocalConfigurationManager.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Environments; 2 | using System.Diagnostics; 3 | using YamlDotNet.Serialization; 4 | 5 | namespace AbpDevTools.LocalConfigurations; 6 | 7 | [RegisterTransient] 8 | public class LocalConfigurationManager 9 | { 10 | protected readonly IDeserializer _deserializer; 11 | protected readonly ISerializer _serializer; 12 | protected readonly FileExplorer fileExplorer; 13 | protected readonly IProcessEnvironmentManager environmentManager; 14 | 15 | public LocalConfigurationManager(IDeserializer deserializer, ISerializer serializer, FileExplorer fileExplorer, IProcessEnvironmentManager environmentManager) 16 | { 17 | _deserializer = deserializer; 18 | _serializer = serializer; 19 | this.fileExplorer = fileExplorer; 20 | this.environmentManager = environmentManager; 21 | } 22 | 23 | public string Save(string path, LocalConfiguration configuration) 24 | { 25 | var directory = Path.GetDirectoryName(path); 26 | if (directory == null) 27 | { 28 | throw new ArgumentException("Invalid path", nameof(path)); 29 | } 30 | 31 | if (!Directory.Exists(directory)) 32 | { 33 | Directory.CreateDirectory(directory); 34 | } 35 | 36 | var yaml = _serializer.Serialize(configuration); 37 | 38 | var filePath = path; 39 | if (!path.EndsWith(".yml")) 40 | { 41 | filePath = Path.Combine(path, "abpdev.yml"); 42 | } 43 | 44 | File.WriteAllText(filePath, yaml); 45 | return filePath; 46 | } 47 | 48 | public bool TryLoad(string path, out LocalConfiguration? localConfiguration, FileSearchDirection direction = FileSearchDirection.Ascendants) 49 | { 50 | localConfiguration = null; 51 | 52 | var directory = Path.GetDirectoryName(path); 53 | if (directory == null) 54 | { 55 | return false; 56 | } 57 | 58 | string? fileName = "abpdev.yml"; 59 | 60 | if (path.EndsWith(".yml")) 61 | { 62 | fileName = Path.GetFileName(path); 63 | } 64 | 65 | var ymlPath = direction switch 66 | { 67 | FileSearchDirection.Ascendants => fileExplorer.FindAscendants(directory, fileName).FirstOrDefault(), 68 | FileSearchDirection.Descendants => fileExplorer.FindDescendants(directory, fileName).FirstOrDefault(), 69 | FileSearchDirection.OnlyCurrent => Path.Combine(directory, fileName), 70 | _ => throw new NotImplementedException() 71 | }; 72 | 73 | if (string.IsNullOrEmpty(ymlPath) || !File.Exists(ymlPath)) 74 | { 75 | return false; 76 | } 77 | 78 | var ymlContent = File.ReadAllText(ymlPath); 79 | 80 | localConfiguration = _deserializer.Deserialize(ymlContent); 81 | 82 | return true; 83 | } 84 | 85 | public void ApplyLocalEnvironmentForProcess(string path, ProcessStartInfo process, LocalConfiguration? localConfiguration = null) 86 | { 87 | if (localConfiguration is not null || TryLoad(path, out localConfiguration)) 88 | { 89 | if (!string.IsNullOrEmpty(localConfiguration?.Environment?.Name)) 90 | { 91 | environmentManager.SetEnvironmentForProcess( 92 | localConfiguration.Environment.Name, 93 | process); 94 | } 95 | 96 | if (localConfiguration!.Environment?.Variables != null) 97 | { 98 | environmentManager.SetEnvironmentVariablesForProcess(process, localConfiguration!.Environment!.Variables); 99 | } 100 | } 101 | } 102 | } 103 | 104 | public enum FileSearchDirection : byte 105 | { 106 | Ascendants, 107 | Descendants, 108 | OnlyCurrent 109 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Notifications/DefaultNotificationManager.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools.Notifications; 2 | public class DefaultNotificationManager : INotificationManager 3 | { 4 | public Task SendAsync(string title, string message, string icon = null) 5 | { 6 | return Task.CompletedTask; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AbpDevTools/Notifications/INotificationManager.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools.Notifications; 2 | public interface INotificationManager 3 | { 4 | Task SendAsync(string title, string message = null, string icon = null); 5 | } 6 | -------------------------------------------------------------------------------- /src/AbpDevTools/Notifications/MacCatalystNotificationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using AbpDevTools.Configuration; 3 | 4 | namespace AbpDevTools.Notifications; 5 | public class MacCatalystNotificationManager : INotificationManager 6 | { 7 | protected readonly ToolsConfiguration toolsConfiguration; 8 | protected readonly NotificationConfiguration notificationConfiguration; 9 | 10 | public MacCatalystNotificationManager(ToolsConfiguration toolsConfiguration, NotificationConfiguration notificationConfiguration) 11 | { 12 | this.toolsConfiguration = toolsConfiguration; 13 | this.notificationConfiguration = notificationConfiguration; 14 | } 15 | 16 | public async Task SendAsync(string title, string message = null, string icon = null) 17 | { 18 | if(!notificationConfiguration.GetOptions().Enabled){ 19 | return; 20 | } 21 | 22 | var tools = toolsConfiguration.GetOptions(); 23 | 24 | var process = Process.Start(tools["osascript"], $"-e \"display notification \\\"{message}\\\" with title \\\"{title}\\\"\""); 25 | 26 | await process.WaitForExitAsync(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AbpDevTools/Notifications/WindowsNotificationManager.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | using System.Diagnostics; 3 | 4 | namespace AbpDevTools.Notifications; 5 | public class WindowsNotificationManager : INotificationManager 6 | { 7 | private string FolderPath => Path.Combine( 8 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 9 | "abpdev"); 10 | 11 | protected readonly ToolsConfiguration toolsConfiguration; 12 | protected readonly NotificationConfiguration notificationConfiguration; 13 | 14 | public WindowsNotificationManager(ToolsConfiguration toolsConfiguration, NotificationConfiguration notificationConfiguration) 15 | { 16 | this.toolsConfiguration = toolsConfiguration; 17 | this.notificationConfiguration = notificationConfiguration; 18 | } 19 | 20 | public async Task SendAsync(string title, string message = null, string icon = null) 21 | { 22 | if (!notificationConfiguration.GetOptions().Enabled) 23 | { 24 | return; 25 | } 26 | 27 | var command = "New-BurntToastNotification"; 28 | 29 | command += $" -Text \"{title}\""; 30 | 31 | if (!string.IsNullOrEmpty(message)) 32 | { 33 | command += $", \"{message}\""; 34 | } 35 | 36 | if (!string.IsNullOrEmpty(icon)) 37 | { 38 | command += $" -AppLogo \"{icon}\""; 39 | } 40 | 41 | var fileName = Guid.NewGuid() + ".ps1"; 42 | 43 | var filePath = Path.Combine(FolderPath, fileName); 44 | 45 | await File.WriteAllTextAsync(filePath, command); 46 | 47 | var tools = toolsConfiguration.GetOptions(); 48 | var process = Process.Start(tools["powershell"], filePath); 49 | 50 | await process.WaitForExitAsync(); 51 | 52 | File.Delete(filePath); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AbpDevTools/Platform.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace AbpDevTools; 5 | 6 | [RegisterTransient] 7 | public class Platform 8 | { 9 | public void Open(string filePath) 10 | { 11 | OpenProcess(filePath).WaitForExit(); 12 | } 13 | 14 | public Task OpenAsync(string filePath) 15 | { 16 | return OpenProcess(filePath).WaitForExitAsync(); 17 | } 18 | 19 | private Process OpenProcess(string filePath) 20 | { 21 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 22 | { 23 | return Process.Start(new ProcessStartInfo("explorer", filePath)); 24 | } 25 | else 26 | { 27 | return Process.Start(new ProcessStartInfo("open", filePath)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AbpDevTools/Program.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Commands; 2 | using AbpDevTools.Commands.Migrations; 3 | using AbpDevTools.Notifications; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | using YamlDotNet.Serialization; 8 | using YamlDotNet.Serialization.NamingConventions; 9 | 10 | namespace AbpDevTools; 11 | 12 | public class Program 13 | { 14 | public static async Task Main() => 15 | await new CliApplicationBuilder() 16 | .SetExecutableName("abpdev") 17 | .SetDescription("A set of tools to make development with ABP easier.") 18 | .SetTitle("Abp Dev Tools") 19 | .BuildServices() 20 | .Build() 21 | .RunAsync(); 22 | } 23 | 24 | public static class Startup 25 | { 26 | public static CliApplicationBuilder BuildServices(this CliApplicationBuilder builder) 27 | { 28 | Console.OutputEncoding = Encoding.UTF8; 29 | 30 | var services = new ServiceCollection(); 31 | 32 | var commands = new Type[] // Keep this instead reflection for performance 33 | { 34 | typeof(BuildCommand), 35 | typeof(ConfigurationClearCommand), 36 | typeof(ReplacementConfigClearCommand), 37 | typeof(EnvironmentAppConfigClearCommand), 38 | typeof(RunConfigClearCommand), 39 | typeof(CleanConfigClearCommand), 40 | typeof(ToolsConfigClearCommand), 41 | typeof(ReplaceConfigurationCommand), 42 | typeof(EnvironmentAppConfigurationCommand), 43 | typeof(RunConfigurationCommand), 44 | typeof(CleanConfigurationCommand), 45 | typeof(ToolsConfigurationCommand), 46 | typeof(ToolsCommand), 47 | typeof(ConfigCommand), 48 | typeof(DisableNotificationsCommand), 49 | typeof(EnableNotificationsCommand), 50 | typeof(EnvironmentAppCommand), 51 | typeof(EnvironmentAppStartCommand), 52 | typeof(EnvironmentAppStopCommand), 53 | typeof(LogsClearCommand), 54 | typeof(LogsCommand), 55 | typeof(MigrateCommand), 56 | typeof(ReplaceCommand), 57 | typeof(RunCommand), 58 | typeof(EnvironmentCommand), 59 | typeof(EnvironmentConfigurationCommand), 60 | typeof(AbpBundleCommand), 61 | typeof(AbpBundleListCommand), 62 | typeof(TestCommand), 63 | typeof(UpdateCheckCommand), 64 | typeof(CleanCommand), 65 | typeof(DatabaseDropCommand), 66 | typeof(SwitchToEnvironmentCommand), 67 | typeof(FindFileCommand), 68 | typeof(MigrationsCommand), 69 | typeof(AddMigrationCommand), 70 | typeof(ClearMigrationsCommand), 71 | typeof(PrepareCommand), 72 | }; 73 | 74 | foreach (var commandType in commands) 75 | { 76 | if (!commandType.IsAbstract && typeof(ICommand).IsAssignableFrom(commandType)) 77 | { 78 | builder.AddCommand(commandType); 79 | services.AddSingleton(commandType); 80 | } 81 | } 82 | 83 | services.AutoRegisterFromAbpDevTools(); 84 | 85 | var yamlSerializer = new SerializerBuilder() 86 | .WithNamingConvention(HyphenatedNamingConvention.Instance) 87 | .Build(); 88 | 89 | services.AddSingleton(yamlSerializer); 90 | 91 | services.AddSingleton( 92 | new DeserializerBuilder() 93 | .WithNamingConvention(HyphenatedNamingConvention.Instance) 94 | .Build()); 95 | 96 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 97 | { 98 | services.AddTransient(); 99 | } 100 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 101 | { 102 | services.AddTransient(); 103 | } 104 | else 105 | { 106 | services.AddTransient(); 107 | } 108 | 109 | var serviceProvider = services.BuildServiceProvider(); 110 | 111 | return builder.UseTypeActivator(serviceProvider); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/AbpDevTools/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "AbpDevTools": { 4 | "commandName": "Project", 5 | "commandLineArgs": "" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Services/EntityFrameworkCoreProjectsProvider.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools.Services; 2 | 3 | [RegisterSingleton] 4 | public class EntityFrameworkCoreProjectsProvider 5 | { 6 | public FileInfo[] GetEfCoreProjects(string path) 7 | { 8 | return Directory.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories) 9 | .Where(DoesHaveEfCoreReference) 10 | .Select(x => new FileInfo(x)) 11 | .ToArray(); 12 | } 13 | 14 | private static bool DoesHaveEfCoreReference(string projectPath) 15 | { 16 | using var fs = new FileStream(projectPath, FileMode.Open, FileAccess.Read); 17 | using var sr = new StreamReader(fs); 18 | 19 | while (!sr.EndOfStream) 20 | { 21 | var line = sr.ReadLine(); 22 | if (string.IsNullOrEmpty(line)) 23 | { 24 | continue; 25 | } 26 | 27 | if (line.Contains("Microsoft.EntityFrameworkCore")) 28 | { 29 | return true; 30 | } 31 | } 32 | 33 | return false; 34 | } 35 | } -------------------------------------------------------------------------------- /src/AbpDevTools/Services/RunnableProjectsProvider.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Configuration; 2 | 3 | namespace AbpDevTools.Services; 4 | 5 | [RegisterSingleton] 6 | public class RunnableProjectsProvider 7 | { 8 | public RunnableProjectsProvider(RunConfiguration runConfiguration) 9 | { 10 | runConfiguration.CleanObsolete(); 11 | } 12 | 13 | public FileInfo[] GetRunnableProjects(string path) 14 | { 15 | return Directory.EnumerateFiles(path, "*.csproj", SearchOption.AllDirectories) 16 | .Where(DoesHaveProgramClass) 17 | .Select(x => new FileInfo(x)) 18 | .ToArray(); 19 | } 20 | 21 | private static bool DoesHaveProgramClass(string projectPath) 22 | { 23 | return File.Exists( 24 | Path.Combine( 25 | Path.GetDirectoryName(projectPath)!, 26 | "Program.cs" 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AbpDevTools/Services/UpdateCheckResult.cs: -------------------------------------------------------------------------------- 1 | namespace AbpDevTools.Services; 2 | public class UpdateCheckResult 3 | { 4 | public UpdateCheckResult( 5 | bool updateAvailable, 6 | string currentVersion, 7 | string latestVersion, 8 | DateTime lastCheckDate) 9 | { 10 | UpdateAvailable = updateAvailable; 11 | CurrentVersion = currentVersion; 12 | LatestVersion = latestVersion; 13 | LastCheckDate = lastCheckDate; 14 | } 15 | 16 | public bool UpdateAvailable { get; } 17 | public string CurrentVersion { get; } 18 | public string LatestVersion { get; } 19 | public DateTime LastCheckDate { get; } 20 | } 21 | -------------------------------------------------------------------------------- /src/AbpDevTools/Services/UpdateChecker.cs: -------------------------------------------------------------------------------- 1 | using AbpDevTools.Commands; 2 | using System.Net.Http.Json; 3 | using System.Text.Json; 4 | 5 | namespace AbpDevTools.Services; 6 | 7 | [RegisterTransient] 8 | public class UpdateChecker 9 | { 10 | public static string FolderPath => Path.Combine( 11 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 12 | "abpdev"); 13 | 14 | public static string FilePath => Path.Combine(FolderPath, "updates.json"); 15 | 16 | public async Task CheckAsync(bool force = false) 17 | { 18 | var data = await GetDataAsync(); 19 | 20 | var currentVersion = typeof(UpdateCheckCommand).Assembly.GetName().Version; 21 | 22 | if (force || DateTime.Now >= data.NextCheck) 23 | { 24 | using var httpClient = new HttpClient(); 25 | 26 | var nugetResponse = await httpClient 27 | .GetFromJsonAsync>("https://api.nuget.org/v3-flatcontainer/abpdevtools/index.json"); 28 | 29 | var latestVersion = nugetResponse["versions"].Where(x => !x.Contains("-")).Last(); 30 | 31 | data.LastChechLatestVersion = latestVersion; 32 | data.LastCheck = DateTime.Now; 33 | data.NextCheck = DateTime.Now.AddDays(1); 34 | await SaveDataAsync(data); 35 | 36 | return new UpdateCheckResult( 37 | currentVersion < Version.Parse(latestVersion), 38 | currentVersion.ToString(), 39 | latestVersion, 40 | DateTime.Now); 41 | } 42 | 43 | return new UpdateCheckResult( 44 | currentVersion < Version.Parse(data.LastChechLatestVersion), 45 | currentVersion.ToString(), 46 | data.LastChechLatestVersion, 47 | data.LastCheck); 48 | } 49 | 50 | private async Task GetDataAsync() 51 | { 52 | if (!File.Exists(FilePath)) 53 | { 54 | await SaveDataAsync(new UpdatesData()); 55 | } 56 | 57 | var json = await File.ReadAllTextAsync(FilePath); 58 | return JsonSerializer.Deserialize(json); 59 | } 60 | 61 | private async Task SaveDataAsync(UpdatesData data) 62 | { 63 | if (!Directory.Exists(FolderPath)) 64 | { 65 | Directory.CreateDirectory(FolderPath); 66 | } 67 | await File.WriteAllTextAsync(FilePath, JsonSerializer.Serialize(data)); 68 | } 69 | 70 | public class UpdatesData 71 | { 72 | public DateTime LastCheck { get; set; } = DateTime.Now; 73 | public DateTime NextCheck { get; set; } = DateTime.Now; 74 | public string LastChechLatestVersion { get; set; } = "1.0.0"; 75 | } 76 | } 77 | --------------------------------------------------------------------------------