diff --git a/tools/AzDev/AzDev/AzDev.format.ps1xml b/tools/AzDev/AzDev/AzDev.format.ps1xml
new file mode 100644
index 000000000000..b3683b00ecb1
--- /dev/null
+++ b/tools/AzDev/AzDev/AzDev.format.ps1xml
@@ -0,0 +1,51 @@
+
+
+
+
+ PSProject
+
+ AzDev.Models.PSModels.PSProject
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Path
+
+
+
+
+
+
+
+ PSModule
+
+ AzDev.Models.PSModels.PSModule
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/AzDev/AzDev/AzDev.psd1 b/tools/AzDev/AzDev/AzDev.psd1
new file mode 100644
index 000000000000..088766402bd1
--- /dev/null
+++ b/tools/AzDev/AzDev/AzDev.psd1
@@ -0,0 +1,135 @@
+#
+# Module manifest for module 'AzDev'
+#
+# Generated by: Yeming Liu
+#
+# Generated on: 7/5/2024
+#
+
+@{
+
+# Script module or binary module file associated with this manifest.
+RootModule = 'AzDev.psm1'
+
+# Version number of this module.
+ModuleVersion = '0.0.1'
+
+# Supported PSEditions
+# CompatiblePSEditions = @()
+
+# ID used to uniquely identify this module
+GUID = 'e989d571-a6ed-4912-bf5c-7b0c55261607'
+
+# Author of this module
+Author = 'Microsoft Corporation'
+
+# Company or vendor of this module
+CompanyName = 'Microsoft Corporation'
+
+# Copyright statement for this module
+Copyright = 'Microsoft Corporation. All rights reserved.'
+
+# Description of the functionality provided by this module
+# Description = ''
+
+# Minimum version of the PowerShell engine required by this module
+# PowerShellVersion = ''
+
+# Name of the PowerShell host required by this module
+# PowerShellHostName = ''
+
+# Minimum version of the PowerShell host required by this module
+# PowerShellHostVersion = ''
+
+# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# DotNetFrameworkVersion = ''
+
+# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# ClrVersion = ''
+
+# Processor architecture (None, X86, Amd64) required by this module
+# ProcessorArchitecture = ''
+
+# Modules that must be imported into the global environment prior to importing this module
+# RequiredModules = @()
+
+# Assemblies that must be loaded prior to importing this module
+# RequiredAssemblies = @()
+
+# Script files (.ps1) that are run in the caller's environment prior to importing this module.
+# ScriptsToProcess = @()
+
+# Type files (.ps1xml) to be loaded when importing this module
+# TypesToProcess = @()
+
+# Format files (.ps1xml) to be loaded when importing this module
+FormatsToProcess = @('AzDev.format.ps1xml')
+
+# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+NestedModules = @('bin/AzDev.dll',
+ 'CommonRepo.psm1')
+
+# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+FunctionsToExport = 'Connect-DevCommonRepo', 'Disconnect-DevCommonRepo'
+
+# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+CmdletsToExport = 'Get-DevContext', 'Set-DevContext',
+ 'Get-DevModule', 'Get-DevProject',
+ 'Open-DevSwagger'
+
+# Variables to export from this module
+VariablesToExport = '*'
+
+# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+AliasesToExport = '*'
+
+# DSC resources to export from this module
+# DscResourcesToExport = @()
+
+# List of all modules packaged with this module
+# ModuleList = @()
+
+# List of all files packaged with this module
+# FileList = @()
+
+# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+PrivateData = @{
+
+ PSData = @{
+
+ # Tags applied to this module. These help with module discovery in online galleries.
+ # Tags = @()
+
+ # A URL to the license for this module.
+ # LicenseUri = ''
+
+ # A URL to the main website for this project.
+ # ProjectUri = ''
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ # ReleaseNotes = ''
+
+ # Prerelease string of this module
+ # Prerelease = ''
+
+ # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+ # RequireLicenseAcceptance = $false
+
+ # External dependent modules of this module
+ # ExternalModuleDependencies = @()
+
+ } # End of PSData hashtable
+
+} # End of PrivateData hashtable
+
+# HelpInfo URI of this module
+# HelpInfoURI = ''
+
+# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+# DefaultCommandPrefix = ''
+
+}
+
diff --git a/tools/AzDev/AzDev/AzDev.psm1 b/tools/AzDev/AzDev/AzDev.psm1
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tools/AzDev/AzDev/CommonRepo.psm1 b/tools/AzDev/AzDev/CommonRepo.psm1
new file mode 100644
index 000000000000..f8acf5f5f9fc
--- /dev/null
+++ b/tools/AzDev/AzDev/CommonRepo.psm1
@@ -0,0 +1,115 @@
+# ----------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# Code generated by Microsoft (R) AutoRest Code Generator.Changes may cause incorrect behavior and will be lost if the code
+# is regenerated.
+# ----------------------------------------------------------------------------------
+
+<#
+ .Synopsis
+ Connects azure-powershell repo to azure-powershell-common repo for debugging.
+
+ .Description
+ Connects azure-powershell repo to azure-powershell-common repo for debugging.
+
+ .Parameter CommonRepoPath
+ Path to the common repo. Relative or absolute.
+
+ .Example
+ Connect-CommonRepo -CommonRepoPath ../azure-powershell-common
+#>
+function Connect-DevCommonRepo {
+ [CmdletBinding()]
+ param()
+
+ $context = Get-DevContext
+
+ Write-Host "1/2 Adding common projects to sln and csproj"
+
+ $CommonRepoPath = $context.AzurePowerShellCommonRepositoryRoot
+ $CommonProjects = Get-ChildItem -Path "$CommonRepoPath/src/" -Include *.csproj -Exclude *.test.* -Recurse
+ $CommonProjects = $CommonProjects.FullName
+
+
+ $RepoRoot = $Context.AzurePowerShellRepositoryRoot
+
+ Push-Location "$RepoRoot/src/Accounts"
+ try {
+ foreach ($csproj in $CommonProjects) {
+ $csproj = [System.IO.Path]::GetFullPath($csproj)
+ dotnet sln add $csproj
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to add $csproj to Accounts.sln"
+ }
+ <#
+ known common project references:
+ Authentication.csproj -> Authentication.Abstractions, ResourceManager
+ Accounts.csproj -> Authentication.Abstractions, ResourceManager, Common
+ Accounts.Test.csproj -> Authentication.Abstractions, ResourceManager, Common
+ TestFx.csproj -> Graph.Rbac.csproj
+ AssemblyLoading.csproj -> Common
+ #>
+ # add all common projects to Authentication.csproj because it will be referenced by most Az projects
+ dotnet add ./Authentication/Authentication.csproj reference $csproj
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to add $csproj to Authentication.csproj"
+ }
+ }
+
+ # AssemblyLoading.csproj references Common.csproj and does not reference Authentication.csproj
+ dotnet add ./AssemblyLoading/AssemblyLoading.csproj reference "$CommonRepoPath/src/Common/Common.csproj"
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to add Common.csproj to AssemblyLoading.csproj"
+ }
+
+ # add common project references below for csproj which does not reference Authentication.csproj
+ }
+ finally {
+ Pop-Location
+ }
+
+
+ Write-Host "2/2: Remove the dependency of those common projects from .targets file"
+
+ $Patterns = @(
+ '"
+ }
+ else {
+ return $line
+ }
+ } | Set-Content $TargetsFile
+
+ Write-Host "Done connecting both repositories."
+}
+
+function Disconnect-DevCommonRepo {
+ Write-Host "Please run the following commands to undo Connect-CommonRepo. Double check those files do not have wanted changes.
+ git checkout -- ./src/Accounts/Accounts.sln
+ git checkout -- ./src/Accounts/AssemblyLoading/AssemblyLoading.csproj
+ git checkout -- ./src/Accounts/Authentication/Authentication.csproj
+ git checkout -- ./tools/Common.Netcore.Dependencies.targets
+ "
+}
+
+Export-ModuleMember -Function Connect-DevCommonRepo, Disconnect-DevCommonRepo
diff --git a/tools/AzDev/CHANGELOG.md b/tools/AzDev/CHANGELOG.md
new file mode 100644
index 000000000000..aee3e46030d3
--- /dev/null
+++ b/tools/AzDev/CHANGELOG.md
@@ -0,0 +1,40 @@
+## Next
+- Feature: further distinguish track 1/2 SDKs, data/management plane, package/project.
+- Local static analysis (Invoke-AzStaticAnalyzer)
+- New-TestEnvironment and other existing tools
+- Scripts to help deploy compliant resources
+- Look into tools/, what else can be ported here?
+- Install daily build
+- Check what CLI's az dev support?
+- Wildcard of project and module names - need to implement by hand? Can leverage `DirectoryInfo.GetFiles`? It supports kind of wildcard.
+- Quick start templates
+- Versioning and publishing AzDev module
+
+## 2025/1/2
+- Misc: moved to azure-powershell repo
+- Feature: Connect common repo and ps repo
+
+## 2024/12/30
+- Fix: path issue on Windows
+- Misc: Better error message when no context
+
+## 2024/12/27
+- Feature: Distinguish SDK-based projects, wrapper projects and other projects.
+- Feature: Distinguish modules types: SdkBased, autorestBased, Hybrid.
+
+## 2024/12/25
+- Fix: Inventory cmdlets ignore live tests
+
+## 2024/10/15
+- Feature: Add reason for deducing project type
+
+## 2024/9/11
+- Feature: Open swagger link
+- Rename SDK-based project type to "Other" because there's no accurate way to distinguish between SDK and wrapper projects
+
+## Older
+- Fix: Two misidentified projects because of live tests: ContainerRegistry and Resources
+- Feature: Get-DevProject: Add support for `-Type` parameter
+- Feature: Add new ProjectType: Test, LegacyHelper, Track1Sdk
+- Renamed "Submodule" to "Project"
+- Feature: Added "Get-DevProject" cmdlet
diff --git a/tools/AzDev/README.md b/tools/AzDev/README.md
new file mode 100644
index 000000000000..4430dc8e8718
--- /dev/null
+++ b/tools/AzDev/README.md
@@ -0,0 +1,84 @@
+# AzDev - developer module for Azure PowerShell
+
+This module is designed to help developers of Azure PowerShell modules. It provides tools to assist development, troubleshooting issues, etc.
+
+All the cmdlets in this module are prefixed with `Dev-` to avoid conflicts with other modules.
+
+## Quick start
+
+```powershell
+# build the module
+./tools/AzDev/build.ps1
+# import the module
+Import-Module ./artifacts/AzDev/AzDev.psd1
+# set up the context (only once)
+Set-DevContext -RepoRoot 'C:\repos\azure-powershell'
+```
+
+## Features
+
+### Repo inventory
+
+`Get-DevModule` and `Get-DevProject` are the main cmdlets to get the inventory of the repo.
+
+```powershell
+PS /> Get-DevModule | Select-Object -First 10
+
+Name Type Path
+---- ---- ----
+Maps AutoRestBased /Users/azps/workspace/azure-powershell/src/Maps
+Kusto AutoRestBased /Users/azps/workspace/azure-powershell/src/Kusto
+StorageMover AutoRestBased /Users/azps/workspace/azure-powershell/src/StorageMover
+ResourceGraph Hybrid /Users/azps/workspace/azure-powershell/src/ResourceGraph
+Terraform AutoRestBased /Users/azps/workspace/azure-powershell/src/Terraform
+PostgreSql AutoRestBased /Users/azps/workspace/azure-powershell/src/PostgreSql
+SpringCloud AutoRestBased /Users/azps/workspace/azure-powershell/src/SpringCloud
+ManagedNetworkFabric AutoRestBased /Users/azps/workspace/azure-powershell/src/ManagedNetworkFabric
+ServiceBus Hybrid /Users/azps/workspace/azure-powershell/src/ServiceBus
+Mdp AutoRestBased /Users/azps/workspace/azure-powershell/src/Mdp
+
+PS /> Get-DevProject | Group-Object -Property Type | Select-Object -Property Name,Count | Sort-Object -Property Count -Descending
+
+Name Count
+---- -----
+AutoRestBased 163
+Wrapper 127
+SdkBased 76
+Test 70
+Track1Sdk 48
+Other 8
+LegacyHelper 4
+```
+
+### Connect azure-powershell and azure-powershell-common
+
+Help you connect the azure-powershell and azure-powershell-common repositories for developing or debugging.
+
+```powershell
+# Connect
+Connect-CommonRepo
+
+# Disconnect
+Disconnect-CommonRepo
+```
+
+### Autorest helper
+
+#### Open swagger online
+
+`Open-DevSwagger` opens the online version of the specified swagger file. It's useful when you want to quickly check the structure of a swagger file.
+
+```powershell
+PS /> Open-DevSwagger workloads
+Multiple projects matching [workloads]
+ 1: SapVirtualInstance.Autorest
+ 2: Monitors.Autorest
+Enter the number corresponding to your selection
+1
+Multiple swagger references found in [SapVirtualInstance.Autorest]
+ 1: $(repo)/specification/workloads/resource-manager/Microsoft.Workloads/SAPVirtualInstance/readme.md
+ 2: $(repo)/specification/workloads/resource-manager/readme.powershell.md
+Enter the number corresponding to your selection
+1
+Opening https://github.com/Azure/azure-rest-api-specs/blob/202321f386ea5b0c103b46902d43b3d3c50e029c/specification/workloads/resource-manager/Microsoft.Workloads/SAPVirtualInstance/readme.md in default browser...
+```
\ No newline at end of file
diff --git a/tools/AzDev/Tests/DefaultContextProviderTests.cs b/tools/AzDev/Tests/DefaultContextProviderTests.cs
new file mode 100644
index 000000000000..ea3bdb454282
--- /dev/null
+++ b/tools/AzDev/Tests/DefaultContextProviderTests.cs
@@ -0,0 +1,52 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Services;
+using Xunit.Sdk;
+
+namespace AzDev.Tests;
+
+public class DefaultContextProviderTests
+{
+ [Fact]
+ public void ContextIO()
+ {
+ var fs = new MockFileSystem();
+ const string profilePath = @"C:\DevContext.json";
+ var contextProvider = new DefaultContextProvider(profilePath, fs);
+ // context file should not exist
+ Assert.False(fs.FileExists(profilePath));
+ Assert.Throws(contextProvider.LoadContext);
+
+ var context = new AzDev.Models.DevContext()
+ {
+ AzurePowerShellRepositoryRoot = @"D:\azure-powershell"
+ };
+ contextProvider.SaveContext(context);
+ // you can save multiple times
+ contextProvider.SaveContext(context);
+ // now the context file should exist
+ Assert.True(fs.FileExists(profilePath));
+ // the in-memory context should be updated
+ Assert.Equal(context.AzurePowerShellRepositoryRoot, contextProvider.LoadContext().AzurePowerShellRepositoryRoot);
+ // force reload from disk
+ contextProvider = new DefaultContextProvider(profilePath, fs);
+ Assert.Equal(context.AzurePowerShellRepositoryRoot, contextProvider.LoadContext().AzurePowerShellRepositoryRoot);
+ }
+
+ [Fact]
+ public void LoadFromDisk()
+ {
+ const string profilePath = @"C:\DevContext.json";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ { profilePath, new MockFileData(@"{
+ ""AzurePowerShellRepositoryRoot"":""D:\\azure-powershell"",
+ ""AzurePowerShellCommonRepositoryRoot"":""D:\\azure-powershell-common"",
+ ""UnsupportedProperty"":""value""
+ }") }
+ }); // UnsupportedProperty should not block deserialization
+ var contextProvider = new DefaultContextProvider(profilePath, fs);
+ var context = contextProvider.LoadContext();
+ Assert.Equal(@"D:\azure-powershell", context.AzurePowerShellRepositoryRoot);
+ Assert.Equal(@"D:\azure-powershell-common", context.AzurePowerShellCommonRepositoryRoot);
+ }
+}
\ No newline at end of file
diff --git a/tools/AzDev/Tests/HelperTests/ConventionTests.cs b/tools/AzDev/Tests/HelperTests/ConventionTests.cs
new file mode 100644
index 000000000000..c53eb5d466b7
--- /dev/null
+++ b/tools/AzDev/Tests/HelperTests/ConventionTests.cs
@@ -0,0 +1,244 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Models.Inventory;
+using AzDev.Services;
+
+namespace AzDev.Tests;
+
+public class ConventionTests
+{
+ [Fact]
+ public void CanDetectLegacyHelperProject()
+ {
+ var path = "C:/path/to/project/Project.helper";
+ Assert.True(Conventions.IsLegacyHelperProject(path, out var reason));
+ Assert.NotNull(reason);
+
+ path = "C:/path/to/project/Project.helpers";
+ Assert.True(Conventions.IsLegacyHelperProject(path, out reason));
+ Assert.NotNull(reason);
+
+ path = "C:/path/to/project/Compute";
+ Assert.False(Conventions.IsLegacyHelperProject(path, out reason));
+ Assert.NotNull(reason);
+ }
+
+ [Fact]
+ public void CanDetectTestProject()
+ {
+ var path = "C:/path/to/project/Project.test";
+ Assert.True(Conventions.IsTestProject(path, out var reason));
+ Assert.NotNull(reason);
+
+ path = "C:/path/to/project/Compute";
+ Assert.False(Conventions.IsTestProject(path, out reason));
+ Assert.NotNull(reason);
+ }
+
+ [Fact]
+ public void CanDetectTrack1SdkProject()
+ {
+ var path = "C:/path/to/project/Project.management.sdk";
+ Assert.True(Conventions.IsTrack1SdkProject(path, out var reason));
+ Assert.NotNull(reason);
+
+ path = "C:/path/to/project/Compute";
+ Assert.False(Conventions.IsTrack1SdkProject(path, out reason));
+ Assert.NotNull(reason);
+ }
+
+ [Fact]
+ public void CanDetectAutorestBasedProject()
+ {
+ var path = "C:/path/to/project/Project.autorest";
+ Assert.True(Conventions.IsAutorestBasedProject(path, out var reason));
+ Assert.NotNull(reason);
+
+ path = "C:/path/to/project/Compute";
+ Assert.False(Conventions.IsAutorestBasedProject(path, out reason));
+ Assert.NotNull(reason);
+ }
+
+ [Fact]
+ public void CanDetectWrapperProject()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var moduleName = "Test";
+ var modulePath = $"{cd}{split}{moduleName}";
+ var wrapperProjPath = $"{modulePath}{split}Test";
+ var autorestProjPath = $"{modulePath}{split}Test.AutoRest";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ {
+ $"{autorestProjPath}{split}README.md", new MockFileData(
+ @""
+ )
+ },
+ {
+ $"{wrapperProjPath}{split}Test.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+ "
+ )
+ }
+ });
+
+ Assert.True(Conventions.IsWrapperProject(fs, wrapperProjPath, out var reason), $"Reason: {reason}");
+ Assert.True(Conventions.IsAutorestBasedProject(autorestProjPath, out reason), $"Reason: {reason}");
+ }
+
+ [Fact]
+ public void CanDetectWrapperProjectNegative()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var moduleName = "Test";
+ var modulePath = $"{cd}{split}{moduleName}";
+ var wrapperProjPath = $"{modulePath}{split}Test";
+ var autorestProjPath = $"{modulePath}{split}Test.AutoRest";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ {
+ $"{autorestProjPath}{split}README.md", new MockFileData(
+ @""
+ )
+ },
+ {
+ $"{wrapperProjPath}{split}Test.csproj", new MockFileData(
+ @"
+
+ netstandard2.0'
+
+
+ "
+ )
+ }
+ });
+
+ Assert.False(Conventions.IsWrapperProject(fs, wrapperProjPath, out var reason), $"Reason: {reason}");
+ }
+
+ [Fact]
+ public void CanDetectSdkBasedProject()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var moduleName = "Test";
+ var modulePath = $"{cd}{split}{moduleName}";
+ var wrapperProjPath = $"{modulePath}{split}Test";
+ var autorestProjPath = $"{modulePath}{split}Test.AutoRest";
+ var track1SdkBasedProjPath = $"{modulePath}{split}Test.Management";
+ var newSdkBasedProjPath = $"{modulePath}{split}Test2";
+ var helperSdkBasedProjPath = $"{modulePath}{split}Test3";
+ var track1DataPlaneSdkBasedProjPath = $"{modulePath}{split}Test.DataPlane";
+ var track2DataPlaneSdkBasedProjPath = $"{modulePath}{split}Test.DataPlane2";
+ var otherProjPath = $"{modulePath}{split}Test.Other";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ {
+ $"{autorestProjPath}{split}README.md", new MockFileData(
+ @""
+ )
+ },
+ {
+ $"{wrapperProjPath}{split}Test.csproj", new MockFileData(
+ @"
+
+ netstandard2.0'
+
+ "
+ )
+ },
+ {
+ $"{track1SdkBasedProjPath}{split}Test.Management.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ },
+ {
+ $"{otherProjPath}{split}Other.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ },
+ {
+ $"{newSdkBasedProjPath}{split}Test2.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ },
+ {
+ $"{track1DataPlaneSdkBasedProjPath}{split}Test.DataPlane.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ },
+ {
+ $"{track2DataPlaneSdkBasedProjPath}{split}Test.DataPlane2.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ },
+ {
+ $"{helperSdkBasedProjPath}{split}Test3.csproj", new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+ "
+ )
+ }
+ });
+ Assert.True(Conventions.IsSdkBasedProject(fs, track1SdkBasedProjPath, out var reason), $"Reason: {reason}");
+ Assert.True(Conventions.IsSdkBasedProject(fs, newSdkBasedProjPath, out reason), $"Reason: {reason}");
+ Assert.True(Conventions.IsSdkBasedProject(fs, helperSdkBasedProjPath, out reason), $"Reason: {reason}");
+ Assert.True(Conventions.IsSdkBasedProject(fs, track1DataPlaneSdkBasedProjPath, out reason), $"Reason: {reason}");
+ Assert.True(Conventions.IsSdkBasedProject(fs, track2DataPlaneSdkBasedProjPath, out reason), $"Reason: {reason}");
+ Assert.False(Conventions.IsSdkBasedProject(fs, wrapperProjPath, out reason), $"Reason: {reason}");
+ Assert.False(Conventions.IsSdkBasedProject(fs, autorestProjPath, out reason), $"Reason: {reason}");
+ Assert.False(Conventions.IsSdkBasedProject(fs, otherProjPath, out reason), $"Reason: {reason}");
+ }
+
+ [Fact]
+ public void CanDeductModuleType()
+ {
+ Project autorestProj = new AutoRestProject() {Type = ProjectType.AutoRestBased};
+ Project sdkProj = new SdkBasedProject() {Type = ProjectType.SdkBased};
+ Module hybridModule = new Module() {Projects = new List {autorestProj, sdkProj}};
+ Assert.Equal(ModuleType.Hybrid, Conventions.DeductModuleType(hybridModule.Projects, out _));
+
+ Project wrapperProj = new WrapperProject() {Type = ProjectType.Wrapper};
+ Module wrapperModule = new Module() {Projects = new List {wrapperProj, autorestProj}};
+ Assert.Equal(ModuleType.AutoRestBased, Conventions.DeductModuleType(wrapperModule.Projects, out _));
+
+ Project legacyHelperProj = new SdkBasedProject() {Type = ProjectType.LegacyHelper};
+ Project track1SdkProj = new SdkBasedProject() {Type = ProjectType.Track1Sdk};
+ Module sdkModule = new Module() {Projects = new List {sdkProj, legacyHelperProj}};
+ Assert.Equal(ModuleType.SdkBased, Conventions.DeductModuleType(sdkModule.Projects, out _));
+ sdkModule = new Module() {Projects = new List {sdkProj, track1SdkProj}};
+ Assert.Equal(ModuleType.SdkBased, Conventions.DeductModuleType(sdkModule.Projects, out _));
+ }
+}
diff --git a/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs b/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs
new file mode 100644
index 000000000000..d3d8099b56d8
--- /dev/null
+++ b/tools/AzDev/Tests/HelperTests/CsprojReaderTests.cs
@@ -0,0 +1,94 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Models.Inventory;
+using AzDev.Services;
+
+namespace AzDev.Tests;
+
+public class CsprojReaderTests
+{
+ [Fact]
+ public void CanParse()
+ {
+ var emptyProject = @"c:/repo/src/MyProject/MyProject.csproj";
+ var projectWithPackageReferences = @"c:/repo/src/MyProject/Package.csproj";
+ var projectWithProjectReferences = @"c:/repo/src/MyProject/Project.csproj";
+ var projectWithBothReferences = @"c:/repo/src/MyProject/Both.csproj";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ {
+ emptyProject, new MockFileData(
+ @"
+
+ netstandard2.0
+
+ "
+ )
+ },
+ {
+ projectWithPackageReferences, new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+
+
+
+ "
+ )
+ },
+ {
+ projectWithProjectReferences, new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+
+
+
+ "
+ )
+ },
+ {
+ projectWithBothReferences, new MockFileData(
+ @"
+
+ netstandard2.0
+
+
+
+
+
+
+
+ "
+ )
+ }
+ });
+
+ var project = CsprojReader.Parse(fs.File.ReadAllText(emptyProject));
+ Assert.Empty(project.PackageReferences);
+ Assert.Empty(project.ProjectReferences);
+
+ project = CsprojReader.Parse(fs.File.ReadAllText(projectWithPackageReferences));
+ Assert.Equal(2, project.PackageReferences.Count());
+ Assert.Contains("Newtonsoft.Json", project.PackageReferences);
+ Assert.Contains("System.Text.Json", project.PackageReferences);
+ Assert.Empty(project.ProjectReferences);
+
+ project = CsprojReader.Parse(fs.File.ReadAllText(projectWithProjectReferences));
+ Assert.Empty(project.PackageReferences);
+ Assert.Equal(2, project.ProjectReferences.Count());
+ Assert.Contains(@"..\MyProject\MyProject.csproj", project.ProjectReferences);
+ Assert.Contains(@"..\OtherProject\OtherProject.csproj", project.ProjectReferences);
+
+ project = CsprojReader.Parse(fs.File.ReadAllText(projectWithBothReferences));
+ Assert.Equal(2, project.PackageReferences.Count());
+ Assert.Contains("Newtonsoft.Json", project.PackageReferences);
+ Assert.Contains("System.Text.Json", project.PackageReferences);
+ Assert.Equal(2, project.ProjectReferences.Count());
+ Assert.Contains(@"..\MyProject\MyProject.csproj", project.ProjectReferences);
+ Assert.Contains(@"..\OtherProject\OtherProject.csproj", project.ProjectReferences);
+ }
+}
\ No newline at end of file
diff --git a/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs b/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs
new file mode 100644
index 000000000000..4d1ddfbc21bd
--- /dev/null
+++ b/tools/AzDev/Tests/HelperTests/FilterHelperTests.cs
@@ -0,0 +1,112 @@
+using AzDev.Models.Inventory;
+using AzDev.Services;
+
+namespace AzDev.Tests;
+
+public class FilterHelperTests
+{
+ [Fact]
+ public void CanFilterProjects()
+ {
+ var codebase = new Codebase
+ {
+ Modules = new List
+ {
+ new Module
+ {
+ Name = "FirstModule",
+ Projects = new List
+ {
+ new OtherProject { Name = "AlphaProject" }
+ }
+ },
+ new Module
+ {
+ Name = "SecondModule",
+ Projects = new List
+ {
+ new OtherProject { Name = "BetaProject" }
+ }
+ }
+ }
+ };
+
+ // filter by module name
+ var filter = "first";
+ var result = codebase.FilterProjects(filter);
+ Assert.Single(result);
+ Assert.Equal("AlphaProject", result.First().Name);
+
+ filter = "second";
+ result = codebase.FilterProjects(filter);
+ Assert.Single(result);
+ Assert.Equal("BetaProject", result.First().Name);
+
+ filter = "module";
+ result = codebase.FilterProjects(filter);
+ Assert.Equal(2, result.Count());
+
+ filter = "third";
+ result = codebase.FilterProjects(filter);
+ Assert.Empty(result);
+
+ // filter by project name
+ filter = "alpha";
+ result = codebase.FilterProjects(filter);
+ Assert.Single(result);
+ Assert.Equal("AlphaProject", result.First().Name);
+
+ filter = "beta";
+ result = codebase.FilterProjects(filter);
+ Assert.Single(result);
+ Assert.Equal("BetaProject", result.First().Name);
+
+ filter = "project";
+ result = codebase.FilterProjects(filter);
+ Assert.Equal(2, result.Count());
+
+ filter = "gamma";
+ result = codebase.FilterProjects(filter);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void FilterProjectsThrowsOnNullOrEmptyFilter()
+ {
+ var codebase = new Codebase();
+ Assert.Throws(() => codebase.FilterProjects(null));
+ Assert.Throws(() => codebase.FilterProjects(string.Empty));
+ }
+
+ [Fact]
+ public void ReturnBothWhenModuleAndProjectSameName()
+ {
+ var codebase = new Codebase
+ {
+ Modules = new List
+ {
+ new Module
+ {
+ Name = "FirstModule",
+ Projects = new List
+ {
+ new OtherProject { Name = "AlphaProject" }
+ }
+ },
+ new Module
+ {
+ Name = "SecondModule",
+ Projects = new List
+ {
+ new OtherProject { Name = "BetaProject (First)" }
+ }
+ }
+ }
+ };
+
+ var filter = "first";
+ var result = codebase.FilterProjects(filter);
+ Assert.Equal(2, result.Count());
+ }
+
+}
\ No newline at end of file
diff --git a/tools/AzDev/Tests/HelperTests/YamlTests.cs b/tools/AzDev/Tests/HelperTests/YamlTests.cs
new file mode 100644
index 000000000000..d87df51d76a0
--- /dev/null
+++ b/tools/AzDev/Tests/HelperTests/YamlTests.cs
@@ -0,0 +1,51 @@
+using AzDev.Models.Inventory;
+
+namespace AzDev.Tests;
+
+public class YamlTests
+{
+ [Fact]
+ public void CanDeserialize()
+ {
+ var yaml = @"module-version: 0.1.0
+title: Alb
+subject-prefix: $(service-name)
+inlining-threshold: 100
+
+# pin the swagger version by using the commit id instead of branch name
+commit: 1b338481329645df2d9460738cbaab6109472488
+require:
+# readme.azure.noprofile.md is the common configuration file
+ - $(this-folder)/../../readme.azure.noprofile.md
+ - $(repo)/specification/servicenetworking/resource-manager/readme.md
+
+try-require:
+ - $(repo)/specification/servicenetworking/resource-manager/readme.powershell.md
+
+directive:
+ # Fix swagger issues
+ - from: swagger-document
+ where: $.definitions.TrafficControllerUpdateProperties
+ transform: delete $['properties']";
+ var result = YamlHelper.Deserialize(yaml);
+ Assert.Equal("Alb", result.Title);
+ Assert.Equal("1b338481329645df2d9460738cbaab6109472488", result.Commit);
+ Assert.Equal(2, result.Require.Count());
+ Assert.Single(result.TryRequire);
+ Assert.Single(result.Directive);
+ Assert.Empty(result.InputFile);
+ }
+
+ [Fact]
+ public void DefaultValues()
+ {
+ var yaml = @"module-version: 0.1.0";
+ var result = YamlHelper.Deserialize(yaml);
+ Assert.Null(result.Title);
+ Assert.Null(result.Commit);
+ Assert.Empty(result.Require);
+ Assert.Empty(result.TryRequire);
+ Assert.Empty(result.Directive);
+ Assert.Empty(result.InputFile);
+ }
+}
diff --git a/tools/AzDev/Tests/ModelTests/ModuleTests.cs b/tools/AzDev/Tests/ModelTests/ModuleTests.cs
new file mode 100644
index 000000000000..fb1b4d634783
--- /dev/null
+++ b/tools/AzDev/Tests/ModelTests/ModuleTests.cs
@@ -0,0 +1,53 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Models.Inventory;
+
+namespace AzDev.Tests;
+
+public class ModuleTests
+{
+ [Fact]
+ public void CanCreateFromFileSystem()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var moduleName = "Test";
+ var path = $"{cd}{split}{moduleName}";
+ var projectName = "Test.AutoRest";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ { $"{path}{split}{projectName}{split}Test.csproj", new MockFileData(
+ @""
+ )}
+ });
+
+ var module = Module.FromFileSystem(fs, path);
+ Assert.Equal(path, module.Path);
+ Assert.Equal(moduleName, module.Name);
+ Assert.Single(module.Projects);
+ }
+
+ [Fact]
+ public void CanRecognizeBothProjectTypes()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var moduleName = "Test";
+ var path = $"{cd}{split}{moduleName}";
+ var sdkProjectName = "Beta";
+ var generatedProjectName = "Alpha.AutoRest";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ { $"{path}{split}{generatedProjectName}{split}Alpha.csproj", new MockFileData(
+ @""
+ )},
+ { $"{path}{split}{sdkProjectName}{split}Beta.csproj", new MockFileData(
+ @""
+ )},
+ });
+
+ var module = Module.FromFileSystem(fs, path);
+ Assert.Equal(2, module.Projects.Count());
+ Assert.Equal(ProjectType.AutoRestBased, module.Projects.ElementAt(0).Type);
+ Assert.Equal(ProjectType.Other, module.Projects.ElementAt(1).Type);
+ }
+}
diff --git a/tools/AzDev/Tests/ModelTests/ProjectTests.cs b/tools/AzDev/Tests/ModelTests/ProjectTests.cs
new file mode 100644
index 000000000000..067a4f79bb2c
--- /dev/null
+++ b/tools/AzDev/Tests/ModelTests/ProjectTests.cs
@@ -0,0 +1,27 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Models.Inventory;
+
+namespace AzDev.Tests;
+
+public class ProjectTests
+{
+ [Fact]
+ public void CanCreateFromFileSystem()
+ {
+ var cd = Directory.GetCurrentDirectory();
+ var split = Path.DirectorySeparatorChar;
+ var projectName = "Test.AutoRest";
+ var path = $"{cd}{split}{projectName}";
+ var readme = $"{path}{split}README.md";
+ var fs = new MockFileSystem(new Dictionary
+ {
+ { readme, new MockFileData(
+ @""
+ )}
+ });
+
+ var project = Project.FromFileSystem(fs, path);
+ Assert.Equal(path, project.Path);
+ Assert.Equal(projectName, project.Name);
+ }
+}
diff --git a/tools/AzDev/Tests/ModelTests/SwaggerTests.cs b/tools/AzDev/Tests/ModelTests/SwaggerTests.cs
new file mode 100644
index 000000000000..4a62023e584f
--- /dev/null
+++ b/tools/AzDev/Tests/ModelTests/SwaggerTests.cs
@@ -0,0 +1,27 @@
+using System.IO.Abstractions.TestingHelpers;
+using AzDev.Models.Inventory;
+
+namespace AzDev.Tests;
+
+public class SwaggerTests
+{
+ [Fact]
+ public void CanParseSwaggerJson()
+ {
+ var uri = "$(repo)/specification/dnsresolver/resource-manager/Microsoft.Network/stable/2022-07-01/dnsresolver.json";
+ var commit = "commit_hash";
+ var swagger = new SwaggerReference(uri, commit);
+ Assert.Equal(uri, swagger.RawUri);
+ Assert.Equal($"https://github.com/Azure/azure-rest-api-specs/blob/{commit}/specification/dnsresolver/resource-manager/Microsoft.Network/stable/2022-07-01/dnsresolver.json", swagger.Uri);
+ }
+
+ [Fact]
+ public void CanParseSwaggerReadme()
+ {
+ var uri = "$(repo)/specification/servicenetworking/resource-manager/readme.md";
+ var commit = "commit_hash";
+ var swagger = new SwaggerReference(uri, commit);
+ Assert.Equal(uri, swagger.RawUri);
+ Assert.Equal($"https://github.com/Azure/azure-rest-api-specs/blob/{commit}/specification/servicenetworking/resource-manager/readme.md", swagger.Uri);
+ }
+}
diff --git a/tools/AzDev/Tests/PSTests/InventoryTests.ps1 b/tools/AzDev/Tests/PSTests/InventoryTests.ps1
new file mode 100644
index 000000000000..ac62ba1ec974
--- /dev/null
+++ b/tools/AzDev/Tests/PSTests/InventoryTests.ps1
@@ -0,0 +1,21 @@
+BeforeAll {
+ if (-not (Get-Module AzDev)) {
+ Import-Module "$PSScriptRoot/../../../../artifacts/AzDev/AzDev.psd1"
+ }
+}
+
+Describe 'Repo inventory' {
+ It 'Every autorest project should have either a Wrapper or SdkBased project' {
+ (Get-DevModule).Project.Count | Should -BeGreaterThan 0
+ Get-DevModule | ForEach-Object{
+ if ($_.Project.Type -contains "AutoRestBased") {
+ $_.Project.Type -contains "Wrapper" -or $_.Project.Type -contains "SdkBased" | Should -BeTrue
+ }
+ }
+ }
+
+ It 'The number of unidentified project types should not grow' {
+ $others = Get-DevProject -Type Other
+ $others.Count | Should -BeLessOrEqual 8
+ }
+}
diff --git a/tools/AzDev/Tests/Tests.csproj b/tools/AzDev/Tests/Tests.csproj
new file mode 100644
index 000000000000..d657b3fb2666
--- /dev/null
+++ b/tools/AzDev/Tests/Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+
+ net6.0
+ false
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/AzDev/azpsdev.sln b/tools/AzDev/azpsdev.sln
new file mode 100644
index 000000000000..bd12f4715892
--- /dev/null
+++ b/tools/AzDev/azpsdev.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzDev", "src\AzDev.csproj", "{749AA94F-C21C-4512-A6D0-B7DFE1366719}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{A7C32887-E3B2-4478-8C05-B370036BAFC1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {749AA94F-C21C-4512-A6D0-B7DFE1366719}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7C32887-E3B2-4478-8C05-B370036BAFC1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {22DEC244-F4A8-44E9-8F52-C3AD674CC437}
+ EndGlobalSection
+EndGlobal
diff --git a/tools/AzDev/build.ps1 b/tools/AzDev/build.ps1
new file mode 100644
index 000000000000..c805d2795e95
--- /dev/null
+++ b/tools/AzDev/build.ps1
@@ -0,0 +1,5 @@
+$module = 'AzDev'
+$artifacts = "$PSScriptRoot/../../artifacts"
+
+dotnet publish $PSScriptRoot/src --sc -o "$artifacts/$module/bin"
+Copy-Item "$PSScriptRoot/$module/*" "$artifacts/$module" -Recurse -Force
diff --git a/tools/AzDev/src/AzDev.csproj b/tools/AzDev/src/AzDev.csproj
new file mode 100644
index 000000000000..71629488d001
--- /dev/null
+++ b/tools/AzDev/src/AzDev.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs b/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs
new file mode 100644
index 000000000000..c14464c3be79
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Context/GetContextCmdlet.cs
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Management.Automation;
+using AzDev.Models.PSModels;
+
+namespace AzDev.Cmdlets.Context
+{
+ [Cmdlet("Get", "DevContext")]
+ [OutputType(typeof(PSDevContext))]
+ public class GetContextCmdlet : DevCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+ WriteObject(new PSDevContext(Context, ContextProvider.ContextPath));
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs b/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs
new file mode 100644
index 000000000000..cbe76920b322
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Context/SetContextCmdlet.cs
@@ -0,0 +1,73 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+using System.Management.Automation;
+using AzDev.Models;
+using AzDev.Models.PSModels;
+
+namespace AzDev.Cmdlets.Context
+{
+ [Cmdlet("Set", "DevContext")]
+ [OutputType(typeof(PSDevContext))]
+ public class SetContextCmdlet : DevCmdletBase
+ {
+ [Parameter()]
+ [Alias("AzurePowerShellRepositoryRoot")]
+ public string RepoRoot { get; set; }
+
+ [Parameter()]
+ [Alias("AzurePowerShellCommonRepositoryRoot")]
+ public string CommonRepoRoot { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ DevContext context;
+ try { context = ContextProvider.LoadContext(); }
+ catch
+ {
+ // ignore if context is not found
+ context = new DevContext();
+ }
+
+ SetPathProperty(nameof(RepoRoot), x => context.AzurePowerShellRepositoryRoot = x);
+ SetPathProperty(nameof(CommonRepoRoot), x => context.AzurePowerShellCommonRepositoryRoot = x);
+ ContextProvider.SaveContext(context);
+ WriteObject(new PSDevContext(context, ContextProvider.ContextPath));
+ }
+
+ private void SetPathProperty(string parameterName, Action propertySetter)
+ {
+ if (MyInvocation.BoundParameters.ContainsKey(parameterName))
+ {
+ string path = (string)MyInvocation.BoundParameters[parameterName];
+
+ if (!string.IsNullOrEmpty(path))
+ {
+ string fullPath = Path.IsPathRooted(path) ? path : Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, path);
+ if (Directory.Exists(fullPath))
+ {
+ propertySetter(Path.GetFullPath(fullPath));
+ return;
+ }
+ }
+
+ throw new ArgumentException($"The input path [{path}] is incorrect or does not exist.]");
+ }
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/DevCmdletBase.cs b/tools/AzDev/src/Cmdlets/DevCmdletBase.cs
new file mode 100644
index 000000000000..506706b58843
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/DevCmdletBase.cs
@@ -0,0 +1,121 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using AzDev.Models;
+using AzDev.Models.Inventory;
+using AzDev.Services;
+
+namespace AzDev.Cmdlets
+{
+ public abstract class DevCmdletBase : PSCmdlet, IModuleAssemblyInitializer
+ {
+ internal DevContext Context
+ {
+ get
+ {
+ try
+ {
+ return ContextProvider.LoadContext();
+ }
+ catch (FileNotFoundException)
+ {
+ WriteWarning("Run Set-DevContext to set context.");
+ throw;
+ }
+ }
+ }
+
+ internal Codebase Codebase
+ {
+ get
+ {
+ try
+ {
+ return CodebaseProvider.GetCodebase();
+ }
+ catch (FileNotFoundException)
+ {
+ WriteWarning("Run Set-DevContext to set context.");
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// Gets the context provider. Use property to get the context.
+ ///
+ internal IContextProvider ContextProvider => AzDevModule.GetComponent(nameof(IContextProvider));
+
+ ///
+ /// Gets the codebase provider. Use property to get the codebase.
+ ///
+ internal ICodebaseProvider CodebaseProvider => AzDevModule.GetComponent(nameof(ICodebaseProvider));
+
+ public DevCmdletBase()
+ {
+ }
+
+ public void OnImport()
+ {
+ var contextProvider = new DefaultContextProvider(Constants.DevContextFilePath);
+ var codebaseProvider = new DefaultCodebaseProvider(contextProvider);
+ AzDevModule.SetComponent(nameof(IContextProvider), contextProvider);
+ AzDevModule.SetComponent(nameof(ICodebaseProvider), codebaseProvider);
+ }
+
+ protected T SelectFrom(string message, IEnumerable options, bool retryIfInvalid = true)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ if (options.Count() == 1)
+ {
+ return options.First();
+ }
+
+ while (true)
+ {
+ Host.UI.WriteLine(message);
+ var index = 1;
+ foreach (var option in options)
+ {
+ Host.UI.WriteLine($" {index++}: {option}");
+ }
+ Host.UI.WriteLine("Enter the number corresponding to your selection");
+
+ if (int.TryParse(Host.UI.ReadLine(), out int choice)
+ && choice >= 1 && choice <= options.Count())
+ {
+ return options.ElementAt(choice - 1);
+ }
+ else if (!retryIfInvalid)
+ {
+ WriteWarning("Invalid selection.");
+ return default;
+ }
+ else
+ {
+ WriteWarning("Invalid selection. Please try again.");
+ }
+ }
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs
new file mode 100644
index 000000000000..6f0b73b1c8c6
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Inventory/GetModuleCmdlet.cs
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Linq;
+using System.Management.Automation;
+using AzDev.Models.PSModels;
+using AzDev.Models.Inventory;
+using ModuleType = AzDev.Models.Inventory.ModuleType;
+
+namespace AzDev.Cmdlets.Inventory
+{
+ [Cmdlet("Get", "DevModule")]
+ [OutputType(typeof(PSModule))]
+ public class GetModuleCmdlet : DevCmdletBase
+ {
+ [Parameter(Position = 0, Mandatory = false)]
+ public string Name { get; set; }
+
+ [Parameter(Mandatory = false)]
+ public ModuleType Type { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ var modules = Codebase.Modules;
+ if (MyInvocation.BoundParameters.ContainsKey(nameof(Name)))
+ {
+ modules = modules.Where(x => x.Name.Equals(Name, System.StringComparison.InvariantCultureIgnoreCase));
+ }
+ if (MyInvocation.BoundParameters.ContainsKey(nameof(Type)))
+ {
+ modules = modules.Where(x => x.Type == Type);
+ }
+
+ WriteObject(modules.Select(m => new PSModule(m)), true);
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs
new file mode 100644
index 000000000000..4d5f1e6adb53
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Inventory/GetProjectCmdlet.cs
@@ -0,0 +1,60 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Linq;
+using System.Management.Automation;
+using AzDev.Models.PSModels;
+using AzDev.Models.Inventory;
+
+namespace AzDev.Cmdlets.Inventory
+{
+ [Cmdlet("Get", "DevProject")]
+ [OutputType(typeof(PSProject))]
+ public class GetProjectCmdlet : DevCmdletBase
+ {
+ [Parameter(Position = 0, Mandatory = false)]
+ [Alias("ProjectName")]
+ public string Name { get; set; }
+
+ [Parameter(Position = 1, Mandatory = false)]
+ public string ModuleName { get; set; }
+
+ [Parameter(Mandatory = false)]
+ public ProjectType Type { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ var modules = MyInvocation.BoundParameters.ContainsKey(nameof(ModuleName))
+ ? Codebase.Modules.Where(x => x.Name.Equals(ModuleName, System.StringComparison.InvariantCultureIgnoreCase))
+ : Codebase.Modules;
+ var projects = modules.SelectMany(x => x.Projects);
+
+ if (MyInvocation.BoundParameters.ContainsKey(nameof(Name)))
+ {
+ // filter by name
+ projects = projects.Where(x => x.Name.Equals(Name, System.StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ if (MyInvocation.BoundParameters.ContainsKey(nameof(Type)))
+ {
+ // filter by type
+ projects = projects.Where(x => x.Type == Type);
+ }
+
+ WriteObject(projects.Select(p => new PSProject(p)), true);
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs b/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs
new file mode 100644
index 000000000000..6d36a0094f48
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Inventory/GetRepoCmdlet.cs
@@ -0,0 +1,31 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.Management.Automation;
+
+namespace AzDev.Cmdlets.Inventory
+{
+ [Cmdlet("Get", "DevCodebase")]
+ [Obsolete("Useless cmdlet")]
+ public class GetRepoCmdlet : DevCmdletBase
+ {
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ WriteObject(Codebase);
+ }
+ }
+}
diff --git a/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs b/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs
new file mode 100644
index 000000000000..7fb304fd3a83
--- /dev/null
+++ b/tools/AzDev/src/Cmdlets/Swagger/OpenSwaggerCmdlet.cs
@@ -0,0 +1,60 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation;
+using AzDev.Models.Inventory;
+using AzDev.Services;
+
+namespace AzDev.Cmdlets.Swagger
+{
+ ///
+ /// Open-DevSwagger -Search "Graph"
+ /// Get-AzDevModule -Name "Graph" | Get-AzDevProject | Get-AzDevSwagger | Open-DevSwagger ?
+ ///
+ [Cmdlet("Open", "DevSwagger")]
+ public class OpenSwaggerCmdlet : DevCmdletBase
+ {
+ public const string SearchParameterSet = "Search";
+
+ [Parameter(Position = 0, Mandatory = true, ParameterSetName = SearchParameterSet)]
+ [ValidateNotNullOrEmpty]
+ public string Search { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ base.ProcessRecord();
+
+ if (ParameterSetName == SearchParameterSet)
+ {
+ IEnumerable projects = Codebase.FilterProjects(Search)
+ .Where(p => p is AutoRestProject)
+ .Cast();
+ if (!projects.Any())
+ {
+ WriteWarning($"No projects found for search term '{Search}'.");
+ return;
+ } else {
+ WriteDebug($"Found {projects.Count()} projects for search term '{Search}'. They are: {string.Join(", ", projects.Select(p => p.Name))}");
+ }
+ AutoRestProject project = SelectFrom($"Multiple projects matching [{Search}]", projects);
+ IEnumerable swaggers = project.Swaggers;
+ SwaggerReference swagger = SelectFrom($"Multiple swagger references found in [{project.Name}]", swaggers);
+ Host.UI.WriteLine($"Opening {swagger.Uri} in default browser...");
+ swagger.OpenOnline();
+ }
+ }
+ }
+}
diff --git a/tools/AzDev/src/Models/Constants.cs b/tools/AzDev/src/Models/Constants.cs
new file mode 100644
index 000000000000..1d0be77e39f4
--- /dev/null
+++ b/tools/AzDev/src/Models/Constants.cs
@@ -0,0 +1,24 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.IO;
+
+namespace AzDev.Models {
+ internal static class Constants
+ {
+ public const string DevContextFileName = "DevContext.json";
+ public static string DevContextFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AzPSDev", DevContextFileName);
+ }
+}
diff --git a/tools/AzDev/src/Models/DevContext.cs b/tools/AzDev/src/Models/DevContext.cs
new file mode 100644
index 000000000000..0d5181a605a5
--- /dev/null
+++ b/tools/AzDev/src/Models/DevContext.cs
@@ -0,0 +1,26 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace AzDev.Models
+{
+ public class DevContext
+ {
+ public string AzurePowerShellRepositoryRoot { get; set; }
+ public string AzurePowerShellCommonRepositoryRoot { get; set; }
+
+ public DevContext()
+ {
+ }
+ }
+}
diff --git a/tools/AzDev/src/Models/Inventory/AutoRestProject.cs b/tools/AzDev/src/Models/Inventory/AutoRestProject.cs
new file mode 100644
index 000000000000..cce639fa7a4b
--- /dev/null
+++ b/tools/AzDev/src/Models/Inventory/AutoRestProject.cs
@@ -0,0 +1,89 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Text.RegularExpressions;
+using AzDev.Services;
+
+namespace AzDev.Models.Inventory
+{
+ internal class AutoRestProject : Project
+ {
+ protected AutoRestProject(IFileSystem fs, string path) : base(fs, path)
+ {
+ _lazySwaggers = new Lazy>(LoadSwaggers);
+ _lazyReadmeText = new Lazy(() => LoadReadme());
+ }
+ internal AutoRestProject() {}
+ public IEnumerable Swaggers => _lazySwaggers.Value;
+ private Lazy> _lazySwaggers;
+ private string ReadmeText => _lazyReadmeText.Value;
+ private Lazy _lazyReadmeText;
+
+ public new static AutoRestProject FromFileSystem(IFileSystem fs, string path)
+ {
+ return new AutoRestProject(fs, path)
+ {
+ Type = ProjectType.AutoRestBased,
+ Name = fs.Path.GetFileName(path)
+ };
+ }
+
+ private IEnumerable LoadSwaggers()
+ {
+ Regex yamlBlockPattern = new Regex(@"(?ms)```\s*yaml(.*?)```");
+ var matches = yamlBlockPattern.Matches(ReadmeText);
+ var yamlBlocks = new List();
+ if (matches.Count == 0)
+ {
+ throw new Exception($"No YAML blocks found in README.md for [{Path}]");
+ }
+ else
+ {
+ foreach (Match match in matches)
+ {
+ yamlBlocks.Add(match.Groups[1].Value.Trim());
+ }
+ }
+
+ var swaggers = new List();
+ return yamlBlocks.Select(y => YamlHelper.Deserialize(y))
+ .Where(c => c != null)
+ .SelectMany(c =>
+ c.InputFile.Concat(c.Require).Concat(c.TryRequire)
+ .Where(uri => !Conventions.IsAutorestInputButNotSwagger(uri))
+ .Select(uri => new SwaggerReference(uri, c.Commit)));
+ }
+
+ private string LoadReadme()
+ {
+ foreach (var file in FileSystem.Directory.GetFiles(Path, "*.md"))
+ {
+ if (file.EndsWith("readme.md", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!file.EndsWith("README.md", StringComparison.Ordinal))
+ {
+ throw new Exception($"Found incorrect casing of README.md in [{file}]");
+ }
+ return FileSystem.File.ReadAllText(file);
+ }
+ }
+ throw new FileNotFoundException($"README.md not found in [{Path}]");
+ }
+ }
+}
diff --git a/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs b/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs
new file mode 100644
index 000000000000..f62c24dca7fe
--- /dev/null
+++ b/tools/AzDev/src/Models/Inventory/AutoRestYamlConfig.cs
@@ -0,0 +1,47 @@
+// ----------------------------------------------------------------------------------
+//
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using YamlDotNet.Serialization;
+
+namespace AzDev.Models.Inventory
+{
+ internal class AutoRestYamlConfig
+ {
+ [YamlMember(Alias = "title")]
+ public string Title { get => _title; set => _title = value?.Trim(); }
+ private string _title;
+
+ [YamlMember(Alias = "input-file")]
+ public IEnumerable InputFile { get => _inputFile; set => _inputFile = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); }
+ private IEnumerable _inputFile = Enumerable.Empty();
+
+ [YamlMember(Alias = "commit")]
+ public string Commit { get => _commit; set => _commit = value?.Trim(); }
+ private string _commit;
+
+ [YamlMember(Alias = "require")]
+ public IEnumerable Require { get => _require; set => _require = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); }
+ private IEnumerable _require = Enumerable.Empty();
+
+ [YamlMember(Alias = "try-require")]
+ public IEnumerable TryRequire { get => _tryRequire; set => _tryRequire = value?.Select(x => x.Trim())?.Where(x => !string.IsNullOrWhiteSpace(x)) ?? Enumerable.Empty(); }
+ private IEnumerable _tryRequire = Enumerable.Empty();
+
+ [YamlMember(Alias = "directive")]
+ public IEnumerable