From cd80cc832abb6d3cfa4681abb796750bf29c8291 Mon Sep 17 00:00:00 2001 From: Joshua Wilson Date: Fri, 29 Aug 2025 01:06:03 -0500 Subject: [PATCH 1/2] Fixing some weird windows issue with folder names --- DevSetup/DevSetup.psd1 | 136 ++++++ DevSetup/DevSetup.psm1 | 48 ++ .../ConvertFrom-3rdPartyInstall.Tests.ps1 | 50 +++ .../3rdParty/ConvertFrom-3rdPartyInstall.ps1 | 17 + .../ConvertFrom-VisualStudioInstall.ps1 | 149 +++++++ .../VisualStudio/Export-VssConfig.ps1 | 59 +++ .../VisualStudio/Import-VssConfig.ps1 | 33 ++ .../ConvertFrom-VisualStudioCodeInstall.ps1 | 186 ++++++++ .../VisualStudioCode/Export-VsCodeConfig.ps1 | 62 +++ .../VisualStudioCode/Import-VsCodeConfig.ps1 | 123 ++++++ .../Commands/Export-DevSetupEnv.Tests.ps1 | 39 ++ .../Private/Commands/Export-DevSetupEnv.ps1 | 80 ++++ .../Commands/Initialize-DevSetup.Tests.ps1 | 68 +++ .../Private/Commands/Initialize-DevSetup.ps1 | 114 +++++ .../Commands/Install-DevSetupEnv.Tests.ps1 | 85 ++++ .../Private/Commands/Install-DevSetupEnv.ps1 | 152 +++++++ .../Commands/Show-DevSetupEnvList.Tests.ps1 | 124 ++++++ .../Private/Commands/Show-DevSetupEnvList.ps1 | 171 ++++++++ .../Commands/Uninstall-DevSetupEnv.Tests.ps1 | 75 ++++ .../Commands/Uninstall-DevSetupEnv.ps1 | 105 +++++ DevSetup/Private/Enums/InstalledState.ps1 | 11 + DevSetup/Private/Enums/TaskState.ps1 | 9 + ...port-InstalledChocolateyPackages.Tests.ps1 | 102 +++++ .../Export-InstalledChocolateyPackages.ps1 | 234 ++++++++++ .../Get-ChocolateyCacheFile.Tests.ps1 | 32 ++ .../Chocolatey/Get-ChocolateyCacheFile.ps1 | 65 +++ ...et-ChocolateyPackageDependencies.Tests.ps1 | 96 ++++ .../Get-ChocolateyPackageDependencies.ps1 | 82 ++++ .../Get-ChocolateyVersion.Tests.ps1 | 46 ++ .../Chocolatey/Get-ChocolateyVersion.ps1 | 79 ++++ .../Chocolatey/Install-Chocolatey.Tests.ps1 | 96 ++++ .../Chocolatey/Install-Chocolatey.ps1 | 113 +++++ .../Install-ChocolateyPackage.Tests.ps1 | 100 +++++ .../Chocolatey/Install-ChocolateyPackage.ps1 | 154 +++++++ .../Install-ChocolateyPackages.Tests.ps1 | 147 +++++++ .../Chocolatey/Install-ChocolateyPackages.ps1 | 162 +++++++ .../Chocolatey/Read-ChocolateyCache.Tests.ps1 | 51 +++ .../Chocolatey/Read-ChocolateyCache.ps1 | 77 ++++ .../Test-ChocolateyInstalled.Tests.ps1 | 25 ++ .../Chocolatey/Test-ChocolateyInstalled.ps1 | 65 +++ .../Test-ChocolateyPackageInstalled.Tests.ps1 | 75 ++++ .../Test-ChocolateyPackageInstalled.ps1 | 118 +++++ .../Uninstall-ChocolateyPackage.Tests.ps1 | 50 +++ .../Uninstall-ChocolateyPackage.ps1 | 97 +++++ .../Uninstall-ChocolateyPackages.Tests.ps1 | 147 +++++++ .../Uninstall-ChocolateyPackages.ps1 | 150 +++++++ .../Write-ChocolateyCache.Tests.ps1 | 56 +++ .../Chocolatey/Write-ChocolateyCache.ps1 | 88 ++++ .../Core/Install-CoreDependencies.Tests.ps1 | 135 ++++++ .../Core/Install-CoreDependencies.ps1 | 132 ++++++ .../Providers/Core/Install-GitRepository.ps1 | 173 ++++++++ .../Providers/Core/Install-Nuget.Tests.ps1 | 114 +++++ .../Private/Providers/Core/Install-Nuget.ps1 | 118 +++++ ...xport-InstalledPowershellModules.Tests.ps1 | 139 ++++++ .../Export-InstalledPowershellModules.ps1 | 264 +++++++++++ .../Install-PowershellModule.Tests.ps1 | 154 +++++++ .../Powershell/Install-PowershellModule.ps1 | 147 +++++++ .../Install-PowershellModules.Tests.ps1 | 170 ++++++++ .../Powershell/Install-PowershellModules.ps1 | 162 +++++++ .../Test-PowershellModuleInstalled.Tests.ps1 | 98 +++++ .../Test-PowershellModuleInstalled.ps1 | 157 +++++++ .../Uninstall-PowershellModule.Tests.ps1 | 109 +++++ .../Powershell/Uninstall-PowershellModule.ps1 | 96 ++++ .../Uninstall-PowershellModules.Tests.ps1 | 170 ++++++++ .../Uninstall-PowershellModules.ps1 | 157 +++++++ .../Export-InstalledScoopPackages.Tests.ps1 | 107 +++++ .../Scoop/Export-InstalledScoopPackages.ps1 | 412 ++++++++++++++++++ .../Providers/Scoop/Find-Scoop.Tests.ps1 | 66 +++ .../Private/Providers/Scoop/Find-Scoop.ps1 | 86 ++++ .../Scoop/Get-ScoopCacheFile.Tests.ps1 | 16 + .../Providers/Scoop/Get-ScoopCacheFile.ps1 | 61 +++ .../Scoop/Get-ScoopVersion.Tests.ps1 | 112 +++++ .../Providers/Scoop/Get-ScoopVersion.ps1 | 103 +++++ .../Providers/Scoop/Install-Scoop.Tests.ps1 | 56 +++ .../Private/Providers/Scoop/Install-Scoop.ps1 | 85 ++++ .../Scoop/Install-ScoopBucket.Tests.ps1 | 90 ++++ .../Providers/Scoop/Install-ScoopBucket.ps1 | 116 +++++ .../Scoop/Install-ScoopComponents.Tests.ps1 | 123 ++++++ .../Scoop/Install-ScoopComponents.ps1 | 256 +++++++++++ .../Scoop/Install-ScoopPackage.Tests.ps1 | 121 +++++ .../Providers/Scoop/Install-ScoopPackage.ps1 | 163 +++++++ .../Providers/Scoop/Read-ScoopCache.Tests.ps1 | 68 +++ .../Providers/Scoop/Read-ScoopCache.ps1 | 74 ++++ .../Test-ScoopComponentInstalled.Tests.ps1 | 135 ++++++ .../Scoop/Test-ScoopComponentInstalled.ps1 | 155 +++++++ .../Scoop/Test-ScoopInstalled.Tests.ps1 | 50 +++ .../Providers/Scoop/Test-ScoopInstalled.ps1 | 82 ++++ .../Scoop/Uninstall-ScoopBucket.Tests.ps1 | 84 ++++ .../Providers/Scoop/Uninstall-ScoopBucket.ps1 | 106 +++++ .../Scoop/Uninstall-ScoopComponents.Tests.ps1 | 137 ++++++ .../Scoop/Uninstall-ScoopComponents.ps1 | 225 ++++++++++ .../Scoop/Uninstall-ScoopPackage.Tests.ps1 | 97 +++++ .../Scoop/Uninstall-ScoopPackage.ps1 | 102 +++++ .../Scoop/Write-ScoopCache.Tests.ps1 | 63 +++ .../Providers/Scoop/Write-ScoopCache.ps1 | 80 ++++ .../Utils/ConvertFrom-Base64.Tests.ps1 | 43 ++ DevSetup/Private/Utils/ConvertFrom-Base64.ps1 | 29 ++ .../Private/Utils/ConvertTo-Base64.Tests.ps1 | 45 ++ DevSetup/Private/Utils/ConvertTo-Base64.ps1 | 32 ++ .../Private/Utils/Find-GitRepositories.ps1 | 121 +++++ DevSetup/Private/Utils/Format-PrettyTable.ps1 | 130 ++++++ .../Utils/Get-DevSetupCachePath.Tests.ps1 | 14 + .../Private/Utils/Get-DevSetupCachePath.ps1 | 60 +++ .../Get-DevSetupCommunityEnvPath.Tests.ps1 | 23 + .../Utils/Get-DevSetupCommunityEnvPath.ps1 | 10 + .../Utils/Get-DevSetupEnvPath.Tests.ps1 | 14 + .../Private/Utils/Get-DevSetupEnvPath.ps1 | 10 + .../Utils/Get-DevSetupLocalEnvPath.Tests.ps1 | 23 + .../Utils/Get-DevSetupLocalEnvPath.ps1 | 10 + .../Utils/Get-DevSetupManifest.Tests.ps1 | 22 + .../Private/Utils/Get-DevSetupManifest.ps1 | 26 ++ .../Private/Utils/Get-DevSetupPath.Tests.ps1 | 14 + DevSetup/Private/Utils/Get-DevSetupPath.ps1 | 9 + .../Utils/Get-DevSetupVersion.Tests.ps1 | 36 ++ .../Private/Utils/Get-DevSetupVersion.ps1 | 104 +++++ .../Utils/Get-EnvironmentVariable.Tests.ps1 | 35 ++ .../Private/Utils/Get-EnvironmentVariable.ps1 | 10 + .../Private/Utils/Get-PwshVersion.Tests.ps1 | 26 ++ DevSetup/Private/Utils/Get-PwshVersion.ps1 | 10 + .../Utils/Initialize-DevSetupEnvs.Tests.ps1 | 139 ++++++ .../Private/Utils/Initialize-DevSetupEnvs.ps1 | 90 ++++ .../Utils/Optimize-DevSetupEnvs.Tests.ps1 | 127 ++++++ .../Private/Utils/Optimize-DevSetupEnvs.ps1 | 93 ++++ .../Utils/Read-ConfigurationFile.Tests.ps1 | 43 ++ .../Private/Utils/Read-ConfigurationFile.ps1 | 7 + .../Utils/Test-OperatingSystem.Tests.ps1 | 62 +++ .../Private/Utils/Test-OperatingSystem.ps1 | 30 ++ .../Utils/Test-RunningAsAdmin.Tests.ps1 | 46 ++ .../Private/Utils/Test-RunningAsAdmin.ps1 | 13 + DevSetup/Private/Utils/Write-NewConfig.ps1 | 223 ++++++++++ .../Utils/Write-StatusMessage.Tests.ps1 | 81 ++++ .../Private/Utils/Write-StatusMessage.ps1 | 59 +++ DevSetup/Public/Use-DevSetup.ps1 | 370 ++++++++++++++++ 133 files changed, 12568 insertions(+) create mode 100644 DevSetup/DevSetup.psd1 create mode 100644 DevSetup/DevSetup.psm1 create mode 100644 DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 create mode 100644 DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 create mode 100644 DevSetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 create mode 100644 DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 create mode 100644 DevSetup/Private/Commands/Export-DevSetupEnv.ps1 create mode 100644 DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 create mode 100644 DevSetup/Private/Commands/Initialize-DevSetup.ps1 create mode 100644 DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 create mode 100644 DevSetup/Private/Commands/Install-DevSetupEnv.ps1 create mode 100644 DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 create mode 100644 DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 create mode 100644 DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 create mode 100644 DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 create mode 100644 DevSetup/Private/Enums/InstalledState.ps1 create mode 100644 DevSetup/Private/Enums/TaskState.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 create mode 100644 DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 create mode 100644 DevSetup/Private/Providers/Core/Install-GitRepository.ps1 create mode 100644 DevSetup/Private/Providers/Core/Install-Nuget.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Core/Install-Nuget.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Install-PowershellModule.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Find-Scoop.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-Scoop.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Read-ScoopCache.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 create mode 100644 DevSetup/Private/Providers/Scoop/Write-ScoopCache.ps1 create mode 100644 DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 create mode 100644 DevSetup/Private/Utils/ConvertFrom-Base64.ps1 create mode 100644 DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 create mode 100644 DevSetup/Private/Utils/ConvertTo-Base64.ps1 create mode 100644 DevSetup/Private/Utils/Find-GitRepositories.ps1 create mode 100644 DevSetup/Private/Utils/Format-PrettyTable.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupCachePath.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupEnvPath.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupManifest.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupPath.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-DevSetupVersion.ps1 create mode 100644 DevSetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-EnvironmentVariable.ps1 create mode 100644 DevSetup/Private/Utils/Get-PwshVersion.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Get-PwshVersion.ps1 create mode 100644 DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 create mode 100644 DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 create mode 100644 DevSetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Read-ConfigurationFile.ps1 create mode 100644 DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Test-OperatingSystem.ps1 create mode 100644 DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Test-RunningAsAdmin.ps1 create mode 100644 DevSetup/Private/Utils/Write-NewConfig.ps1 create mode 100644 DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 create mode 100644 DevSetup/Private/Utils/Write-StatusMessage.ps1 create mode 100644 DevSetup/Public/Use-DevSetup.ps1 diff --git a/DevSetup/DevSetup.psd1 b/DevSetup/DevSetup.psd1 new file mode 100644 index 0000000..b68f608 --- /dev/null +++ b/DevSetup/DevSetup.psd1 @@ -0,0 +1,136 @@ +# +# Module manifest for module 'DevSetup' +# +# Generated by: Joshua Wilson +# +# Generated on: 7/31/2025 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'DevSetup.psm1' + +# Version number of this module. +ModuleVersion = '0.0.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '316f7319-7063-492b-a5d3-dc32c616f157' + +# Author of this module +Author = 'Joshua Wilson' + +# Company or vendor of this module +CompanyName = 'PwshDevs' + +# Copyright statement for this module +Copyright = '(c) 2025 PwshDevs. All rights reserved.' + +# Description of the functionality provided by this module +Description = '' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '5.1' + +# 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 = @('powershell-yaml', 'VSSetup', 'PowerShellForGitHub', 'EZlog') + +# 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 = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# 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 = @( + 'Use-DevSetup' +) + +# 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 = @() + +# 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 = @('devsetup') + +# 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 = @('BuildTools', 'Toolchain', 'Build', 'Automation', 'Package', 'Management', 'CMake', 'MSBuild', 'NMake', 'B2', 'Chocolatey', 'MySQL', 'Development') + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/pwshdevs/devsetup' + + # A URL to the main environments website for this project. + EnvironmentsProjectUri = 'https://github.com/pwshdevs/devsetup.environments' + + # 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 = '' + +} \ No newline at end of file diff --git a/DevSetup/DevSetup.psm1 b/DevSetup/DevSetup.psm1 new file mode 100644 index 0000000..8b88c5f --- /dev/null +++ b/DevSetup/DevSetup.psm1 @@ -0,0 +1,48 @@ +# DevSetup PowerShell Module + +# Get the current module path +$ModulePath = $PSScriptRoot + +# Get all function files from both Private and Public directories, excluding test files +$PrivateFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$PrivateUtilsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Utils") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$PrivateProvidersFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Providers") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$PrivateCommandsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Commands") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$Private3rdpartyFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\3rdparty") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$PrivateEnumsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Enums") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } +$PublicFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Public") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } + +# Combine all function files +$AllFunctions = @() +if ($PrivateFunctions) { $AllFunctions += $PrivateFunctions } +if ($PrivateUtilsFunctions) { $AllFunctions += $PrivateUtilsFunctions } +if ($PrivateProvidersFunctions) { $AllFunctions += $PrivateProvidersFunctions } +if ($PrivateCommandsFunctions) { $AllFunctions += $PrivateCommandsFunctions } +if ($Private3rdpartyFunctions) { $AllFunctions += $Private3rdpartyFunctions } +if ($PrivateEnumsFunctions) { $AllFunctions += $PrivateEnumsFunctions } +if ($PublicFunctions) { $AllFunctions += $PublicFunctions } + +# Import all functions +foreach ($FunctionFile in $AllFunctions) { + try { + . $FunctionFile.FullName + Write-Verbose "Imported function from: $($FunctionFile.Name)" + } + catch { + Write-Error "Failed to import function from $($FunctionFile.Name): $_" + } +} + +# Initialize global variables +if (-not $global:InstalledPackages) { + $global:InstalledPackages = @() +} + +if (-not $global:InstalledPackagesFile) { + $global:InstalledPackagesFile = $null +} + +New-Alias -Name devsetup -Value Use-DevSetup + +# Export module members (functions will be exported via the manifest) +Write-Verbose "DevSetup module loaded successfully. $($AllFunctions.Count) functions imported ($($PrivateFunctions.Count) private, $($PrivateUtilsFunctions.Count) utils, $($PrivateProvidersFunctions.Count) providers, $($PrivateCommandsFunctions.Count) commands, $($Private3rdpartyFunctions.Count) 3rdparty, $($PublicFunctions.Count) public)." diff --git a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 new file mode 100644 index 0000000..e1383d0 --- /dev/null +++ b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 @@ -0,0 +1,50 @@ +BeforeAll { + . $PSScriptRoot\ConvertFrom-3rdPartyInstall.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudio\ConvertFrom-VisualStudioInstall.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudioCode\ConvertFrom-VisualStudioCodeInstall.ps1 + Mock Write-Host { } + Mock Write-Warning { } + Mock ConvertFrom-VisualStudioInstall { $true } + Mock ConvertFrom-VisualStudioCodeInstall { $true } +} + +Describe "ConvertFrom-3rdPartyInstall" { + + Context "When both conversions succeed" { + It "Should not write any warnings" { + $result = ConvertFrom-3rdPartyInstall -Config "test" + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + Assert-MockCalled Write-Host -Exactly 2 -Scope It + } + } + + Context "When Visual Studio conversion fails" { + It "Should write a warning for Visual Studio" { + Mock ConvertFrom-VisualStudioInstall { $false } + Mock ConvertFrom-VisualStudioCodeInstall { $true } + $result = ConvertFrom-3rdPartyInstall -Config "test" + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Visual Studio installations" } + Assert-MockCalled Write-Host -Exactly 2 -Scope It + } + } + + Context "When Visual Studio Code conversion fails" { + It "Should write a warning for Visual Studio Code" { + Mock ConvertFrom-VisualStudioInstall { $true } + Mock ConvertFrom-VisualStudioCodeInstall { $false } + $result = ConvertFrom-3rdPartyInstall -Config "test" + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Visual Studio Code installation" } + Assert-MockCalled Write-Host -Exactly 2 -Scope It + } + } + + Context "When both conversions fail" { + It "Should write warnings for both" { + Mock ConvertFrom-VisualStudioInstall { $false } + Mock ConvertFrom-VisualStudioCodeInstall { $false } + $result = ConvertFrom-3rdPartyInstall -Config "test" + Assert-MockCalled Write-Warning -Exactly 2 -Scope It + Assert-MockCalled Write-Host -Exactly 2 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 new file mode 100644 index 0000000..540a0f6 --- /dev/null +++ b/DevSetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 @@ -0,0 +1,17 @@ +Function ConvertFrom-3rdPartyInstall { + Param( + [string]$Config + ) + + # Convert from Visual Studio installations + Write-Host "`nScanning Visual Studio installations..." -ForegroundColor Cyan + if (-not (ConvertFrom-VisualStudioInstall -Config $Config)) { + Write-Warning "Failed to convert Visual Studio installations, but continuing..." + } + + # Convert from Visual Studio Code installations + Write-Host "`nScanning Visual Studio Code installation..." -ForegroundColor Cyan + if (-not (ConvertFrom-VisualStudioCodeInstall -Config $Config)) { + Write-Warning "Failed to convert Visual Studio Code installation, but continuing..." + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 b/DevSetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 new file mode 100644 index 0000000..d619c33 --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 @@ -0,0 +1,149 @@ +Function ConvertFrom-VisualStudioInstall { + Param( + [Parameter(Mandatory=$true)] + [string]$Config, + [string]$OutFile, + [switch]$DryRun + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "This operation requires administrator privileges. Please run as administrator." + } + + # Get Visual Studio instances + Write-Host "- Detecting Visual Studio installations..." -ForegroundColor Gray + $vsInstances = Get-VSSetupInstance + + if (-not $vsInstances) { + Write-Warning "No Visual Studio instances found." + return $true + } + + # Read existing YAML configuration + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure chocolateyPackages section exists + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } + + foreach ($instance in $vsInstances) { + Write-Host " - Found: $($instance.DisplayName)" -ForegroundColor Gray + + # Convert display name to Chocolatey package name + # Extract year and type separately to ensure correct ordering + $displayName = $instance.DisplayName + $year = if ($displayName -match '(\d{4})') { $matches[1] } else { '' } + $type = '' + if ($displayName -match 'Community') { $type = 'community' } + elseif ($displayName -match 'Professional') { $type = 'professional' } + elseif ($displayName -match 'Enterprise') { $type = 'enterprise' } + + # Build package name as visualstudio + $packageName = "visualstudio$year$type" + + Write-Host " - Converted to Chocolatey package: $packageName" -ForegroundColor Gray + + # Create temporary file for Visual Studio configuration export + $base64Config = Export-VssConfig -VssInstallPath $instance.InstallationPath + + # Create command string for importing the VS configuration + $Command = "Import-VssConfig -EncodedConfigFile '$base64Config' -VssInstallPath '$($instance.InstallationPath)'" + $commandPackageName = "$packageName.importConfig" + + # Check if command already exists for this package + $existingCommand = $YamlData.devsetup.commands | Where-Object { + ($_ -is [hashtable] -and $_.packageName -eq $commandPackageName) + } + + if ($existingCommand) { + Write-Host " - Updating existing VS configuration command..." -ForegroundColor Gray + + # Find index of existing command + $commandIndex = $YamlData.devsetup.commands.IndexOf($existingCommand) + + # Update with new command + $YamlData.devsetup.commands[$commandIndex] = @{ + packageName = $commandPackageName + command = $Command + } + } else { + Write-Host " - Adding new VS configuration command..." -ForegroundColor Gray + + # Add new command + $YamlData.devsetup.commands += @{ + packageName = $commandPackageName + command = $Command + } + } + + $existingPackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { + ($_ -is [string] -and $_ -eq $packageName) -or + ($_ -is [hashtable] -and $_.name -eq $packageName) + } + + if ($existingPackage) { + Write-Host " - Updating existing Visual Studio packages..." -ForegroundColor Gray + + # Find index of existing package + $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) + + # Update with components + $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ + name = $packageName + version = $null + } + } else { + Write-Host " - Adding new Visual Studio package..." -ForegroundColor Gray + + # Add new package with components + $YamlData.devsetup.dependencies.chocolatey.packages += @{ + name = $packageName + version = $null + } + } + } + + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + } + catch { + Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" + $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 + } + + # Handle output based on parameters + if ($DryRun) { + Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan + Write-Host $yamlOutput -ForegroundColor White + Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow + } else { + # Determine output file + $outputFile = if ($OutFile) { $OutFile } else { $Config } + + try { + Write-Debug "`nSaving configuration to: $outputFile" + $yamlOutput | Out-File -FilePath $outputFile -Encoding UTF8 + Write-Debug "Configuration saved successfully!" + } + catch { + Write-Error "Failed to save configuration to $outputFile`: $_" + return $false + } + } + + # "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" export --installPath "" --config ".vsconfig" + # "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "C:\Program Files\Microsoft Visual Studio\\" --config "C:\Path\To\Your\Config.vsconfig" --passive --allowUnsignedExtensions + + Write-Host "Visual Studio installation conversion completed!" -ForegroundColor Green + return $true + } + catch { + Write-Error "Error in Visual Studio installation conversion: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 b/DevSetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 new file mode 100644 index 0000000..7033535 --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 @@ -0,0 +1,59 @@ +Function Export-VssConfig { + param ( + [string]$VssInstallPath + ) + + if (-not (Test-Path -Path $VssInstallPath)) { + Write-Error "Visual Studio installation path not found: $VssInstallPath" + return $false + } + + try { + $tempConfigFile = [System.IO.Path]::GetTempFileName() + ".vsconfig" + + # Execute the command + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" export --installPath $VssInstallPath --config "$tempConfigFile" --passive + + # Since setup.exe is async, wait for the config file to be created and populated + $timeout = 60 # seconds + $elapsed = 0 + $pollInterval = 2 # seconds + + Write-Host " - Waiting for Visual Studio export to complete." -ForegroundColor Gray -NoNewline + + while ($elapsed -lt $timeout) { + if ((Test-Path -Path $tempConfigFile) -and (Get-Item $tempConfigFile).Length -gt 0) { + Write-Host "`n - Export completed successfully." -ForegroundColor Gray + break + } + Start-Sleep -Seconds $pollInterval + $elapsed += $pollInterval + Write-Host "." -NoNewline -ForegroundColor Gray + } + + # Check if we timed out + if ($elapsed -ge $timeout) { + Write-Host " - Export operation timed out after $timeout seconds." -ForegroundColor Gray + Write-Warning "Visual Studio export may still be running in the background. Check the installation manually." + } + + if (-not (Test-Path -Path $tempConfigFile)) { + Write-Error "Failed to export Visual Studio configuration to temporary file." + return $false + } + + $encodedConfig = ConvertTo-Base64 -FilePath $tempConfigFile + if (-not $encodedConfig) { + Write-Error "Failed to convert configuration file to Base64." + return $false + } + + # Clean up temporary files + if (Test-Path $tempConfigFile) { Remove-Item $tempConfigFile -Force } + + return $encodedConfig + } catch { + Write-Error "Failed to export configuration to file: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 b/DevSetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 new file mode 100644 index 0000000..268f5fb --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 @@ -0,0 +1,33 @@ +Function Import-VssConfig { + param ( + [string]$EncodedConfigFile, + [string]$VssInstallPath + ) + + if (-not $EncodedConfigFile) { + Write-Error "Encoded configuration file is empty." + return $false + } + + try { + # Decode the base64 encoded configuration + $decodedConfig = ConvertFrom-Base64 -EncodedString $EncodedConfigFile + + # Create config file in user's home directory + $configFile = Join-Path -Path $env:USERPROFILE -ChildPath ".vssconfig-devsetup" + + # Write the decoded configuration to the config file + $decodedConfig | Out-File -FilePath $configFile -Encoding UTF8 + + Write-Host "Visual Studio configuration saved to: $configFile" -ForegroundColor Green + + # Run the Visual Studio installer with the config file (suppress output) + & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath $VssInstallPath --config "$configFile" --passive --allowUnsignedExtensions > $null 2>&1 + + return $true + } + catch { + Write-Error "Failed to process Visual Studio configuration: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 b/DevSetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 new file mode 100644 index 0000000..9f01805 --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 @@ -0,0 +1,186 @@ +Function ConvertFrom-VisualStudioCodeInstall { + Param ( + [string]$Config + ) + + try { + Write-Host "- Detecting Visual Studio Code installation..." -ForegroundColor Gray + + # Read existing configuration + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure chocolateyPackages section exists + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } + + # Check if vscode is already in chocolatey packages + $existingVscodePackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { + ($_ -is [string] -and $_ -eq "vscode") -or + ($_ -is [hashtable] -and $_.name -eq "vscode") + } + + if ($existingVscodePackage) { + Write-Host " - Visual Studio Code already configured in chocolatey packages" -ForegroundColor Green + + # Export VS Code configuration + Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray + $encodedConfig = Export-VsCodeConfig + + if ($encodedConfig) { + # Ensure commands section exists + if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } + + # Check if vscode.importConfig command already exists + $existingCommand = $YamlData.devsetup.commands | Where-Object { + ($_ -is [hashtable] -and $_.packageName -eq "vscode.importConfig") + } + + if ($existingCommand) { + # Update existing command with new encoded config + $existingCommand.command = "Import-VsCodeConfig -EncodedConfig $encodedConfig" + Write-Host " - VS Code import command updated in configuration" -ForegroundColor Green + } + else { + # Add new Import-VsCodeConfig command + $YamlData.devsetup.commands += @{ + command = "Import-VsCodeConfig -EncodedConfig '$encodedConfig'" + packageName = "vscode.importConfig" + } + Write-Host " - VS Code import command added to configuration" -ForegroundColor Green + } + + # Save updated configuration + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + $yamlOutput | Out-File -FilePath $Config -Encoding UTF8 + Write-Host " - Configuration updated successfully" -ForegroundColor Green + } + catch { + Write-Error "Failed to save updated configuration: $_" + return $false + } + } + else { + Write-Host " - No VS Code configuration to export" -ForegroundColor Yellow + } + + return $true + } + + # Check for manual installation using multiple methods + $vscodeInstalled = $false + $detectionMethod = "" + + # Method 1: Check if 'code --version' works + try { + $codeVersion = & code --version 2>$null + if ($LASTEXITCODE -eq 0 -and $codeVersion) { + $vscodeInstalled = $true + $detectionMethod = "command line (code --version)" + Write-Host " - Found VS Code via command line: $($codeVersion[0])" -ForegroundColor Gray + } + } + catch { + # Command not found, continue with other methods + } + + # Method 2: Check registry + if (-not $vscodeInstalled) { + try { + $regPath = "HKLM:\SOFTWARE\Classes\Applications\Code.exe\shell\open\command" + $regValue = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue + if ($regValue) { + $vscodeInstalled = $true + $detectionMethod = "registry" + Write-Host " - Found VS Code via registry" -ForegroundColor Gray + } + } + catch { + # Registry check failed, continue + } + } + + # Method 3: Filesystem checks + if (-not $vscodeInstalled) { + $userPath = "$env:LocalAppData\Programs\Microsoft VS Code\bin\code.cmd" + $systemPath = "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd" + + if (Test-Path $userPath) { + $vscodeInstalled = $true + $detectionMethod = "user installation path" + Write-Host " - Found VS Code at: $userPath" -ForegroundColor Gray + } + elseif (Test-Path $systemPath) { + $vscodeInstalled = $true + $detectionMethod = "system installation path" + Write-Host " - Found VS Code at: $systemPath" -ForegroundColor Gray + } + } + + # Method 4: Get-Package check + if (-not $vscodeInstalled) { + try { + $package = Get-Package -Name "*vscode*" -ErrorAction SilentlyContinue + if ($package) { + $vscodeInstalled = $true + $detectionMethod = "package manager" + Write-Host " - Found VS Code via Get-Package: $($package.Name)" -ForegroundColor Gray + } + } + catch { + # Get-Package failed, continue + } + } + + if ($vscodeInstalled) { + Write-Host " - Visual Studio Code detected ($detectionMethod), adding to chocolatey packages" -ForegroundColor Green + + # Add vscode to chocolatey packages + $YamlData.devsetup.dependencies.chocolatey.packages += @{ + name = "vscode" + version = $null + } + + # Export VS Code configuration + Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray + $encodedConfig = Export-VsCodeConfig + + if ($encodedConfig) { + # Ensure commands section exists + if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } + + # Add Import-VsCodeConfig command + $YamlData.devsetup.commands += @{ + command = "Import-VsCodeConfig -EncodedConfig '$encodedConfig'" + packageName = "vscode.importConfig" + } + Write-Host " - VS Code import command added to configuration" -ForegroundColor Green + } + else { + Write-Host " - No VS Code configuration to export" -ForegroundColor Yellow + } + + # Save updated configuration + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + $yamlOutput | Out-File -FilePath $Config -Encoding UTF8 + Write-Host " - Configuration updated successfully" -ForegroundColor Green + } + catch { + Write-Error "Failed to save updated configuration: $_" + return $false + } + } + else { + Write-Host " - Visual Studio Code not detected on this system" -ForegroundColor Yellow + } + + return $true + } + catch { + Write-Error "Error detecting Visual Studio Code installation: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 b/DevSetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 new file mode 100644 index 0000000..e67694c --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 @@ -0,0 +1,62 @@ +Function Export-VsCodeConfig { + Param( + + ) + + try { + Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray + + # Check if 'code' command is available + $codeCommand = Get-Command code -ErrorAction SilentlyContinue + if (-not $codeCommand) { + Write-Warning "VS Code 'code' command not found in PATH. Cannot export extensions." + return $null + } + + Write-Host " - VS Code command found, listing extensions..." -ForegroundColor Gray + + # Get list of installed extensions + try { + $command = { + & code --list-extensions 2>$null + } + $extensionsOutput = Invoke-Command -ScriptBlock $command + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to get VS Code extensions list" + return $null + } + + # Convert output to array (filter out empty lines) + $extensionsArray = $extensionsOutput | Where-Object { $_ -and $_.Trim() -ne "" } + + if (-not $extensionsArray -or $extensionsArray.Count -eq 0) { + Write-Host " - No VS Code extensions found" -ForegroundColor Yellow + return $null + } + + Write-Host " - Found $($extensionsArray.Count) VS Code extensions" -ForegroundColor Gray + + # Convert array to JSON + $jsonData = $extensionsArray | ConvertTo-Json + + # Convert JSON to Base64 + $base64Config = ConvertTo-Base64 -InputString $jsonData + + if (-not $base64Config) { + Write-Error "Failed to encode VS Code extensions to Base64" + return $null + } + + Write-Host " - VS Code extensions exported and encoded successfully" -ForegroundColor Gray + return $base64Config + } + catch { + Write-Error "Error getting VS Code extensions: $_" + return $null + } + } + catch { + Write-Error "Error exporting VS Code configuration: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 b/DevSetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 new file mode 100644 index 0000000..9208043 --- /dev/null +++ b/DevSetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 @@ -0,0 +1,123 @@ +Function Import-VsCodeConfig { + Param( + [string]$EncodedConfig + ) + + try { + Write-Host "- Importing VS Code configuration..." -ForegroundColor Gray + + if (-not $EncodedConfig) { + Write-Warning "No encoded configuration provided" + return $false + } + + # Check if 'code' command is available + $codeCommand = Get-Command code -ErrorAction SilentlyContinue + $codePath = $null + + if ($codeCommand) { + $codePath = "code" + Write-Host " - VS Code command found in PATH" -ForegroundColor Gray + } + else { + # Manual path checks when code command is not in PATH + $userPath = "$env:LocalAppData\Programs\Microsoft VS Code\bin\code.cmd" + $systemPath = "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd" + + if (Test-Path $userPath) { + $codePath = $userPath + Write-Host " - VS Code found at user path: $userPath" -ForegroundColor Gray + } + elseif (Test-Path $systemPath) { + $codePath = $systemPath + Write-Host " - VS Code found at system path: $systemPath" -ForegroundColor Gray + } + } + + if (-not $codePath) { + Write-Warning "VS Code executable not found. Cannot install extensions." + return $false + } + + Write-Host " - VS Code command found, decoding configuration..." -ForegroundColor Gray + + # Decode the base64 configuration + $decodedJson = ConvertFrom-Base64 -EncodedString $EncodedConfig + if (-not $decodedJson) { + Write-Error "Failed to decode base64 configuration" + return $false + } + + Write-Host " - Configuration decoded, parsing JSON..." -ForegroundColor Gray + + # Convert from JSON + try { + $extensions = $decodedJson | ConvertFrom-Json + } + catch { + Write-Error "Failed to parse JSON from decoded configuration: $_" + return $false + } + + # Handle both array and single string cases + if ($extensions -is [string]) { + # Single extension + $extensionList = @($extensions) + } + elseif ($extensions -is [array]) { + # Array of extensions + $extensionList = $extensions + } + else { + Write-Error "Unexpected extension data type: $($extensions.GetType())" + return $false + } + + if ($extensionList.Count -eq 0) { + Write-Host " - No extensions to install" -ForegroundColor Yellow + return $true + } + + Write-Host " - Installing $($extensionList.Count) VS Code extensions..." -ForegroundColor Gray + + $successCount = 0 + $failureCount = 0 + + # Install each extension + foreach ($extension in $extensionList) { + if (-not $extension -or $extension.Trim() -eq "") { + continue + } + + Write-Host " - Installing extension: $extension" -ForegroundColor Gray + + try { + $command = { + & $codePath --install-extension $extension --force 2>&1 + } + $result = Invoke-Command -ScriptBlock $command + if ($LASTEXITCODE -eq 0) { + Write-Host " - Successfully installed: $extension" -ForegroundColor Green + $successCount++ + } + else { + Write-Warning " - Failed to install: $extension - $result" + $failureCount++ + } + } + catch { + Write-Warning " - Error installing: $extension - $_" + $failureCount++ + } + } + + # Summary + Write-Host " - Extension installation complete: $successCount successful, $failureCount failed" -ForegroundColor Gray + + return $true + } + catch { + Write-Error "Error importing VS Code configuration: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 new file mode 100644 index 0000000..d4e4e23 --- /dev/null +++ b/DevSetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 @@ -0,0 +1,39 @@ +BeforeAll { + . $PSScriptRoot\Export-DevSetupEnv.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Write-NewConfig.ps1 + Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } + Mock Get-DevSetupLocalEnvPath { "TestDrive:\DevSetupEnvs\local"} + Mock Write-NewConfig { param($OutFile) $OutFile } + Mock Write-Host { } + Mock Write-Error { } +} + +Describe "Export-DevSetupEnv" { + + Context "When called with a valid name" { + It "Should create the config file and return its path" { + $result = Export-DevSetupEnv -Name "MyEnv" + $result | Should -Be "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" + Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" } + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "exported to" -and $ForegroundColor -eq "Green" } + } + } + + Context "When called with a name that needs sanitization" { + It "Should sanitize the name and warn" { + $result = Export-DevSetupEnv -Name "Data Science Environment!" + $result | Should -Be "TestDrive:\DevSetupEnvs\local\DataScienceEnvironment.devsetup" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "sanitized" -and $ForegroundColor -eq "Yellow" } + } + } + + Context "When Write-NewConfig fails" { + It "Should write error and return null" { + Mock Write-NewConfig { param($OutFile) $null } + $result = Export-DevSetupEnv -Name "FailEnv" + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to create configuration file" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 new file mode 100644 index 0000000..8efc008 --- /dev/null +++ b/DevSetup/Private/Commands/Export-DevSetupEnv.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + Exports the current system environment to a new DevSetup configuration file. + +.DESCRIPTION + This function creates a new DevSetup environment configuration by scanning the current system + for installed packages and components. It automatically sanitizes the environment name to ensure + file system compatibility and exports the configuration to a YAML file in the DevSetup + environments directory. The function captures the current state of PowerShell modules, + Chocolatey packages, and other installed components for later reproduction. + +.PARAMETER Name + The name for the new environment configuration. + This parameter is mandatory and will be sanitized to contain only alphanumeric characters, hyphens, and periods. + The resulting YAML file will be named "{Name}.yaml" in the DevSetup environments directory. + +.OUTPUTS + [System.String] + Returns the full path to the created configuration file if successful. + Returns $null if the export operation fails. + +.EXAMPLE + Export-DevSetupEnv -Name "MyCurrentSetup" + + Exports the current system state to a configuration file named "MyCurrentSetup.yaml". + +.EXAMPLE + $configPath = Export-DevSetupEnv -Name "WebDev-2024" + if ($configPath) { + Write-Host "Configuration saved to: $configPath" + } else { + Write-Host "Export failed" + } + + Demonstrates capturing the return value to verify export success. + +.EXAMPLE + Export-DevSetupEnv -Name "Data Science Environment!" + + The exclamation mark will be removed, resulting in "DataScienceEnvironment.yaml". + A warning message will indicate the sanitization that occurred. + +.NOTES + - Automatically sanitizes the environment name by removing non-alphanumeric characters except hyphens and periods + - Displays a warning message if sanitization changes the original name + - Uses Get-DevSetupEnvPath to determine the target directory for the configuration file + - Calls Write-NewConfig to perform the actual system scanning and file creation + - Returns the full file path on success for further processing or verification + - Returns $null if Write-NewConfig fails to create the configuration + - The exported configuration can be used with Install-DevSetupEnv to recreate the environment + - Provides color-coded console output: Yellow for warnings, Green for success, Red for errors + +.LINK + +.COMPONENT + DevSetup.Commands + +.FUNCTIONALITY + Environment Export, Configuration Creation, System State Capture +#> + +Function Export-DevSetupEnv { + Param( + [string]$Name + ) + # Sanitize EnvName to only contain alphanumeric characters, hyphens, and periods + $sanitizedEnvName = $Name -replace '[^a-zA-Z0-9\-\.]', '' + if ($sanitizedEnvName -ne $Name) { + Write-Host "EnvName sanitized from '$Name' to '$sanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow + } + $Name = $sanitizedEnvName + $OutFile = Join-Path -Path (Get-DevSetupLocalEnvPath) -ChildPath "$Name.devsetup" + $config = Write-NewConfig -OutFile $OutFile + if (-not $config) { + Write-Error "Failed to create configuration file" + return $null + } + Write-Host "Configuration file exported to: $OutFile" -ForegroundColor Green + return $OutFile +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 b/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 new file mode 100644 index 0000000..1f2d442 --- /dev/null +++ b/DevSetup/Private/Commands/Initialize-DevSetup.Tests.ps1 @@ -0,0 +1,68 @@ +BeforeAll { + . $PSScriptRoot\Initialize-DevSetup.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Core\Install-CoreDependencies.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Initialize-DevSetupEnvs.ps1 + Mock Write-Host { } + Mock Write-Error { } + Mock Write-Verbose { } + Mock Install-CoreDependencies { $true } + Mock Get-DevSetupPath { "TestDrive:\Users\Test\devsetup" } + Mock Get-DevSetupEnvPath { "TestDrive:\Users\Test\devsetup\envs" } + Mock Get-DevSetupLocalEnvPath { "TestDrive:\Users\Test\devsetup\envs\local" } + Mock Get-DevSetupCommunityEnvPath { "TestDrive:\Users\Test\devsetup\envs\community" } + Mock Test-Path { $false } + Mock New-Item { } + Mock Initialize-DevSetupEnvs { "TestDrive:\Users\Test\devsetup\envs" } +} + +Describe "Initialize-DevSetup" { + + Context "When all steps succeed" { + It "Should install dependencies, create directories, and return true" { + $result = Initialize-DevSetup + $result | Should -Be $true + Assert-MockCalled Install-CoreDependencies -Exactly 1 -Scope It + Assert-MockCalled New-Item -Exactly 1 -Scope It + Assert-MockCalled Initialize-DevSetupEnvs -Exactly 1 -Scope It + #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "initialized at" -and $ForegroundColor -eq "Green" } + } + } + + Context "When core dependencies fail to install" { + It "Should write error and return nothing" { + Mock Install-CoreDependencies { $false } + $result = Initialize-DevSetup + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install core dependencies" } + } + } + + Context "When .devsetup directory already exists" { + It "Should not create the directory and should log verbose" { + Mock Test-Path { $true } + $result = Initialize-DevSetup + $result | Should -Be $true + Assert-MockCalled New-Item -Exactly 0 -Scope It + Assert-MockCalled Write-Verbose -Scope It -ParameterFilter { $Message -match "already exists" } + } + } + + Context "When environment path initialization fails" { + It "Should write error and return false" { + Mock Initialize-DevSetupEnvs { $null } + $result = Initialize-DevSetup + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment path" } + } + } + + Context "When an exception occurs during initialization" { + It "Should write error and return false" { + Mock Install-CoreDependencies { throw "Unexpected error" } + $result = Initialize-DevSetup + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Initialize-DevSetup.ps1 b/DevSetup/Private/Commands/Initialize-DevSetup.ps1 new file mode 100644 index 0000000..bd50b5f --- /dev/null +++ b/DevSetup/Private/Commands/Initialize-DevSetup.ps1 @@ -0,0 +1,114 @@ +<# +.SYNOPSIS + Initializes the DevSetup environment and directory structure. + +.DESCRIPTION + This function sets up the complete DevSetup environment by installing core dependencies and creating + the necessary directory structure. It performs a comprehensive initialization process including + dependency validation, directory creation, and environment path setup. The function ensures all + prerequisites are in place before DevSetup can be used for environment management operations. + +.OUTPUTS + [System.Boolean] + Returns $true if the DevSetup environment is successfully initialized. + Returns $false if initialization fails at any step. + +.EXAMPLE + Initialize-DevSetup + + Initializes the complete DevSetup environment with default settings. + +.EXAMPLE + if (Initialize-DevSetup) { + Write-Host "DevSetup is ready for use" + # Proceed with environment operations + } else { + Write-Host "DevSetup initialization failed" + # Handle initialization failure + } + + Demonstrates conditional logic based on initialization success. + +.EXAMPLE + $setupReady = Initialize-DevSetup + if ($setupReady) { + Use-DevSetup -List + } + + Shows using the function result to proceed with DevSetup operations. + +.NOTES + - This should be the first function called when setting up DevSetup + - Performs initialization in a specific sequence: + 1. Installs core dependencies via Install-CoreDependencies + 2. Creates the main .devsetup directory using Get-DevSetupPath + 3. Initializes the environments directory via Initialize-DevSetupEnvs + - Uses fail-fast approach - stops immediately if core dependencies cannot be installed + - Creates the .devsetup directory in the user's home directory if it doesn't exist + - Uses -Force flag for directory creation to handle any permission issues + - Suppresses directory creation output using Out-Null for clean console experience + - Provides verbose logging when .devsetup directory already exists + - Validates each initialization step and returns appropriate success/failure status + - Includes comprehensive try-catch error handling with descriptive error messages + - Color-coded console output for different phases: Cyan for progress, Green for success + +.LINK + +.COMPONENT + DevSetup.Commands + +.FUNCTIONALITY + Environment Setup, Directory Management, Dependency Installation +#> + +Function Initialize-DevSetup { + try { + # Install core dependencies first + Write-Host "- Installing core dependencies..." -ForegroundColor Cyan + if (-not (Install-CoreDependencies)) { + Write-Error "Failed to install core dependencies" + return + } + Write-Host "- Core dependencies installed successfully" -ForegroundColor Green + + # Define .devsetup folder path + $devSetupPath = Get-DevSetupPath + + # Check if .devsetup folder exists + if (-not (Test-Path -Path $devSetupPath)) { + #Write-Host "Creating .devsetup directory at: $devSetupPath" -ForegroundColor Cyan + New-Item -Path $devSetupPath -ItemType Directory -Force | Out-Null + #Write-Host ".devsetup directory created successfully" -ForegroundColor Green + } else { + Write-Verbose ".devsetup directory already exists at: $devSetupPath" + } + + Write-Host "" + Write-Host "- Installing community environments..." -ForegroundColor Cyan + # Initialize DevSetup environments path + $envSetupPath = Initialize-DevSetupEnvs + if (-not $envSetupPath) { + Write-Error "Failed to initialize DevSetup environment path" + return $false + } else { + Write-Host "- Community environments installed successfully" -ForegroundColor Green + } + + Write-Host "" + Write-Host "Path Information: " -ForegroundColor Yellow + Write-Host "- DevSetup:" -ForegroundColor Cyan + Write-Host " - $devSetupPath" -ForegroundColor Gray + Write-Host "- Local Environments: " -ForegroundColor Cyan + Write-Host " - $($envSetupPath.Local)" -ForegroundColor Gray + Write-Host "- Community Environments: " -ForegroundColor Cyan + Write-Host " - $($envSetupPath.Community)" -ForegroundColor Gray + Write-Host "" + + # Return the path for use by other functions + return $true + } + catch { + Write-Error "Failed to initialize DevSetup environment: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 new file mode 100644 index 0000000..05a5b21 --- /dev/null +++ b/DevSetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 @@ -0,0 +1,85 @@ +BeforeAll { + . $PSScriptRoot\Install-DevSetupEnv.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Install-PowershellModules.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackages.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Install-ScoopComponents.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 + Mock Get-DevSetupEnvPath { "C:\DevSetupEnvs" } + Mock Test-Path { $true } + Mock Read-ConfigurationFile { } + Mock Install-PowershellModules { } + Mock Install-ChocolateyPackages { } + Mock Install-ScoopComponents { } + Mock Write-Host { } + Mock Write-Error { } + Mock Write-Warning { } + Mock Invoke-Command { } + Mock Invoke-Expression { } +} + +Describe "Install-DevSetupEnv" { + + Context "When environment file does not exist" { + It "Should write error and return" { + Mock Test-Path { $false } + $result = Install-DevSetupEnv -Name "missing-env" + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } + } + } + + Context "When YAML parsing fails" { + It "Should write error and return" { + Mock Test-Path { $true } + Mock Read-ConfigurationFile { $null } + $result = Install-DevSetupEnv -Name "bad-yaml" + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } + } + } + + Context "When all dependencies install and no commands are present" { + It "Should install dependencies and write status" { + Mock Test-Path { $true } + Mock Read-ConfigurationFile { @{ devsetup = @{ } } } + $result = Install-DevSetupEnv -Name "basic-env" + $result | Should -Be $null + Assert-MockCalled Install-PowershellModules -Exactly 1 -Scope It + Assert-MockCalled Install-ChocolateyPackages -Exactly 1 -Scope It + Assert-MockCalled Install-ScoopComponents -Exactly 1 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No commands found" } + } + } + + Context "When commands are present and executed" { + It "Should execute all commands" { + $commands = @( + @{ command = "echo Hello"; packageName = "git" }, + @{ command = "echo World"; packageName = "nodejs" } + ) + Mock Test-Path { $true } + Mock Read-ConfigurationFile { @{ devsetup = @{ commands = $commands } } } + $result = Install-DevSetupEnv -Name "cmd-env" + $result | Should -Be $null + Assert-MockCalled Invoke-Expression -Exactly 2 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: git" } + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: nodejs" } + } + } + + Context "When a command entry is missing the command property" { + It "Should skip and warn" { + $commands = @( + @{ packageName = "git" }, + @{ command = "echo World"; packageName = "nodejs" } + ) + Mock Test-Path { $true } + Mock Read-ConfigurationFile { @{ devsetup = @{ commands = $commands } } } + $result = Install-DevSetupEnv -Name "missing-cmd" + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "missing command property" } + Assert-MockCalled Invoke-Expression -Exactly 1 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 new file mode 100644 index 0000000..5382ed1 --- /dev/null +++ b/DevSetup/Private/Commands/Install-DevSetupEnv.ps1 @@ -0,0 +1,152 @@ +<# +.SYNOPSIS + Installs a complete development environment from a YAML configuration file. + +.DESCRIPTION + This function orchestrates the installation of a development environment by reading a YAML configuration file + and processing all defined dependencies and commands. It sequentially installs PowerShell modules, Chocolatey + packages, Scoop buckets and packages, then executes any custom commands specified in the configuration. + The function provides comprehensive error handling and progress reporting throughout the installation process. + +.PARAMETER Name + The name of the environment configuration file to install (without the .yaml extension). + The function will look for a file named "{Name}.yaml" in the DevSetup environment path. + This parameter is mandatory and accepts positional input. + +.OUTPUTS + None. This function does not return a value but writes status information to the console. + +.EXAMPLE + Install-DevSetupEnv -Name "development" + + Installs the development environment from the "development.yaml" configuration file. + +.EXAMPLE + Install-DevSetupEnv "web-dev" + + Installs the web development environment using positional parameter syntax. + +.EXAMPLE + Install-DevSetupEnv -Name "my-environment" + + Demonstrates the PSCustomObject structure that would be parsed from the YAML file. + +.NOTES + - Requires the environment YAML file to exist in the DevSetup environment path + - Uses Get-DevSetupEnvPath to determine the configuration file location + - Returns early with error if YAML file is not found or cannot be parsed + - Processes dependencies in a specific order: PowerShell modules, Chocolatey packages, then Scoop components + - Commands are executed after all package installations are complete + - Individual installation failures do not stop the overall process + - Uses Read-ConfigurationFile to parse YAML configuration + - Leverages Install-PowershellModules, Install-ChocolateyPackages, and Install-ScoopComponents functions + - Custom commands are executed using Invoke-CommandFromEnv function + - Provides detailed console output with color-coded status messages + - Skips command entries that are missing the required command property + - Command execution includes package name context for better traceability + +.LINK + +.COMPONENT + DevSetup.Commands + +.FUNCTIONALITY + Environment Installation, Configuration Processing, Development Setup +#> + +Function Install-DevSetupEnv { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0, ParameterSetName = "Install")] + [string]$Name, + [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallPath")] + [string]$Path, + [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallUrl")] + [string]$Url + ) + + $YamlFile = $null + + if($PSBoundParameters.ContainsKey('Name')) { + $Provider = "local" + + if($Name -like "*:*") { + $parts = $Name.Split(":") + $Name = $parts[1]; + $Provider = $parts[0] + } + + $YamlFile = Join-Path -Path (Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider) -ChildPath "$Name.devsetup" + } elseif($PSBoundParameters.ContainsKey('Path')) { + if(-not (Test-Path -Path $Path)) { + Write-Error "Invalid Path provided" + return + } + $YamlFile = $Path + } elseif($PSBoundParameters.ContainsKey('Url')) { + $FileName = Split-Path $Url -Leaf + Write-Host "Downloading DevSetup environment from:" -ForegroundColor Cyan + Write-Host "- $Url" -ForegroundColor Gray + $YamlFile = Join-Path -Path (Get-DevSetupLocalEnvPath) -ChildPath $FileName + Write-Host "Saving Devsetup environment file to:" -ForegroundColor Cyan + Write-Host "- $YamlFile" -ForegroundColor Gray + if((Test-Path -Path $YamlFile)) { + Write-Warning "File $YamlFile already exists" + do { + if(($sAnswer = Read-Host "Overwrite existing file and continue? [Y/N]") -eq '') { $sAnswer = 'N' } + } until ($sAnswer.ToUpper()[0] -match '[yYnN]') + if(-not ($sAnswer.ToUpper()[0] -match '[Y]')) { + return + } + } + try { + Invoke-WebRequest -Uri $Url -OutFile $YamlFile | Out-Null + } catch { + Write-Error "Failed to download devsetup env file" + return + } + } + + if (-not (Test-Path $YamlFile)) { + Write-Error "Environment file not found: $YamlFile" + return + } + + Write-Host "Installing DevSetup environment from:" -ForegroundColor Cyan + Write-Host "- $YamlFile" -ForegroundColor Gray + Write-Host "" + + # Read the configuration from the YAML file + $YamlData = Read-ConfigurationFile -Config $YamlFile + + # Check if YAML data was successfully parsed + if ($null -eq $YamlData) { + Write-Error "Failed to parse YAML configuration from: $YamlFile" + return + } + + # Install PowerShell module dependencies + Install-PowershellModules -YamlData $YamlData | Out-Null + + # Install Chocolatey package dependencies + Install-ChocolateyPackages -YamlData $YamlData | Out-Null + + # Install Scoop package dependencies + Install-ScoopComponents -YamlData $YamlData | Out-Null + + # Execute any commands defined in the configuration + if ($YamlData.devsetup.commands -and $YamlData.devsetup.commands.Count -gt 0) { + Write-Host "Executing configuration commands..." -ForegroundColor Cyan + + foreach ($commandEntry in $YamlData.devsetup.commands) { + if ($commandEntry.command) { + Write-Host " - Executing command for: $($commandEntry.packageName)" -ForegroundColor Gray + Invoke-Expression -Command $commandEntry.command *> $null + } else { + Write-Warning "Skipping command entry with missing command property" + } + } + } else { + Write-Host "No commands found in configuration to execute." -ForegroundColor Gray + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 b/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 new file mode 100644 index 0000000..422e8cd --- /dev/null +++ b/DevSetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 @@ -0,0 +1,124 @@ +BeforeAll { + . $PSScriptRoot\Show-DevSetupEnvList.ps1 + . $PSScriptRoot\Show-DevSetupEnvList.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Optimize-DevSetupEnvs.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Format-PrettyTable.ps1 + Mock Get-DevSetupPath { "C:\DevSetup" } + Mock Optimize-DevSetupEnvs { } + Mock Write-Host { } + Mock Write-Warning { } + Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) $false } + Mock Test-Path { $true } + Mock Get-Content { '[{"name":"EnvWin","version":"1.0","platform":"windows","file":"envwin.yaml"},{"name":"EnvLinux","version":"2.0","platform":"linux","file":"envlinux.yaml"},{"name":"EnvMac","version":"3.0","platform":"macos","file":"envmac.yaml"},{"name":"EnvCross","version":"4.0","platform":"cross-platform","file":"envcross.yaml"},{"name":"EnvUnspec","version":"5.0","file":"envunspec.yaml"}]' } + Mock ConvertFrom-Json { + @( + @{ name = "EnvWin"; version = "1.0"; platform = "windows"; file = "envwin.yaml" }, + @{ name = "EnvLinux"; version = "2.0"; platform = "linux"; file = "envlinux.yaml" }, + @{ name = "EnvMac"; version = "3.0"; platform = "macos"; file = "envmac.yaml" }, + @{ name = "EnvCross"; version = "4.0"; platform = "cross-platform"; file = "envcross.yaml" }, + @{ name = "EnvUnspec"; version = "5.0"; file = "envunspec.yaml" } + ) + } +} + +Describe "Show-DevSetupEnvList" { + Context "When environments.json does not exist" { + It "Should run optimization to create it" { + Mock Test-Path { $false } + Show-DevSetupEnvList -Platform "all" + Assert-MockCalled Optimize-DevSetupEnvs -Exactly 1 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No environments index found" } + } + } + + Context "When environments.json is corrupt" { + It "Should run optimization to recreate it" { + Mock Test-Path { $true } + Mock Get-Content { throw "corrupt file" } + Show-DevSetupEnvList -Platform "all" + Assert-MockCalled Optimize-DevSetupEnvs -Exactly 1 -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to read environments.json" } + } + } + + Context "When filtering for current platform (windows)" { + It "Should detect windows platform and filter environments" { + Mock Format-PrettyTable { } + Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($Windows) { $true } else { $false } } + Show-DevSetupEnvList -Platform "current" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: windows" } + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } + + Context "When filtering for current platform (linux)" { + It "Should detect linux platform and filter environments" { + Mock Format-PrettyTable { } + Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($Linux) { $true } else { $false } } + Show-DevSetupEnvList -Platform "current" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: linux" } + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } + + Context "When filtering for current platform (macos)" { + It "Should detect macos platform and filter environments" { + Mock Format-PrettyTable { } + Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($MacOS) { $true } else { $false } } + Show-DevSetupEnvList -Platform "current" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: macos" } + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } + + Context "When filtering for all platforms" { + It "Should show all environments without filtering" { + Mock Format-PrettyTable { } + Show-DevSetupEnvList -Platform "all" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Showing all environments regardless of platform" } + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } + + Context "When filtering for specific platform (windows)" { + It "Should filter and display only windows-compatible environments" { + Mock Format-PrettyTable { } + Show-DevSetupEnvList -Platform "windows" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for platform: windows" } + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } + + Context "When no environments are found for a platform" { + It "Should display guidance message" { + Mock Format-PrettyTable { } + Mock ConvertFrom-Json { @() } + Show-DevSetupEnvList -Platform "windows" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No development environments found for platform: windows" } + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Use -Platform 'all' to see all available environments" } + Assert-MockCalled Format-PrettyTable -Exactly 0 -Scope It + } + } + + Context "When no environments exist at all" { + It "Should display no environments found message" { + Mock Format-PrettyTable { } + Mock ConvertFrom-Json { @() } + Show-DevSetupEnvList -Platform "all" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No development environments found." } + Assert-MockCalled Format-PrettyTable -Exactly 0 -Scope It + } + } + + Context "When environments are found" { + It "Should display the environments table and count" { + Mock Format-PrettyTable { } + Mock Write-Host { } + Show-DevSetupEnvList -Platform "all" + Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 b/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 new file mode 100644 index 0000000..70d60d7 --- /dev/null +++ b/DevSetup/Private/Commands/Show-DevSetupEnvList.ps1 @@ -0,0 +1,171 @@ +<# +.SYNOPSIS + Lists available development environment configurations with platform filtering. + +.DESCRIPTION + This function displays all available development environment configurations in a formatted table. + It supports platform-specific filtering to show only environments compatible with the current + system or a specified platform. The function reads environment metadata from environments.json + and automatically creates this index file if it doesn't exist using Optimize-DevSetupEnvs. + Environments can be filtered by Windows, Linux, macOS, or shown for all platforms. + +.PARAMETER Platform + The platform to filter environments by. + Valid values: "current", "all", "windows", "linux", "macos" + Default value is "current" which shows environments for the detected platform. + Use "all" to display environments regardless of platform compatibility. + +.OUTPUTS + [System.Boolean] + Returns $true when the function completes successfully, regardless of whether environments are found. + +.EXAMPLE + Show-DevSetupEnvList + + Lists development environments compatible with the current platform. + +.EXAMPLE + Show-DevSetupEnvList -Platform "all" + + Displays all available development environments regardless of platform. + +.EXAMPLE + Show-DevSetupEnvList -Platform "linux" + + Shows only environments specifically designed for Linux systems. + +.EXAMPLE + Show-DevSetupEnvList -Platform "windows" + + Lists environments compatible with Windows systems. + +.NOTES + - Automatically detects the current platform using [System.Environment]::OSVersion.Platform + - Maps platform detection: Win32NT → windows, Unix → linux/macos via uname command + - Uses 'uname -s' command on Unix systems to distinguish between Linux (default) and macOS (Darwin) + - Reads environment metadata from environments.json in the DevSetup directory + - Automatically creates environments.json index if missing using Optimize-DevSetupEnvs + - Recreates the index file if environments.json is corrupted or unreadable JSON + - Supports cross-platform environments that work on multiple operating systems + - Includes environments with empty/unspecified platform as compatible with all platforms + - Platform filtering includes exact matches, "cross-platform" tagged environments, and unspecified platforms + - Displays results in a formatted table showing Name, Version, Platform, and File columns + - Shows "Not specified" for missing platform information and "Unknown" for missing version + - Provides helpful guidance when no environments are found for the specified platform + - Platform filtering and matching is case-insensitive for user convenience + - Displays environment count summary after the table + - Uses color-coded console output for better user experience + +.LINK + +.COMPONENT + DevSetup.Commands + +.FUNCTIONALITY + Environment Discovery, Platform Detection, Configuration Listing +#> + +Function Show-DevSetupEnvList { + Param ( + [Parameter(Mandatory=$false, Position=0)] + [ValidateSet("current", "all", "windows", "linux", "macos")] + [string]$Platform = "current" # Default to current platform + ) + + + Write-Host "Listing available development environments..." -ForegroundColor Yellow + + # Determine the platform filter + $platformFilter = $Platform.ToLower() + if ($platformFilter -eq "current") { + # Get current system platform + if((Test-OperatingSystem -Windows)) { + $platformFilter = "windows" + } elseif((Test-OperatingSystem -Linux)) { + $platformFilter = "linux" + } elseif((Test-OperatingSystem -MacOS)) { + $platformFilter = "macos" + } else { + $platformFilter = "windows" + } + Write-Host "Filtering for current platform: $platformFilter" -ForegroundColor Gray + } elseif ($platformFilter -eq "all") { + Write-Host "Showing all environments regardless of platform" -ForegroundColor Gray + } else { + Write-Host "Filtering for platform: $platformFilter" -ForegroundColor Gray + } + + # Get the environments.json file path + $devSetupPath = Get-DevSetupPath + $environmentsJsonPath = Join-Path -Path $devSetupPath -ChildPath "environments.json" + + if (-not (Test-Path $environmentsJsonPath)) { + Write-Host "No environments index found. Running optimization to create it..." -ForegroundColor Cyan + Optimize-DevSetupEnvs | Out-Null + } else { + try { + # Read the environments.json file + $jsonContent = Get-Content -Path $environmentsJsonPath -Raw + $environments = $jsonContent | ConvertFrom-Json + } + catch { + Write-Warning "Failed to read environments.json. Running optimization to recreate it..." + Optimize-DevSetupEnvs | Out-Null + } + } + + # Filter environments by platform + if ($platformFilter -ne "all") { + $filteredEnvironments = @() + foreach ($env in $environments) { + $envPlatform = if ($env.platform) { $env.platform.ToLower() } else { "" } + # Match exact platform or cross-platform environments + if ($envPlatform -eq $platformFilter) { + $filteredEnvironments += $env + } + } + $environments = $filteredEnvironments + } + + if ($environments.Count -eq 0) { + if ($platformFilter -eq "all") { + Write-Host "No development environments found." -ForegroundColor Yellow + } else { + Write-Host "No development environments found for platform: $platformFilter" -ForegroundColor Yellow + Write-Host "Use -Platform 'all' to see all available environments." -ForegroundColor Gray + } + return $true + } + + # Create a formatted table + $tableData = @() + foreach ($env in $environments) { + $platformDisplay = if ($env.platform) { $env.platform } else { "Not specified" } + $versionDisplay = if ($env.version) { $env.version } else { "Unknown" } + $tableData += @{ + Name = $env.name + Version = $versionDisplay + Platform = $platformDisplay + File = $env.file + Provider = $env.provider + Color = "DarkGray" + } + } + + $columnDefinitions = [ordered]@{ + Name = @{ Name = "Name"; Width = 32; Alignment = "Left"; Color = "White"; Key = "Name" } + Version = @{ Name = "Version"; Width = 10; Alignment = "Center"; Color = "White"; Key = "Version" } + Platform = @{ Name = "Platform"; Width = 15; Alignment = "Center"; Color = "White"; Key = "Platform" } + Provider = @{ Name = "Provider"; Width = 15; Alignment = "Center"; Color = "White"; Key = "Provider" } + File = @{ Name = "File"; Width = 42; Alignment = "Left"; Color = "White"; Key = "File" } + } + + $tableFormat = @{ + BorderColor = "DarkGray" + } + + Format-PrettyTable -Columns $columnDefinitions -Rows $tableData -TableFormat $tableFormat + + Write-Host "Found $($environments.Count) environment(s)" -ForegroundColor Cyan + Write-Host "" +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 new file mode 100644 index 0000000..24bb3ed --- /dev/null +++ b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 @@ -0,0 +1,75 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-DevSetupEnv.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Uninstall-ScoopComponents.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Uninstall-ChocolateyPackages.ps1 + . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Uninstall-PowershellModules.ps1 + Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } + Mock Test-Path { $true } + Mock Read-ConfigurationFile { } + Mock Uninstall-PowershellModules { $true } + Mock Uninstall-ChocolateyPackages { $true } + Mock Uninstall-ScoopComponents { $true } + Mock Write-Host { } + Mock Write-Error { } +} + +Describe "Uninstall-DevSetupEnv" { + + Context "When environment file does not exist" { + It "Should write error and return" { + Mock Test-Path { $false } + $result = Uninstall-DevSetupEnv -Name "missing-env" + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } + } + } + + Context "When YAML parsing fails" { + It "Should write error and return" { + Mock Test-Path { $true } + Mock Read-ConfigurationFile { $null } + $result = Uninstall-DevSetupEnv -Name "bad-yaml" + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } + } + } + + Context "When all uninstallers succeed" { + It "Should call all uninstallers and write status" { + Mock Test-Path { $true } + Mock Read-ConfigurationFile { @{ devsetup = @{ } } } + $result = Uninstall-DevSetupEnv -Name "basic-env" + $result | Should -Be $null + Assert-MockCalled Uninstall-PowershellModules -Exactly 1 -Scope It + Assert-MockCalled Uninstall-ChocolateyPackages -Exactly 1 -Scope It + Assert-MockCalled Uninstall-ScoopComponents -Exactly 1 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Uninstalling DevSetup environment from:" } + } + } + + Context "When a component uninstaller fails" { + It "Should continue calling other uninstallers" { + $script:callCount = 0 + Mock Uninstall-PowershellModules { $script:callCount++; $false } + Mock Uninstall-ChocolateyPackages { $script:callCount++; $true } + Mock Uninstall-ScoopComponents { $script:callCount++; $true } + Mock Test-Path { $true } + Mock Read-ConfigurationFile { @{ devsetup = @{ } } } + $result = Uninstall-DevSetupEnv -Name "partial-fail" + $result | Should -Be $null + $script:callCount | Should -Be 3 + } + } + + Context "When an exception occurs during uninstall" { + It "Should write error and return" { + Mock Test-Path { $true } + Mock Read-ConfigurationFile { throw "Unexpected error" } + $result = Uninstall-DevSetupEnv -Name "exception-env" + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 new file mode 100644 index 0000000..a2b4190 --- /dev/null +++ b/DevSetup/Private/Commands/Uninstall-DevSetupEnv.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + Uninstalls a development environment configuration and removes all associated packages. + +.DESCRIPTION + This function removes a complete development environment by uninstalling all packages and components + defined in a YAML configuration file. It processes PowerShell modules, Chocolatey packages, and + Scoop packages in sequence, effectively reversing the installation performed by Install-DevSetupEnv. + The function validates the configuration file exists and can be parsed before proceeding with + the uninstallation process. + +.PARAMETER Name + The name of the environment configuration to uninstall. + This parameter is mandatory and must match an existing YAML configuration file in the DevSetup environments directory. + The file should be named "{Name}.yaml" and contain valid DevSetup configuration structure. + +.OUTPUTS + None + This function does not return a value but provides console output indicating the progress of uninstallation operations. + +.EXAMPLE + Uninstall-DevSetupEnv -Name "WebDev" + + Uninstalls all packages and components from the "WebDev" environment configuration. + +.EXAMPLE + Uninstall-DevSetupEnv "DataScience" + + Removes the complete "DataScience" development environment using positional parameter. + +.EXAMPLE + $envName = "GameDev" + Uninstall-DevSetupEnv -Name $envName + + Demonstrates using a variable to specify the environment name for uninstallation. + +.NOTES + - Requires the specified environment configuration file to exist in the DevSetup environments directory + - Uses Get-DevSetupEnvPath to locate the environments directory + - Validates YAML file existence before attempting to parse configuration + - Processes uninstallation in specific order: + 1. PowerShell modules via Uninstall-PowershellModules + 2. Chocolatey packages via Uninstall-ChocolateyPackages + 3. Scoop packages via Uninstall-ScoopComponents + - Each uninstaller function handles its own error reporting and validation + - Does not remove the YAML configuration file itself after uninstallation + - Provides descriptive error messages for missing or invalid configuration files + - Status variables are assigned but not currently used for flow control + +.LINK + +.COMPONENT + DevSetup.Commands + +.FUNCTIONALITY + Environment Management, Package Removal, Configuration Processing +#> + +Function Uninstall-DevSetupEnv { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$Name + ) + + try { + $Provider = "local" + + if($Name -like "*:*") { + $parts = $Name.Split(":") + $Name = $parts[1]; + $Provider = $parts[0] + } + + $YamlFile = Join-Path -Path (Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider) -ChildPath "$Name.devsetup" + #$YamlFile = Join-Path -Path (Get-DevSetupEnvPath) -ChildPath "$Name.yaml" + if (-not (Test-Path $YamlFile)) { + Write-Error "Environment file not found: $YamlFile" + return + } + + Write-Host "Uninstalling DevSetup environment from: $YamlFile" -ForegroundColor Cyan + + # Read the configuration from the YAML file + $YamlData = Read-ConfigurationFile -Config $YamlFile + + # Check if YAML data was successfully parsed + if ($null -eq $YamlData) { + Write-Error "Failed to parse YAML configuration from: $YamlFile" + return + } + + # Install PowerShell module dependencies + $status = Uninstall-PowershellModules -YamlData $YamlData + + # Install Chocolatey package dependencies + $status = Uninstall-ChocolateyPackages -YamlData $YamlData + + # Install Scoop package dependencies + $status = Uninstall-ScoopComponents -YamlData $YamlData + } catch { + Write-Error "An error occurred during uninstallation: $_" + return + } +} \ No newline at end of file diff --git a/DevSetup/Private/Enums/InstalledState.ps1 b/DevSetup/Private/Enums/InstalledState.ps1 new file mode 100644 index 0000000..9a4d05d --- /dev/null +++ b/DevSetup/Private/Enums/InstalledState.ps1 @@ -0,0 +1,11 @@ +Add-Type -Language CSharp -TypeDefinition @" + [System.FlagsAttribute] + public enum InstalledState { + NotInstalled = 0, + Installed = 1 << 0, + MinimumVersionMet = 1 << 1, + RequiredVersionMet = 1 << 2, + GlobalVersionMet = 1 << 3, + Pass = Installed | MinimumVersionMet | RequiredVersionMet | GlobalVersionMet + } +"@ \ No newline at end of file diff --git a/DevSetup/Private/Enums/TaskState.ps1 b/DevSetup/Private/Enums/TaskState.ps1 new file mode 100644 index 0000000..3f07168 --- /dev/null +++ b/DevSetup/Private/Enums/TaskState.ps1 @@ -0,0 +1,9 @@ +Add-Type -Language CSharp -TypeDefinition @" + [System.FlagsAttribute] + public enum TaskState { + Unknown = 0, + Pass = 1 << 0, + Warn = 1 << 1, + Fail = 1 << 2, + } +"@ \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 new file mode 100644 index 0000000..0c0cf58 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 @@ -0,0 +1,102 @@ +BeforeAll { + function ConvertTo-Yaml { } + . $PSScriptRoot\Export-InstalledChocolateyPackages.ps1 + . $PSScriptRoot\Get-ChocolateyPackageDependencies.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Get-ChocolateyPackageDependencies { @('chocolatey-core.extension') } + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + Mock ConvertTo-Yaml { param($obj) "yaml-output" } + Mock ConvertTo-Json { param($obj) "json-output" } + Mock Out-File { $true } + Mock Write-Host { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Write-Debug { } + Mock Write-Verbose { } +} + +Describe "Export-InstalledChocolateyPackages" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It + } + } + + Context "When no Chocolatey packages are found" { + It "Should warn and return true" { + Mock Test-RunningAsAdmin { $true } + Mock Invoke-Expression { @() } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No Chocolatey packages found" } + } + } + + Context "When Chocolatey packages are found and DryRun is used" { + It "Should display the YAML output and not write to file" { + Mock Invoke-Expression { @("git|2.40.0", "nodejs|18.16.0") } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Times 0 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } + } + } + + Context "When Chocolatey packages are found and OutFile is specified" { + It "Should write the YAML output to the specified file" { + Mock Invoke-Expression { @("git|2.40.0", "nodejs|18.16.0") } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" -OutFile "out.yaml" + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } + } + } + + Context "When YAML conversion fails" { + It "Should fallback to JSON output" { + Mock Invoke-Expression { @("git|2.40.0") } + Mock ConvertTo-Yaml { throw "YAML error" } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Json -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } + } + } + + Context "When Out-File fails" { + It "Should write error and return false" { + Mock Invoke-Expression { @("git|2.40.0") } + Mock Out-File { throw "File error" } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } + } + } + + Context "When package version changes" { + It "Should update the package version in the config" { + Mock Invoke-Expression { @("git|2.41.0") } + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.40.0" }) } } } } } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating package: git" } + } + } + + Context "When package is new" { + It "Should add the package to the config" { + Mock Invoke-Expression { @("newpkg|1.0.0") } + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } + $result = Export-InstalledChocolateyPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding package: newpkg" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 new file mode 100644 index 0000000..c259a60 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 @@ -0,0 +1,234 @@ +<# +.SYNOPSIS + Exports installed Chocolatey packages to a YAML configuration file. + +.DESCRIPTION + This function scans the system for installed Chocolatey packages and exports them to a YAML + configuration file in DevSetup format. It uses 'choco list --local-only --limit-output' to retrieve + comprehensive package information including versions. The function intelligently filters out + system packages and can update existing configuration files by merging new packages with existing ones. + +.PARAMETER Config + The path to the YAML configuration file to read from and write to. + This parameter is mandatory and specifies both the input and output file unless OutFile is specified. + +.PARAMETER OutFile + The path to save the updated YAML configuration. + Optional parameter that allows saving to a different file than the input Config file. + +.PARAMETER DryRun + Switch parameter that prevents writing to files and displays the resulting configuration to the console. + Useful for previewing changes before committing them to a file. + +.OUTPUTS + [System.Boolean] + Returns $true if the export completes successfully or if no packages are found. + Returns $false if there are errors during the export process. + +.EXAMPLE + Export-InstalledChocolateyPackages -Config "environment.yaml" + + Exports installed Chocolatey packages to the existing environment.yaml configuration file. + +.EXAMPLE + Export-InstalledChocolateyPackages -Config "current.yaml" -OutFile "backup.yaml" + + Reads from current.yaml and saves the updated configuration with installed packages to backup.yaml. + +.EXAMPLE + Export-InstalledChocolateyPackages -Config "dev-env.yaml" -DryRun + + Shows what the configuration would look like without actually saving to file. + +.NOTES + - Requires administrator privileges to access all installed packages + - Uses 'choco list --local-only --limit-output' for machine-readable package information + - Automatically filters out system packages: + * Packages ending with '.install' (installer packages) + * Packages starting with 'chocolatey' (Chocolatey system packages) + - Merges with existing YAML configuration, preserving other sections and structure + - Supports both simple string format and complex object format for packages + - Updates existing packages when versions have changed + - Converts string entries to hashtable format when version information is added + - Creates the devsetup.dependencies.chocolatey structure if it doesn't exist + - Provides detailed console output with color-coded status messages for operations + - Handles YAML conversion errors gracefully by falling back to JSON format + - Tracks package changes: new additions, version updates, and no-change skips + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Configuration Export, Package Discovery, YAML Generation +#> + +Function Export-InstalledChocolateyPackages { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Config, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$OutFile, + [Parameter(Mandatory = $false)] + [switch]$DryRun + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "This operation requires administrator privileges. Please run as administrator." + } + + # Get list of installed Chocolatey packages + Write-Host "- Getting list of installed Chocolatey packages..." -ForegroundColor Gray + $chocoList = Invoke-Expression "& choco list --local-only --limit-output" + + if (-not $chocoList) { + Write-Warning "No Chocolatey packages found or Chocolatey is not installed." + return $true + } + + $chocolateyPackages = @() + + $packagesToIgnore = Get-ChocolateyPackageDependencies | Select-Object -Unique + + foreach ($line in $chocoList) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + + # Parse package info (format: packagename|version) + $parts = $line.Split('|') + if ($parts.Count -ge 2) { + $packageName = $parts[0].Trim() + $version = $parts[1].Trim() + + # Skip packages starting with chocolatey + if ($packageName -like "chocolatey*") { + Write-Verbose "Skipping chocolatey package: $packageName" + continue + } + + if($packagesToIgnore -contains $packageName) { + Write-Verbose "Skipping ignored package: $packageName" + continue + } + + Write-Debug "Found package: $packageName (version: $version)" + $chocolateyPackages += @{ + name = $packageName + version = $version + } + } + } + + Write-Debug "Found $($chocolateyPackages.Count) Chocolatey packages (excluding .install and chocolatey* packages)" + + # Read existing YAML configuration + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure chocolateyPackages section exists + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } + if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } + + # Add packages to YAML data + foreach ($package in $chocolateyPackages) { + # Check if package already exists + $existingPackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { + ($_ -is [string] -and $_ -eq $package.name) -or + ($_ -is [hashtable] -and $_.name -eq $package.name) + } + + if (-not $existingPackage) { + Write-Host " - Adding package: $($package.name) ($($package.version))" -ForegroundColor Gray + $YamlData.devsetup.dependencies.chocolatey.packages += @{ + name = $package.name + version = $package.version + } + } else { + # Package exists, check if version has changed + $existingVersion = $null + if ($existingPackage -is [hashtable] -and $existingPackage.version) { + $existingVersion = $existingPackage.version + } + + if ($existingVersion -and $existingVersion -ne $package.version) { + Write-Host " - Updating package: $($package.name) ($existingVersion -> $($package.version))" -ForegroundColor Cyan + + # Find index and update + $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) + + # Preserve existing package structure but update version + if ($existingPackage -is [string]) { + # Convert string to hashtable with version + $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ + name = $package.name + version = $package.version + } + } else { + # Update existing hashtable + $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version + } + } elseif (-not $existingVersion) { + Write-Host " - Updating package: $($package.name)" -ForegroundColor Yellow + + # Find index and add version + $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) + + if ($existingPackage -is [string]) { + # Convert string to hashtable with version + $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ + name = $package.name + version = $package.version + } + } else { + # Add version to existing hashtable + $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version + } + } else { + Write-Host " - Skipping package (No Change): $($package.name) ($($package.version))" -ForegroundColor Gray + } + } + } + + # Convert to YAML + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + } + catch { + Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" + $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 + } + + # Handle output based on parameters + if ($DryRun) { + Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan + Write-Host $yamlOutput -ForegroundColor White + Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow + } else { + # Determine output file + $outputFile = if ($OutFile) { $OutFile } else { $Config } + + try { + Write-Debug "`nSaving configuration to: $outputFile" + $yamlOutput | Out-File -FilePath $outputFile + Write-Debug "Configuration saved successfully!" + } + catch { + Write-Error "Failed to save configuration to $outputFile`: $_" + return $false + } + } + + Write-Host "Chocolatey packages conversion completed!" -ForegroundColor Green + return $true + } + catch { + Write-Error "Error converting Chocolatey packages: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 new file mode 100644 index 0000000..bf6cb94 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 @@ -0,0 +1,32 @@ +BeforeAll { + . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1 + Mock Write-Error { } +} + +Describe "Get-ChocolateyCacheFile" { + + Context "When Get-DevSetupCachePath returns a valid path" { + It "Should return the correct cache file path" { + Mock Get-DevSetupCachePath { return "C:\Users\Test\.devsetup\.cache" } + $result = Get-ChocolateyCacheFile + $result | Should -Be "C:\Users\Test\.devsetup\.cache\chocolatey.cache" + } + } + + Context "When Get-DevSetupCachePath returns a different path" { + It "Should append chocolatey.cache to the returned path" { + Mock Get-DevSetupCachePath { return "D:\DevSetupCache" } + $result = Get-ChocolateyCacheFile + $result | Should -Be "D:\DevSetupCache\chocolatey.cache" + } + } + + Context "When Get-DevSetupCachePath returns an empty string" { + It "Should write error and return null" { + Mock Get-DevSetupCachePath { return "" } + $result = Get-ChocolateyCacheFile + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 new file mode 100644 index 0000000..78472fb --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS + Gets the file path for the Chocolatey package cache file. + +.DESCRIPTION + This function constructs and returns the full path to the Chocolatey package cache file within the DevSetup + cache directory. The cache file is used to store information about installed Chocolatey packages and their + versions for performance optimization and offline reference. The function uses Get-DevSetupCachePath + to ensure the cache directory exists before returning the file path. + +.OUTPUTS + [System.String] + Returns the full path to the Chocolatey cache file (chocolatey.cache) within the DevSetup cache directory. + +.EXAMPLE + Get-ChocolateyCacheFile + + Returns the path to the Chocolatey cache file, e.g., "C:\Users\Username\.devsetup\.cache\chocolatey.cache" + +.EXAMPLE + $chocoCacheFile = Get-ChocolateyCacheFile + if (Test-Path $chocoCacheFile) { + $cachedData = Get-Content $chocoCacheFile + } + + Gets the cache file path and checks if it exists before reading cached data. + +.EXAMPLE + $cacheFile = Get-ChocolateyCacheFile + Export-Clixml -Path $cacheFile -InputObject $chocolateyPackages + + Uses the cache file path to save Chocolatey package information. + +.NOTES + - Uses Get-DevSetupCachePath to ensure the cache directory exists + - Returns a consistent file path (chocolatey.cache) within the DevSetup cache structure + - The cache file is used for storing Chocolatey package metadata and version information + - Does not create the cache file itself - only returns the path where it should be located + - Used by other Chocolatey-related functions for performance optimization and data persistence + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Path Management, Cache Management, File System Operations +#> + +Function Get-ChocolateyCacheFile { + [CmdletBinding()] + Param() + + # Get the DevSetup cache path + $cachePath = Get-DevSetupCachePath + if([string]::IsNullOrEmpty($cachePath)) { + Write-Error "Failed to retrieve DevSetup cache path." + return $null + } + + # Construct the full path to the cache file + $cacheFilePath = Join-Path -Path $cachePath -ChildPath "chocolatey.cache" + + return $cacheFilePath +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 new file mode 100644 index 0000000..100e8d1 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 @@ -0,0 +1,96 @@ +BeforeAll { + . $PSScriptRoot\Get-ChocolateyPackageDependencies.ps1 + Mock Write-Debug { } + $isPS5 = $PSVersionTable.PSVersion.Major -eq 5 +} + +Describe "Get-ChocolateyPackageDependencies" { + + Context "When Chocolatey install path does not exist" { + It "Should return $null in PS5, empty array in PS6+" { + Mock Test-Path { return $false } + $result = Get-ChocolateyPackageDependencies + $result | Should -Be $null + } + } + + Context "When no nuspec files are found" { + It "Should return $null in PS5, empty array in PS6+" { + Mock Test-Path { return $true } + Mock Get-ChildItem { @() } + $result = Get-ChocolateyPackageDependencies + $result | Should -Be $null + } + } + + Context "When nuspec files have no dependencies" { + It "Should return $null in PS5, empty array in PS6+" { + Mock Test-Path { return $true } + Mock Get-ChildItem { + @( + [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } + ) + } + Mock Get-Content { + '' + } + $result = Get-ChocolateyPackageDependencies + $result | Should -Be $null + } + } + + Context "When nuspec files have dependencies including chocolatey system packages" { + It "Should return only non-chocolatey dependencies" { + Mock Test-Path { return $true } + Mock Get-ChildItem { + @( + [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } + ) + } + Mock Get-Content { + ' + + + + ' + } + $result = Get-ChocolateyPackageDependencies + $result | Should -Not -Be $null + $result | Should -Contain "git" + $result | Should -Contain "nodejs" + $result | Should -Not -Contain "chocolatey-core.extension" + } + } + + Context "When multiple nuspec files have overlapping dependencies" { + It "Should return all dependencies including duplicates" { + Mock Test-Path { return $true } + Mock Get-ChildItem { + @( + [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" }, + [PSCustomObject]@{ FullName = "C:\choco\lib\bar\bar.nuspec" } + ) + } + $nuspecs = @( + ' + + + ', + ' + + + ' + ) + $script:callCount = 0 + Mock Get-Content -MockWith { + $nuspecs[$script:callCount++] + } + $result = Get-ChocolateyPackageDependencies + $result | Should -Not -Be $null + $result | Should -Contain "git" + $result | Should -Contain "nodejs" + $result | Should -Contain "python" + ($result | Where-Object { $_ -eq "nodejs" }).Count | Should -Be 2 + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 new file mode 100644 index 0000000..0c4e424 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + Retrieves all package dependencies from installed Chocolatey packages. + +.DESCRIPTION + This function scans all installed Chocolatey packages and extracts their dependency information + by parsing the .nuspec files in the Chocolatey lib directory. It reads the XML metadata from + each package's nuspec file and collects all non-Chocolatey dependencies into a consolidated + list. The function automatically filters out Chocolatey-specific dependencies to focus on + actual package dependencies. + +.OUTPUTS + [System.Array] + Returns an array of package dependency names (strings) found across all installed packages. + Returns an empty array if no dependencies are found or Chocolatey is not installed. + +.EXAMPLE + Get-ChocolateyPackageDependencies + + Returns all package dependencies from installed Chocolatey packages. + +.EXAMPLE + $dependencies = Get-ChocolateyPackageDependencies + if ($dependencies.Count -gt 0) { + Write-Host "Found $($dependencies.Count) dependencies" + $dependencies | ForEach-Object { Write-Host "- $_" } + } + + Demonstrates retrieving and displaying all package dependencies. + +.EXAMPLE + $allDeps = Get-ChocolateyPackageDependencies + $uniqueDeps = $allDeps | Select-Object -Unique | Sort-Object + + Gets all dependencies and creates a sorted list of unique dependency names. + +.NOTES + - Requires Chocolatey to be installed with packages in the standard lib directory + - Uses $Env:ChocolateyInstall environment variable to locate the Chocolatey installation + - Scans all .nuspec files recursively in the Chocolatey lib directory + - Parses XML metadata from nuspec files to extract dependency information + - Automatically filters out dependencies with IDs starting with "chocolatey" (Chocolatey system packages) + - Returns all dependencies in a flat array, including duplicates from multiple packages + - Provides debug logging for troubleshooting package discovery issues + - Returns empty array gracefully if Chocolatey installation path is not found + - Uses ForEach-Object (%) for efficient processing of large package collections + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Dependency Analysis, Package Management, Metadata Extraction +#> + +Function Get-ChocolateyPackageDependencies { + [CmdletBinding()] + Param() + + write-Debug "Retrieving Chocolatey package dependencies..." + $packageDependencies = @() + + $chocolateyInstallPath = Join-Path $Env:ChocolateyInstall lib + if (-not (Test-Path $chocolateyInstallPath)) { + Write-Debug "Chocolatey installation path not found: $chocolateyInstallPath" + return $packageDependencies + } + + Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | % { + $dependencies = ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | Foreach-Object { + if (-not ($_.id -like "chocolatey*")) { + $_.id + } + } + + if ($dependencies) { + $packageDependencies = $packageDependencies + $dependencies; + } + } + return [array]$packageDependencies +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 new file mode 100644 index 0000000..7494a1f --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 @@ -0,0 +1,46 @@ +BeforeAll { + . $PSScriptRoot\Get-ChocolateyVersion.ps1 + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + Mock Write-Warning { } +} + +Describe "Get-ChocolateyVersion" { + + Context "When Chocolatey is not installed" { + It "Should return null and write a warning" { + Mock Test-ChocolateyInstalled { return $false } + $result = Get-ChocolateyVersion + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } + } + } + + Context "When Chocolatey is installed and version is returned" { + It "Should return the trimmed version string" { + Mock Test-ChocolateyInstalled { return $true } + Mock Invoke-Expression { " 1.4.0 " } + $result = Get-ChocolateyVersion + $result | Should -Be "1.4.0" + } + } + + Context "When Chocolatey is installed but version is not returned" { + It "Should return null and write a warning" { + Mock Test-ChocolateyInstalled { return $true } + Mock Invoke-Expression { $null } + $result = Get-ChocolateyVersion + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve" } + } + } + + Context "When an error occurs during version retrieval" { + It "Should return null and write a warning" { + Mock Test-ChocolateyInstalled { return $true } + Mock Invoke-Expression { throw "choco error" } + $result = Get-ChocolateyVersion + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "An error occurred" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 new file mode 100644 index 0000000..0f14d5d --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Retrieves the version of the installed Chocolatey package manager. + +.DESCRIPTION + This function gets the version information from Chocolatey by executing the 'choco --version' command. + It includes validation to ensure Chocolatey is installed before attempting to retrieve version information + and provides comprehensive error handling with appropriate warning messages for various failure scenarios. + +.OUTPUTS + [System.String] + Returns the Chocolatey version string (trimmed of whitespace) if successful. + Returns $null if Chocolatey is not installed, version retrieval fails, or an error occurs. + +.EXAMPLE + Get-ChocolateyVersion + + Returns the installed Chocolatey version, e.g., "1.4.0" + +.EXAMPLE + $chocoVersion = Get-ChocolateyVersion + if ($chocoVersion) { + Write-Host "Chocolatey version: $chocoVersion" + } else { + Write-Host "Could not determine Chocolatey version" + } + + Demonstrates capturing and validating the version result. + +.EXAMPLE + $version = Get-ChocolateyVersion + if ($version -and [version]$version -lt [version]"1.0.0") { + Write-Warning "Chocolatey version is outdated. Consider upgrading." + } + + Shows version comparison for compatibility checking. + +.NOTES + - Requires Chocolatey to be installed on the system + - Uses Test-ChocolateyInstalled to verify Chocolatey availability before proceeding + - Returns $null immediately if Chocolatey is not installed + - Suppresses stderr output using '2>$null' to avoid console clutter + - Trims whitespace from the version string for clean output + - Includes comprehensive try-catch error handling + - Provides descriptive warning messages for different failure scenarios + - Does not require administrator privileges + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Version Detection, System Information, Package Manager Utilities +#> + +Function Get-ChocolateyVersion { + [CmdletBinding()] + Param( + ) + + if (-not (Test-ChocolateyInstalled)) { + Write-Warning "Chocolatey is not installed. Cannot retrieve version." + return $null + } + + try { + $version = Invoke-Expression "& choco --version" 2>$null + if ($version) { + return $version.Trim() + } else { + Write-Warning "Failed to retrieve Chocolatey version." + return $null + } + } catch { + Write-Warning "An error occurred while trying to get Chocolatey version: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 new file mode 100644 index 0000000..e213399 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 @@ -0,0 +1,96 @@ +BeforeAll { + . $PSScriptRoot\Install-Chocolatey.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } + Mock Write-Host { } + Mock Write-Error { } + Mock Test-RunningAsAdmin { return $true } + Mock Get-Command { $null } + Mock Invoke-Expression { } + Mock Set-ExecutionPolicy { } +} + +Describe "Install-Chocolatey" { + + Context "When not running on Windows" { + It "Should skip installation and return true" { + Mock Test-OperatingSystem { param($Windows) $false } + $result = Install-Chocolatey + $result | Should -Be $true + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match "not available on this platform" } + } + } + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-OperatingSystem { param($Windows) $true } + Mock Test-RunningAsAdmin { return $false } + $result = Install-Chocolatey + $result | Should -Be $false + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "administrator privileges" } + } + } + + Context "When Chocolatey is already installed" { + It "Should return true and show version" { + Mock Test-OperatingSystem { param($Windows) $true } + Mock Test-RunningAsAdmin { return $true } + Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } + Mock Invoke-Expression { "1.4.0" } + $result = Install-Chocolatey + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } + } + } + + Context "When Chocolatey is not installed and installation succeeds" { + It "Should install and return true" { + Mock Test-OperatingSystem { param($Windows) $true } + $script:installCalled = $false + $script:commandCallCount = 0 + Mock Test-RunningAsAdmin { return $true } + Mock Get-Command -MockWith { + $script:commandCallCount++ + if ($script:commandCallCount -eq 1) { return $null } + else { return [PSCustomObject]@{ Name = "choco" } } + } + Mock Invoke-Expression -MockWith { + param($expr) + if ($expr -like "*--version*") { return "1.4.0" } + $script:installCalled = $true + } + $result = Install-Chocolatey + $result | Should -Be $true + $script:installCalled | Should -Be $true + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } + } + } + + Context "When Chocolatey is not installed and installation fails" { + It "Should return false and write error" { + Mock Test-OperatingSystem { param($Windows) $true } + $script:commandCallCount = 0 + Mock Test-RunningAsAdmin { return $true } + Mock Get-Command -MockWith { + $script:commandCallCount++ + return $null + } + Mock Invoke-Expression { } + $result = Install-Chocolatey + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } + } + } + + Context "When an unexpected error occurs" { + It "Should return false and write error" { + Mock Test-OperatingSystem { param($Windows) $true } + Mock Test-RunningAsAdmin { throw "Unexpected error" } + $result = Install-Chocolatey + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing Chocolatey" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 new file mode 100644 index 0000000..66a6744 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 @@ -0,0 +1,113 @@ +<# +.SYNOPSIS + Installs Chocolatey package manager on Windows systems. + +.DESCRIPTION + This function installs the Chocolatey package manager by downloading and executing the official + installation script from the Chocolatey website. It includes comprehensive validation for platform + compatibility, administrator privileges, and existing installations. The function handles security + protocol configuration and execution policy adjustments required for the installation process. + +.OUTPUTS + [System.Boolean] + Returns $true if Chocolatey is successfully installed or already exists. + Returns $false if the installation fails or system requirements are not met. + +.EXAMPLE + Install-Chocolatey + + Installs Chocolatey package manager on the current system. + +.EXAMPLE + if (Install-Chocolatey) { + Write-Host "Chocolatey is ready for use" + # Proceed with package installations + } else { + Write-Host "Failed to install Chocolatey" + # Handle installation failure + } + + Demonstrates conditional logic based on installation success. + +.EXAMPLE + $chocoReady = Install-Chocolatey + if ($chocoReady) { + choco install git -y + } + + Shows using the function result to proceed with package operations. + +.NOTES + - Requires administrator privileges on Windows systems + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Automatically skips installation on non-Windows platforms (returns $true) + - Checks for existing Chocolatey installation before attempting download + - Sets execution policy to Bypass for the current process scope during installation + - Configures TLS 1.2 security protocol for secure download + - Downloads installation script from https://community.chocolatey.org/install.ps1 + - Verifies successful installation by checking for 'choco' command availability + - Displays version information after successful installation + - Uses comprehensive try-catch error handling with descriptive error messages + - Suppresses command output using Out-Null to avoid console clutter + - Returns $true even if Chocolatey is already installed (idempotent behavior) + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Package Manager Installation, System Setup, Prerequisites Management +#> + +Function Install-Chocolatey { + [CmdletBinding()] + Param() + + try { + # Check if we're on Windows - Chocolatey is Windows-only + if (-not (Test-OperatingSystem -Windows)) { + Write-Host "Chocolatey is not available on this platform. Skipping installation." -ForegroundColor Yellow + return $true + } + + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "Chocolatey installation requires administrator privileges. Please run as administrator." + } + + Write-StatusMessage "- Installing Chocolatey package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + # Check if chocolatey is installed by testing the command + $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue + + if ($chocoInstalled) { + $chocoVersion = Invoke-Expression "& choco --version" 2>$null + #Write-Host "Chocolatey is already installed (version: $chocoVersion)" -ForegroundColor Green + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + #Write-Host "Chocolatey not found. Installing Chocolatey..." -ForegroundColor Cyan + + # Set security protocols and execution policy + Set-ExecutionPolicy Bypass -Scope Process -Force | Out-Null + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + + # Download and install Chocolatey + (Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) *> $null) *> $null + + # Verify installation + $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue + if ($chocoInstalled) { + $chocoVersion = Invoke-Expression "& choco --version" 2>$null + #Write-Host "Chocolatey successfully installed (version: $chocoVersion)!" -ForegroundColor Green + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + throw "Failed to install Chocolatey" + } + } + return $true + } + catch { + Write-Error "Error checking/installing Chocolatey: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 new file mode 100644 index 0000000..2299ecf --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 @@ -0,0 +1,100 @@ +BeforeAll { + . $PSScriptRoot\Install-ChocolateyPackage.ps1 + . $PSScriptRoot\Test-ChocolateyPackageInstalled.ps1 + . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Test-ChocolateyPackageInstalled { } + Mock Uninstall-ChocolateyPackage { $true } + Mock Get-Command { "choco" } + Mock Invoke-Command { } + Mock Write-ChocolateyCache { $true } + Mock Write-Debug { } + Mock Write-Warning { } + Mock Write-Error { } +} + +Describe "Install-ChocolateyPackage" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } + } + } + + Context "When package is already installed and version matches" { + It "Should return true immediately" { + Mock Test-ChocolateyPackageInstalled { + [InstalledState]::Pass -bor [InstalledState]::Installed + } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $true + } + } + + Context "When package is installed but version does not match" { + It "Should uninstall and reinstall the package" { + Mock Test-ChocolateyPackageInstalled { [InstalledState]::Installed } + $script:uninstallCalled = $false + Mock Uninstall-ChocolateyPackage -MockWith { + param($PackageName) + $script:uninstallCalled = $true + $true + } + $LASTEXITCODE = 0 + Mock Invoke-Command { } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $true + $script:uninstallCalled | Should -Be $true + } + } + + Context "When installing with version and params" { + It "Should build the correct choco command" { + $LASTEXITCODE = 0 + $script:paramsPassed = $null + Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } + Mock Invoke-Command -MockWith { + param($ScriptBlock) + $script:paramsPassed = $ScriptBlock.ToString() + } + $result = Install-ChocolateyPackage -PackageName "git" -Version "2.42.0" -Param "/silent" + $result | Should -Be $true + # You can add more checks for $paramsPassed if needed + } + } + + Context "When installation fails (non-zero exit code)" { + It "Should write error and return false" { + $LASTEXITCODE = 1 + Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } + } + } + + Context "When Write-ChocolateyCache fails after install" { + It "Should write warning and return false" { + $LASTEXITCODE = 0 + Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } + Mock Write-ChocolateyCache { $false } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } + } + } + + Context "When an exception occurs during install" { + It "Should write error and return false" { + Mock Test-ChocolateyPackageInstalled { throw "Unexpected error" } + $result = Install-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing package" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 new file mode 100644 index 0000000..6342b5f --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 @@ -0,0 +1,154 @@ +<# +.SYNOPSIS + Installs a Chocolatey package with optional version and parameter specification. + +.DESCRIPTION + This function installs a Chocolatey package using the 'choco install' command with comprehensive + validation and conflict resolution. It checks for existing installations, handles version conflicts + by reinstalling when necessary, and validates administrator privileges before proceeding. The function + supports custom installation parameters and provides detailed error handling throughout the process. + +.PARAMETER PackageName + The name of the Chocolatey package to install. + This parameter is mandatory and must be a valid, non-empty string representing a Chocolatey package name. + +.PARAMETER Version + The specific version of the package to install. + Optional parameter that specifies the exact version required. If not provided, the latest version is installed. + +.PARAMETER Param + Custom installation parameters to pass to the Chocolatey package. + Optional parameter that allows passing package-specific installation arguments using the --params flag. + +.OUTPUTS + [System.Boolean] + Returns $true if the package was successfully installed or already meets requirements. + Returns $false if the installation failed or insufficient privileges. + +.EXAMPLE + Install-ChocolateyPackage -PackageName "git" + + Installs the latest version of git package. + +.EXAMPLE + Install-ChocolateyPackage -PackageName "nodejs" -Version "18.17.0" + + Installs a specific version of nodejs package. + +.EXAMPLE + Install-ChocolateyPackage -PackageName "googlechrome" -Param "/nogoogle" + + Installs Google Chrome with custom installation parameters. + +.EXAMPLE + $result = Install-ChocolateyPackage -PackageName "vscode" -Version "1.75.0" -Param "/silent" + if ($result) { + Write-Host "Visual Studio Code installed successfully" + } else { + Write-Host "Failed to install Visual Studio Code" + } + + Demonstrates capturing the return value and using custom parameters. + +.NOTES + - Requires administrator privileges to install packages + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Uses Test-ChocolateyPackageInstalled to check existing installations + - Automatically uninstalls existing packages when version conflicts exist + - Uses comprehensive logic to determine installation necessity: + * Returns immediately if package with correct version exists + * Uninstalls and reinstalls if package exists but version differs + * Installs directly if package doesn't exist + - Uses $LASTEXITCODE to verify command execution success + - Includes comprehensive try-catch error handling with descriptive error messages + - Provides detailed debug logging for troubleshooting installation issues + - Suppresses command output using Out-Null to avoid console clutter + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Package Management, Software Installation, Version Control +#> + +Function Install-ChocolateyPackage { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [String] $PackageName, + + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [String] $Version, + + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [String] $Param + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "Chocolatey package installation requires administrator privileges. Please run as administrator." + } + + $testParams = @{ + PackageName = $PackageName + } + + if($PSBoundParameters.ContainsKey('Version')) { + $testParams.Version = $Version + } + + $testResult = Test-ChocolateyPackageInstalled @testParams + + if($testResult.HasFlag([InstalledState]::Pass)) { + return $true + } + + if($testResult.HasFlag([InstalledState]::Installed)) { + Uninstall-ChocolateyPackage -PackageName $PackageName | Out-Null + } + + $installParams = @( + 'install', + '-y', + $PackageName + ) + + if($PSBoundParameters.ContainsKey('Version')) { + $installParams = $installParams + @('--version', $Version) + } + + if($PSBoundParameters.ContainsKey('Param')) { + $installParams = $installParams + @('--params', $Param) + } + + $chocoCommand = Get-Command choco -ErrorAction SilentlyContinue + + $command = { + & $chocoCommand @installParams + } + + Invoke-Command -ScriptBlock $command | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Debug "INSTALL:Successfully installed: $PackageName" + if (-not (Write-ChocolateyCache)) { + Write-Warning "Failed to write Chocolatey cache." + return $false + } + return $true + } else { + Write-Error "Failed to install: $PackageName" + return $false + } + } + catch { + Write-Error "Error checking/installing package $PackageName`: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 new file mode 100644 index 0000000..75d45a7 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 @@ -0,0 +1,147 @@ +BeforeAll { + . $PSScriptRoot\Install-ChocolateyPackages.ps1 + . $PSScriptRoot\Install-ChocolateyPackage.ps1 + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\Read-ChocolateyCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Write-ChocolateyCache { $true } + Mock Write-Warning { } + Mock Write-StatusMessage { } -Verifiable + Mock Install-ChocolateyPackage { $true } + Mock Write-Error {} + Mock Write-Host {} +} + +Describe "Install-ChocolateyPackages" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Install-ChocolateyPackages -YamlData @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + $result | Should -Be $false + } + } + + Context "When Chocolatey packages config is missing" { + It "Should write warning and return" { + $yamlData = @{ devsetup = @{ dependencies = @{ } } } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not found" } + } + } + + Context "When Write-ChocolateyCache fails" { + It "Should write warning and return false" { + Mock Write-ChocolateyCache { $false } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $false + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } + } + } + + Context "When all packages install successfully (string format)" { + It "Should process all packages and return true" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git", "nodejs") + } + } + } + } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It + #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "installation completed" } + } + } + + Context "When all packages install successfully (object format)" { + It "Should process all packages and return true" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git"; version = "2.42.0" }, + @{ name = "nodejs"; params = "/silent" } + ) + } + } + } + } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It + } + } + + Context "When some packages fail to install" { + It "Should continue processing and return true" { + $callCount = 0 + Mock Install-ChocolateyPackage -MockWith { + param($PackageName, $Version, $Param) + $callCount++ + if ($callCount -eq 1) { $true } else { $false } + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git", "nodejs") + } + } + } + } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It + #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -eq "[FAILED]" } + } + } + + Context "When package entry is empty or missing name" { + It "Should skip invalid entries and continue" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + $null, + @{ version = "1.0.0" }, + "git" + ) + } + } + } + } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "no name specified" } + } + } + + Context "When an exception occurs during installation" { + It "Should write error and return false" { + Mock Install-ChocolateyPackage { throw "Unexpected error" } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git") + } + } + } + } + $result = Install-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error installing Chocolatey packages" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 new file mode 100644 index 0000000..3583f32 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 @@ -0,0 +1,162 @@ +<# +.SYNOPSIS + Installs Chocolatey packages from YAML configuration data. + +.DESCRIPTION + This function processes YAML configuration data to install Chocolatey packages using Install-ChocolateyPackage. + It supports both simple string formats and complex object formats for packages, allowing for detailed + configuration including versions and custom installation parameters. The function validates administrator + privileges before proceeding and provides comprehensive error handling and progress reporting throughout + the installation process. + +.PARAMETER YamlData + The YAML configuration data containing Chocolatey package definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.chocolatey.packages + +.OUTPUTS + [System.Boolean] + Returns $true if installation completes successfully (even if individual packages fail). + Returns $false if configuration is invalid or critical errors occur. + +.EXAMPLE + $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml + Install-ChocolateyPackages -YamlData $yamlData + + Installs Chocolatey packages from a YAML configuration file. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + "git", + @{ + name = "nodejs" + version = "18.17.0" + }, + @{ + name = "googlechrome" + params = "/nogoogle" + }, + @{ + name = "vscode" + version = "1.75.0" + params = "/silent" + } + ) + } + } + } + } + Install-ChocolateyPackages -YamlData $yamlData + + Demonstrates the PSCustomObject structure and installs the configured packages. + +.NOTES + - Requires administrator privileges to install Chocolatey packages + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Throws an exception if not running as administrator + - Returns early with warning if Chocolatey packages configuration is missing + - Supports both string and object formats for package definitions: + * String format: Simple package name for latest version + * Object format: Supports name (required), version (optional), params (optional) + - Skips empty or invalid entries in the configuration without stopping execution + - Uses Install-ChocolateyPackage function for actual installation + - Provides detailed progress reporting with color-coded status messages + - Individual installation failures do not stop the overall process + - Tracks and reports installation counts for all processed packages + - Uses parameter splatting for reliable package installation + - Displays installation status ([OK]/[FAILED]) for each package + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Bulk Installation, Configuration Processing, Package Management +#> + +Function Install-ChocolateyPackages { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [PSCustomObject]$YamlData + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "Chocolatey package installation requires administrator privileges. Please run as administrator." + } + + # Check if chocolatey dependencies exist + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { + Write-Warning "Chocolatey packages not found in YAML configuration. Skipping installation." + return + } + + if (-not (Write-ChocolateyCache)) { + Write-Warning "Failed to write Chocolatey cache." + return $false + } + + $chocolateyPackages = $YamlData.devsetup.dependencies.chocolatey.packages + Write-StatusMessage "- Installing Chocolatey packages from configuration:" -ForegroundColor Cyan + + $packageCount = 0 + + foreach ($package in $chocolateyPackages) { + if (-not $package) { continue } + + $packageCount++ + + # Normalize package to object format + if ($package -is [string]) { + $packageObj = @{ name = $package } + } else { + $packageObj = $package + } + + # Validate package name + if ([string]::IsNullOrEmpty($packageObj.name)) { + Write-Warning "Package entry #$packageCount has no name specified, skipping" + continue + } + + # Build install parameters + $installParams = @{ + PackageName = $packageObj.name + } + if ($packageObj.version) { + $installParams.Version = $packageObj.version + Write-StatusMessage "- Installing Chocolatey package: $($packageObj.name) (version: $($packageObj.version))" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline + } else { + Write-StatusMessage "- Installing Chocolatey package: $($packageObj.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline + } + + if($packageObj.params) { + $installParams.Param = $packageObj.params + } + + #$installParams.Debug = $true + + if((Install-ChocolateyPackage @installParams)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } + } + + Write-StatusMessage "- Chocolatey packages installation completed! Processed $packageCount packages." -ForegroundColor Green + write-host "" + return $true + } + catch { + Write-Error "Error installing Chocolatey packages: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 new file mode 100644 index 0000000..d8ba920 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 @@ -0,0 +1,51 @@ +BeforeAll { + . $PSScriptRoot\Read-ChocolateyCache.ps1 + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 + Mock Get-ChocolateyCacheFile { "C:\fakepath\choco.cache" } + Mock Write-Debug { } + Mock Write-Error { } + Mock Write-ChocolateyCache { return $true } +} + +Describe "Read-ChocolateyCache" { + + Context "When cache file exists and can be read" { + It "Should return the cache data as an array of strings" { + Mock Test-Path { param($Path) $true } + Mock Get-Content { @("git|2.42.0", "nodejs|20.10.0") } + $result = Read-ChocolateyCache + $result | Should -Contain "git|2.42.0" + $result | Should -Contain "nodejs|20.10.0" + } + } + + Context "When cache file does not exist and Write-ChocolateyCache succeeds" { + It "Should create the cache file and return its contents" { + Mock Test-Path { param($Path) $false } + Mock Write-ChocolateyCache { return $true } + Mock Get-Content { @("git|2.42.0") } + $result = Read-ChocolateyCache + $result | Should -Contain "git|2.42.0" + Assert-MockCalled Write-ChocolateyCache -Exactly 1 -Scope It + } + } + + Context "When cache file does not exist and Write-ChocolateyCache fails" { + It "Should throw an exception" { + Mock Test-Path { param($Path) return $false } + Mock Write-ChocolateyCache { return $false } + { Read-ChocolateyCache } | Should -Throw "Failed to create Chocolatey cache file: C:\fakepath\choco.cache" + } + } + + Context "When reading cache file fails" { + It "Should write error and return null" { + Mock Test-Path { param($Path) $true } + Mock Get-Content { throw "Read error" } + $result = Read-ChocolateyCache + $result | Should -Be $null + Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to read Chocolatey cache file" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 new file mode 100644 index 0000000..92296ba --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + Reads cached Chocolatey package information from the DevSetup cache file. + +.DESCRIPTION + This function reads cached Chocolatey package data from the DevSetup cache system. + It automatically handles cache file creation if the file doesn't exist by calling Write-ChocolateyCache, + and provides comprehensive error handling for file operations. The function returns the cached data + as an array of strings for use by other Chocolatey-related functions. + +.OUTPUTS + [System.Array] + Returns the cached data as an array of strings if successful. + Returns $null if the cache file cannot be read or parsed. + +.EXAMPLE + Read-ChocolateyCache + + Reads the Chocolatey cache data and returns it as an array of strings. + +.EXAMPLE + $chocoCache = Read-ChocolateyCache + if ($chocoCache) { + Write-Host "Found $($chocoCache.Count) cached entries" + } else { + Write-Host "No cache data available" + } + + Demonstrates reading cache data and checking for successful retrieval. + +.EXAMPLE + $cachedPackages = Read-ChocolateyCache + $gitPackage = $cachedPackages | Where-Object { $_ -like "*git*" } + + Shows reading cache data and filtering for specific package information. + +.NOTES + - Uses Get-ChocolateyCacheFile to determine the cache file location + - Automatically creates cache file if it doesn't exist using Write-ChocolateyCache + - Throws an exception if cache file creation fails + - Uses Get-Content to read the cached data as an array of strings + - Provides comprehensive error handling for file operations + - Returns $null on any error to allow calling functions to handle gracefully + - Used by other Chocolatey functions to avoid repeated system queries for performance + - Provides debug logging when cache file is not found + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Cache Management, Data Retrieval, Performance Optimization +#> + +Function Read-ChocolateyCache { + [CmdletBinding()] + Param() + + $cacheFile = Get-ChocolateyCacheFile + + if (-Not (Test-Path $cacheFile)) { + Write-Debug "Chocolatey cache file not found: $cacheFile" + if(-not (Write-ChocolateyCache)) { + throw "Failed to create Chocolatey cache file: $cacheFile" + } + } + + try { + $cacheData = Get-Content $cacheFile + return $cacheData + } + catch { + Write-Error "Failed to read Chocolatey cache file: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 new file mode 100644 index 0000000..c6de976 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 @@ -0,0 +1,25 @@ +BeforeAll { + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + Mock Write-Warning { } +} + +Describe "Test-ChocolateyInstalled" { + + Context "When Chocolatey is installed" { + It "Should return true" { + Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } + $result = Test-ChocolateyInstalled + $result | Should -Be $true + Assert-MockCalled Write-Warning -Exactly 0 -Scope It + } + } + + Context "When Chocolatey is not installed" { + It "Should return false and write a warning" { + Mock Get-Command { $null } + $result = Test-ChocolateyInstalled + $result | Should -Be $false + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 new file mode 100644 index 0000000..f599417 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 @@ -0,0 +1,65 @@ +<# +.SYNOPSIS + Tests whether Chocolatey package manager is installed on the system. + +.DESCRIPTION + This function checks if Chocolatey is installed and available by attempting to locate the 'choco' command. + It uses Get-Command to verify that the Chocolatey executable is accessible in the system PATH. + The function provides a warning message when Chocolatey is not found and returns a boolean result + indicating the installation status. + +.OUTPUTS + [System.Boolean] + Returns $true if Chocolatey is installed and the 'choco' command is available. + Returns $false if Chocolatey is not installed or the 'choco' command cannot be found. + +.EXAMPLE + Test-ChocolateyInstalled + + Checks if Chocolatey is installed on the system. + +.EXAMPLE + if (Test-ChocolateyInstalled) { + Write-Host "Chocolatey is available" + # Proceed with Chocolatey operations + } else { + Write-Host "Chocolatey is not installed" + # Handle missing Chocolatey + } + + Demonstrates conditional logic based on Chocolatey availability. + +.EXAMPLE + $hasChocolatey = Test-ChocolateyInstalled + if (-not $hasChocolatey) { + Install-Chocolatey + } + + Shows using the function result to trigger Chocolatey installation if needed. + +.NOTES + - Uses Get-Command with -ErrorAction SilentlyContinue to suppress errors when 'choco' is not found + - Provides a descriptive warning message when Chocolatey is not installed + - Does not require administrator privileges to check installation status + - Checks for command availability rather than file system presence for more reliable detection + - Used as a prerequisite check by other Chocolatey-related functions in the DevSetup module + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Installation Verification, Prerequisites Check, System Detection +#> + +Function Test-ChocolateyInstalled { + [CmdletBinding()] + Param() + + if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Warning "Chocolatey is not installed. Cannot check for Chocolatey packages." + return $false + } + return $true +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 new file mode 100644 index 0000000..37daa35 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 @@ -0,0 +1,75 @@ +BeforeAll { + . $PSScriptRoot\Test-ChocolateyPackageInstalled.ps1 + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + . $PSScriptRoot\Read-ChocolateyCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { } + Mock Write-Warning { } +} + +Describe "Test-ChocolateyPackageInstalled" { + + Context "When Chocolatey is not installed" { + It "Should return NotInstalled and write a warning" { + Mock Test-ChocolateyInstalled { $false } + $result = Test-ChocolateyPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::NotInstalled) + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } + } + } + + Context "When package is not in cache" { + It "Should return NotInstalled" { + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { @("nodejs|20.10.0") } + $result = Test-ChocolateyPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::NotInstalled) + } + } + + Context "When package is in cache (any version)" { + It "Should return Installed, GlobalVersionMet, MinimumVersionMet, RequiredVersionMet" { + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { @("git|2.42.0") } + $result = Test-ChocolateyPackageInstalled -PackageName "git" + $result.HasFlag([InstalledState]::Installed) | Should -Be $true + $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true + $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $true + $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $true + } + } + + Context "When package is in cache but version does not match" { + It "Should not set MinimumVersionMet or RequiredVersionMet" { + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { @("git|2.42.0") } + $result = Test-ChocolateyPackageInstalled -PackageName "git" -Version "2.41.0" + $result.HasFlag([InstalledState]::Installed) | Should -Be $true + $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true + $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $false + $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $false + } + } + + Context "When package is in cache and version matches" { + It "Should set all flags" { + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { @("git|2.42.0") } + $result = Test-ChocolateyPackageInstalled -PackageName "git" -Version "2.42.0" + $result.HasFlag([InstalledState]::Installed) | Should -Be $true + $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true + $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $true + $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $true + } + } + + Context "When Read-ChocolateyCache throws an error" { + It "Should return NotInstalled" { + Mock Test-ChocolateyInstalled { $true } + Mock Read-ChocolateyCache { throw "cache error" } + $result = Test-ChocolateyPackageInstalled -PackageName "git" + $result | Should -Be ([InstalledState]::NotInstalled) + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 new file mode 100644 index 0000000..b61a5f0 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 @@ -0,0 +1,118 @@ +<# +.SYNOPSIS + Tests whether a Chocolatey package is installed with optional version validation. + +.DESCRIPTION + This function checks if a Chocolatey package is installed on the system and optionally validates + a specific version requirement. It uses 'choco list' with exact matching to find installed packages + and examines the returned package information to determine installation status and version details. + The function supports multiple parameter sets to check different combinations of package existence + and version matching. + +.PARAMETER PackageName + The name of the Chocolatey package to check. + This parameter is mandatory for all parameter sets and must be a valid, non-empty string representing a Chocolatey package name. + +.PARAMETER Version + The specific version of the package to validate. + Mandatory parameter for PackageVersionCheck parameter set. + When specified, the function checks if the installed package matches this exact version. + +.OUTPUTS + [System.Boolean] + Returns $true if the package meets all specified criteria (exists, version matches if specified). + Returns $false if the package is not installed, doesn't meet the specified criteria, or an error occurs. + +.EXAMPLE + Test-ChocolateyPackageInstalled -PackageName "git" + + Checks if the git package is installed (any version). + +.EXAMPLE + Test-ChocolateyPackageInstalled -PackageName "nodejs" -Version "18.17.0" + + Checks if nodejs package version 18.17.0 is installed. + +.EXAMPLE + $isInstalled = Test-ChocolateyPackageInstalled -PackageName "vscode" + if ($isInstalled) { + Write-Host "Visual Studio Code is installed" + } else { + Write-Host "Visual Studio Code is not installed" + } + + Demonstrates capturing the return value to check installation status. + +.NOTES + - Requires Chocolatey to be installed on the system + - Uses Test-ChocolateyInstalled to verify Chocolatey availability before proceeding + - Returns $false immediately if Chocolatey is not installed + - Uses 'choco list' with -exact and -r flags for precise package matching and machine-readable output + - Parses package information in "packagename|version" format returned by Chocolatey + - Suppresses command output using '*>$null' to avoid console clutter + - Parameter sets determine validation criteria: + * PackageCheck: Only checks if package exists (PackageName parameter only) + * PackageVersionCheck: Checks existence and exact version match (PackageName and Version parameters) + - Includes comprehensive try-catch error handling with descriptive error messages + - Provides detailed debug logging for troubleshooting installation issues + - Uses ValidateNotNullOrEmpty attribute to ensure parameters contain valid values + - Returns early if package is not found to avoid unnecessary processing + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Package Detection, Installation Verification, Version Validation +#> + +Function Test-ChocolateyPackageInstalled { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] + [ValidateNotNullOrEmpty()] + [string]$PackageName, + + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] + [ValidateNotNullOrEmpty()] + [string]$Version + ) + + if (-not (Test-ChocolateyInstalled)) { + Write-Warning "Chocolatey is not installed. Cannot check for packages." + return [InstalledState]::NotInstalled + } + + [InstalledState]$installedState = [InstalledState]::NotInstalled + + try { + $package = Read-ChocolateyCache + if ($package) { + # choco list can return multiple lines, so find the exact match + $exactMatch = $package | Where-Object { ($_ -split '\|')[0] -eq $PackageName } + if ($exactMatch) { + $installedState += [InstalledState]::Installed + $installedState += [InstalledState]::GlobalVersionMet + + $parts = $exactMatch -split '\|' + $installedVersion = if ($parts.Length -gt 1) { $parts[1] } else { $null } + + # Now compare with the requested version + if ($PSBoundParameters.ContainsKey('Version')) { + if([Version]$installedVersion -eq [Version]$Version) { + $installedState += [InstalledState]::MinimumVersionMet + $installedState += [InstalledState]::RequiredVersionMet + } + } else { + $installedState += [InstalledState]::MinimumVersionMet + $installedState += [InstalledState]::RequiredVersionMet + } + } + } + return $installedState + } catch { + return [InstalledState]::NotInstalled + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 new file mode 100644 index 0000000..d4df488 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 @@ -0,0 +1,50 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Write-Debug { } + Mock Write-Error { } + Mock Invoke-Expression { } +} + +Describe "Uninstall-ChocolateyPackage" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } + } + } + + Context "When uninstallation succeeds" { + It "Should return true and write debug" { + Mock Test-RunningAsAdmin { $true } + $global:LASTEXITCODE = 0 + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $true + Assert-MockCalled Write-Debug -Scope It -ParameterFilter { $Message -match "uninstalled successfully" } + } + } + + Context "When uninstallation fails (non-zero exit code)" { + It "Should write error and return false" { + Mock Test-RunningAsAdmin { $true } + $global:LASTEXITCODE = 1 + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to uninstall" } + } + } + + Context "When an exception occurs during uninstall" { + It "Should write error and return false" { + Mock Test-RunningAsAdmin { $true } + Mock Invoke-Expression { throw "Unexpected error" } + $result = Uninstall-ChocolateyPackage -PackageName "git" + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error uninstalling Chocolatey package" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 new file mode 100644 index 0000000..a33ef47 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 @@ -0,0 +1,97 @@ +<# +.SYNOPSIS + Uninstalls a Chocolatey package and its dependencies from the system. + +.DESCRIPTION + This function removes a Chocolatey package from the system using the 'choco uninstall' command. + It validates administrator privileges before proceeding, handles package dependencies by uninstalling + them first, and removes all versions of the specified package including metapackages. The function + provides comprehensive error handling and uses exit codes to verify successful uninstallation. + +.PARAMETER PackageName + The name of the Chocolatey package to uninstall. + This parameter is mandatory and must be a valid, non-empty string representing an installed Chocolatey package name. + +.OUTPUTS + [System.Boolean] + Returns $true if the package and all dependencies were successfully uninstalled. + Returns $false if the uninstallation failed or insufficient privileges. + +.EXAMPLE + Uninstall-ChocolateyPackage -PackageName "git" + + Uninstalls the git package and any dependent packages from the system. + +.EXAMPLE + $result = Uninstall-ChocolateyPackage -PackageName "nodejs" + if ($result) { + Write-Host "Node.js and dependencies removed successfully" + } else { + Write-Host "Failed to remove Node.js" + } + + Demonstrates capturing the return value to check uninstallation success. + +.EXAMPLE + @("git", "nodejs", "vscode") | ForEach-Object { + Uninstall-ChocolateyPackage -PackageName $_ + } + + Shows bulk uninstallation of multiple packages with dependency handling. + +.NOTES + - Requires administrator privileges to uninstall packages + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Throws an exception if not running as administrator + - Handles package dependencies by uninstalling them first using Get-ChocolateyPackageDependencies + - Uses recursive calls to uninstall dependency packages before the main package + - Automatically handles metapackages (packages ending with .install) + - Uses 'choco uninstall' with -y flag for automatic confirmation + - Uses --all-versions flag to remove all installed versions of the package + - Uses $LASTEXITCODE to verify command execution success + - Suppresses command output using Out-Null to avoid console clutter + - Includes comprehensive try-catch error handling with descriptive error messages + - Provides detailed debug logging for troubleshooting uninstallation issues + - Checks for and removes associated .install metapackages after main package removal + +.LINK + +.COMPONENT + DevSetup,Providers.Chocolatey + +.FUNCTIONALITY + Package Management, Software Removal, System Cleanup, Dependency Management +#> + +Function Uninstall-ChocolateyPackage { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [String] $PackageName + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "Chocolatey package uninstallation requires administrator privileges. Please run as administrator." + } + + Write-Debug "Uninstalling Chocolatey package: $PackageName" + + # Uninstall the package + Invoke-Expression "& choco uninstall -y $PackageName --remove-dependencies --all-versions --ignore-package-exit-codes" | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Debug "Chocolatey package '$PackageName' uninstalled successfully." + return $true + } else { + Write-Error "Failed to uninstall Chocolatey package '$PackageName'." + return $false + } + } + catch { + Write-Error "Error uninstalling Chocolatey package: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 new file mode 100644 index 0000000..accff48 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 @@ -0,0 +1,147 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-ChocolateyPackages.ps1 + . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\Read-ChocolateyCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Write-ChocolateyCache { $true } + Mock Write-Warning { } + Mock Write-StatusMessage { } + Mock Uninstall-ChocolateyPackage { $true } + Mock Write-Error { } + Mock Write-Host { } +} + +Describe "Uninstall-ChocolateyPackages" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Uninstall-ChocolateyPackages -YamlData @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + $result | Should -Be $false + } + } + + Context "When Chocolatey packages config is missing" { + It "Should write warning and return" { + $yamlData = @{ devsetup = @{ dependencies = @{ } } } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $null + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not found" } + } + } + + Context "When Write-ChocolateyCache fails" { + It "Should write warning and return false" { + Mock Write-ChocolateyCache { $false } + $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $false + Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } + } + } + + Context "When all packages uninstall successfully (string format)" { + It "Should process all packages and return true" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git", "nodejs") + } + } + } + } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It + #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "uninstallation completed" } + } + } + + Context "When all packages uninstall successfully (object format)" { + It "Should process all packages and return true" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + @{ name = "git"; version = "2.42.0" }, + @{ name = "nodejs"; params = "/silent" } + ) + } + } + } + } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It + } + } + + Context "When some packages fail to uninstall" { + It "Should continue processing and return true" { + $callCount = 0 + Mock Uninstall-ChocolateyPackage -MockWith { + param($PackageName) + $callCount++ + if ($callCount -eq 1) { $true } else { $false } + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git", "nodejs") + } + } + } + } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It + #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -eq "[FAILED]" } + } + } + + Context "When package entry is empty or missing name" { + It "Should skip invalid entries and continue" { + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @( + $null, + @{ version = "1.0.0" }, + "git" + ) + } + } + } + } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $true + Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "no name specified" } + } + } + + Context "When an exception occurs during uninstallation" { + It "Should write error and return false" { + Mock Uninstall-ChocolateyPackage { throw "Unexpected error" } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git") + } + } + } + } + $result = Uninstall-ChocolateyPackages -YamlData $yamlData + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error uninstalling Chocolatey packages" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 new file mode 100644 index 0000000..35943c1 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 @@ -0,0 +1,150 @@ +<# +.SYNOPSIS + Uninstalls multiple Chocolatey packages from the system based on YAML configuration. + +.DESCRIPTION + This function removes multiple Chocolatey packages specified in a DevSetup YAML configuration. + It validates administrator privileges, parses the configuration for Chocolatey package definitions, + and systematically uninstalls each package. The function supports both simple string format and + complex object format for package specifications, handles version constraints, and provides + comprehensive progress reporting during the uninstallation process. + +.PARAMETER YamlData + The parsed YAML configuration data containing Chocolatey package definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.chocolatey.packages containing an array of package specifications. + +.OUTPUTS + [System.Boolean] + Returns $true if all packages are successfully processed (even if some individual uninstalls fail). + Returns $false if the operation encounters critical errors or cannot proceed. + +.EXAMPLE + $config = Read-ConfigurationFile -Path "environment.yaml" + Uninstall-ChocolateyPackages -YamlData $config + + Uninstalls all Chocolatey packages defined in the environment.yaml configuration. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @("git", "nodejs", "vscode") + } + } + } + } + Uninstall-ChocolateyPackages -YamlData $yamlData + + Demonstrates uninstalling packages using a programmatically created configuration. + +.EXAMPLE + if (Uninstall-ChocolateyPackages -YamlData $config) { + Write-Host "All Chocolatey packages processed successfully" + } else { + Write-Host "Chocolatey uninstallation encountered errors" + } + + Shows checking the return value to verify uninstallation completion. + +.NOTES + - Requires administrator privileges to uninstall Chocolatey packages + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Throws an exception if not running as administrator + - Updates Chocolatey cache using Write-ChocolateyCache before uninstallation + - Skips uninstallation gracefully if no Chocolatey packages are found in configuration + - Supports two package specification formats: + * Simple string: "packagename" + * Complex object: @{ name = "packagename"; version = "1.0.0" } + - Validates package names and skips entries with missing names + - Uses Uninstall-ChocolateyPackage for individual package removal + - Provides detailed progress reporting with package counts and status indicators + - Uses color-coded console output: Cyan for progress, Gray for package status, Green/Red for results + - Continues processing remaining packages even if individual uninstalls fail + - Returns $true for overall success even with individual package failures + - Includes comprehensive try-catch error handling with descriptive error messages + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Package Management, Batch Uninstallation, Configuration Processing, System Cleanup +#> + +Function Uninstall-ChocolateyPackages { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [PSCustomObject]$YamlData + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "Chocolatey package uninstallation requires administrator privileges. Please run as administrator." + } + + # Check if chocolatey dependencies exist + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { + Write-Warning "Chocolatey packages not found in YAML configuration. Skipping uninstallation." + return + } + + if (-not (Write-ChocolateyCache)) { + Write-Warning "Failed to write Chocolatey cache." + return $false + } + + $chocolateyPackages = $YamlData.devsetup.dependencies.chocolatey.packages + Write-StatusMessage "- Uninstalling Chocolatey packages from configuration:" -ForegroundColor Cyan + + $packageCount = 0 + + foreach ($package in $chocolateyPackages) { + if (-not $package) { continue } + + $packageCount++ + + # Normalize package to object format + if ($package -is [string]) { + $packageObj = @{ name = $package } + } else { + $packageObj = $package + } + + # Validate package name + if ([string]::IsNullOrEmpty($packageObj.name)) { + Write-Warning "Package entry #$packageCount has no name specified, skipping" + continue + } + + # Build install parameters + $installParams = @{ + PackageName = $packageObj.name + } + if ($packageObj.version) { + Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: $($packageObj.version))" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline + } else { + Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline + } + + if((Uninstall-ChocolateyPackage @installParams)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } + } + + Write-StatusMessage "- Chocolatey packages uninstallation completed! Processed $packageCount packages." -ForegroundColor Green + write-host "" + return $true + } + catch { + Write-Error "Error uninstalling Chocolatey packages: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 new file mode 100644 index 0000000..7406561 --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 @@ -0,0 +1,56 @@ +BeforeAll { + . $PSScriptRoot\Write-ChocolateyCache.ps1 + . $PSScriptRoot\Test-ChocolateyInstalled.ps1 + . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 + Mock Write-Error { } + Mock Write-Debug { } +} + +Describe "Write-ChocolateyCache" { + + Context "When Chocolatey is not installed" { + It "Should return false and write error" { + Mock Test-ChocolateyInstalled { return $false } + Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } + $result = Write-ChocolateyCache + $result | Should -Be $false + } + } + + Context "When cache file is written successfully" { + It "Should return true and write debug" { + Mock Test-ChocolateyInstalled { return $true } + Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } + Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } + $script:setContentCalled = $false + Mock Set-Content -MockWith { + param($Path, $Value, $Force) + $script:setContentCalled = $true + } + $result = Write-ChocolateyCache + $result | Should -Be $true + $script:setContentCalled | Should -Be $true + } + } + + Context "When writing cache file fails" { + It "Should return false and write error" { + Mock Test-ChocolateyInstalled { return $true } + Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } + Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } + Mock Set-Content { throw "Failed to write file" } + $result = Write-ChocolateyCache + $result | Should -Be $false + } + } + + Context "When choco command throws an exception" { + It "Should return false and write error" { + Mock Test-ChocolateyInstalled { return $true } + Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } + Mock Invoke-Expression { throw "choco failed" } + $result = Write-ChocolateyCache + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 new file mode 100644 index 0000000..2058c2b --- /dev/null +++ b/DevSetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS + Writes current Chocolatey package information to the DevSetup cache file. + +.DESCRIPTION + This function exports the current Chocolatey package installation data and writes it to the DevSetup + cache file for performance optimization and offline reference. It validates Chocolatey installation, + executes 'choco list -r' to generate machine-readable package data, and saves the output to the + cache file. The function provides comprehensive error handling and validation throughout the process. + +.OUTPUTS + [System.Boolean] + Returns $true if the cache file is successfully written. + Returns $false if Chocolatey is not installed or the write operation fails. + +.EXAMPLE + Write-ChocolateyCache + + Exports current Chocolatey packages and writes them to the cache file. + +.EXAMPLE + if (Write-ChocolateyCache) { + Write-Host "Chocolatey cache updated successfully" + } else { + Write-Host "Failed to update Chocolatey cache" + } + + Demonstrates checking the return value to verify cache update success. + +.EXAMPLE + $cacheUpdated = Write-ChocolateyCache + if ($cacheUpdated) { + $cachedData = Read-ChocolateyCache + } + + Shows writing cache data and then reading it back for use. + +.NOTES + - Requires Chocolatey to be installed on the system + - Uses Test-ChocolateyInstalled to validate Chocolatey availability + - Returns $false immediately if Chocolatey is not available + - Executes 'choco list -r' to generate machine-readable package data with pipe-delimited format + - Uses Get-ChocolateyCacheFile to determine the cache file location + - Overwrites existing cache file using -Force flag + - Provides debug logging for successful cache operations + - Includes comprehensive try-catch error handling for command execution and file operations + - Uses Set-Content for reliable file writing with proper encoding + +.LINK + +.COMPONENT + DevSetup.Providers.Chocolatey + +.FUNCTIONALITY + Cache Management, Data Serialization, Performance Optimization +#> + +Function Write-ChocolateyCache { + [CmdletBinding()] + Param() + + $cacheFile = Get-ChocolateyCacheFile + + if(-not (Test-ChocolateyInstalled)) { + Write-Error "Chocolatey is not installed. Cannot write cache file." + return $false + } + + try { + #$chocolatelyPackages = @{} + #choco list -r | foreach-object { + # $package = $_ -split '\|' + # if($package.Count -eq 2) { + # $chocolatelyPackages[$package[0]] = @{ + # Name = $package[0] + # Version = $package[1] + # } + # } + #} + Invoke-Expression "& choco list -r" | Set-Content $cacheFile -Force + Write-Debug "Chocolatey cache written successfully to: $cacheFile" + return $true + } + catch { + Write-Error "Failed to write Chocolatey cache file: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 b/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 new file mode 100644 index 0000000..f5b7300 --- /dev/null +++ b/DevSetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 @@ -0,0 +1,135 @@ +BeforeAll { + . $PSScriptRoot\Install-CoreDependencies.ps1 + . $PSScriptRoot\Install-Nuget.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Powershell\Install-PowerShellModule.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-Chocolatey.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Scoop\Install-Scoop.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } + Mock Write-Host { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Test-RunningAsAdmin { return $true } +} + +Describe "Install-CoreDependencies" { + + Context "When NuGet installation fails" { + It "Should return false and write error" { + Mock Install-NuGet { return $false } + Mock Test-OperatingSystem { param($os) return $false } + $result = Install-CoreDependencies + $result | Should -Be $false + } + } + + Context "When manifest is missing or has no required modules" { + It "Should return true and write warning" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return $null } + Mock Test-OperatingSystem { param($os) return $false } + $result = Install-CoreDependencies + $result | Should -Be $true + + Mock Get-DevSetupManifest { return @{ RequiredModules = $null } } + $result = Install-CoreDependencies + $result | Should -Be $true + } + } + + Context "When required module installation fails" { + It "Should return false and write error" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } + Mock Test-OperatingSystem { param($os) return $false } + $script:callCount = 0 + Mock Install-PowerShellModule -MockWith { + param($ModuleName, $Force, $AllowClobber, $Scope) + $script:callCount++ + if ($script:callCount -eq 1) { return $true } + else { return $false } + } + $result = Install-CoreDependencies + $result | Should -Be $false + } + } + + Context "When required modules include empty names" { + It "Should skip empty module names and return true" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", $null, "PSReadLine") } } + Mock Install-PowerShellModule { return $true } + Mock Test-OperatingSystem { param($os) return $false } + $result = Install-CoreDependencies + $result | Should -Be $true + } + } + + Context "When all core dependencies install successfully on Windows" { + It "Should install everything and return true" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } + Mock Install-PowerShellModule { return $true } + Mock Install-Chocolatey { return $true } + Mock Install-ChocolateyPackage { return $true } + Mock Install-Scoop { return $true } + Mock Test-OperatingSystem { param($os) if ($os -eq 'Windows') { return $true } else { return $false } } + $result = Install-CoreDependencies + $result | Should -Be $true + } + } + + Context "When Chocolatey installation fails on Windows" { + It "Should return false and write error" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } + Mock Install-PowerShellModule { return $true } + Mock Install-Chocolatey { return $false } + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + $result = Install-CoreDependencies + $result | Should -Be $false + } + } + + Context "When Git installation fails on Windows" { + It "Should return false and write error" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } + Mock Install-PowerShellModule { return $true } + Mock Install-Chocolatey { return $true } + Mock Install-ChocolateyPackage { return $false } + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + $result = Install-CoreDependencies + $result | Should -Be $false + } + } + + Context "When Scoop installation fails on Windows" { + It "Should return false and write error" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } + Mock Install-PowerShellModule { return $true } + Mock Install-Chocolatey { return $true } + Mock Install-ChocolateyPackage { return $true } + Mock Install-Scoop { return $false } + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + $result = Install-CoreDependencies + $result | Should -Be $false + } + } + + Context "When all core dependencies install successfully on non-Windows" { + It "Should skip Windows-only installs and return true" { + Mock Install-NuGet { return $true } + Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } + Mock Install-PowerShellModule { return $true } + Mock Test-OperatingSystem { param($os) return $false } + $result = Install-CoreDependencies + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 b/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 new file mode 100644 index 0000000..e03f285 --- /dev/null +++ b/DevSetup/Private/Providers/Core/Install-CoreDependencies.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS + Installs core dependencies required for the DevSetup module to function properly. + +.DESCRIPTION + This function installs essential system dependencies and package managers required for DevSetup operations. + It sequentially installs NuGet PackageProvider, required PowerShell modules from the DevSetup manifest, + and platform-specific tools. On Windows, it also installs Chocolatey, Git, and Scoop. The function + validates each installation step and fails fast if any critical component cannot be installed. It also + refreshes the PATH environment variable to ensure newly installed tools are immediately available. + +.OUTPUTS + [System.Boolean] + Returns $true if all core dependencies are successfully installed. + Returns $false if any critical installation fails. + +.EXAMPLE + Install-CoreDependencies + + Installs all core dependencies required for DevSetup functionality. + +.EXAMPLE + if (Install-CoreDependencies) { + Write-Host "DevSetup is ready for use" + # Proceed with environment setup + } else { + Write-Host "Failed to install core dependencies" + # Handle installation failure + } + + Demonstrates conditional logic based on installation success. + +.EXAMPLE + $coreReady = Install-CoreDependencies + if ($coreReady) { + # Continue with package installations + Install-ChocolateyPackages -YamlData $config + } + + Shows using the function result to proceed with subsequent operations. + +.NOTES + - Cross-platform support with platform detection using $IsWindows, $IsLinux, $IsMacOS + - Sets up platform variables if not defined ($IsWindows = $true, others = $false by default) + - Installs dependencies in a specific order to ensure proper functionality: + 1. NuGet PackageProvider (all platforms) + 2. Required PowerShell modules from DevSetup manifest (all platforms) + 3. Windows-only components: + - Chocolatey package manager + - Git version control system via Chocolatey + - Scoop package manager + - Uses fail-fast approach - stops immediately if any critical component fails + - Installs PowerShell modules with -Force, -AllowClobber, and CurrentUser scope + - Refreshes PATH environment variable after Git installation for immediate availability + - Gets required modules list from Get-DevSetupManifest + - Provides color-coded console output for installation progress + - Skips empty module names in the manifest gracefully + - Returns $true even if no required modules are found (considered success) + - Windows-specific installations are conditionally executed based on platform detection + +.LINK + +.COMPONENT + DevSetup.Providers.Core + +.FUNCTIONALITY + System Setup, Dependency Management, Package Manager Installation +#> + +Function Install-CoreDependencies { + [CmdletBinding()] + Param() + + # Install NuGet PackageProvider + if (-not (Install-NuGet)) { + Write-Error "Failed to install NuGet PackageProvider" + return $false + } + + # Get required modules from DevSetup manifest + $manifest = Get-DevSetupManifest + if (-not $manifest -or -not $manifest.RequiredModules) { + Write-Warning "No required modules found in DevSetup manifest" + return $true + } + + # Install each required PowerShell module + foreach ($moduleName in $manifest.RequiredModules) { + if (-not $moduleName -or [string]::IsNullOrEmpty($moduleName)) { + Write-Warning "Skipping empty module name" + continue + } + + Write-StatusMessage "- Installing powershell module: $moduleName" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if (-not (Install-PowerShellModule -ModuleName $moduleName -Force -AllowClobber -Scope 'CurrentUser')) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + Write-Error "Failed to install required PowerShell module: $moduleName" + return $false + } + Write-StatusMessage "[OK]" -ForegroundColor Green + } + + if ((Test-OperatingSystem -Windows)) { + # Install Chocolatey first + if (-not (Install-Chocolatey)) { + Write-Error "Cannot proceed without Chocolatey" + return $false + } + + # Install Git using Chocolatey + Write-StatusMessage "- Installing Git package via Chocolatey" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if (-not (Install-ChocolateyPackage -PackageName "git" -Version 2.50.1)) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + Write-Error "Failed to install Git package" + return $false + } else { + Write-StatusMessage "[OK]" -ForegroundColor Green + } + + $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "User") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "Machine") + + # Install Scoop PackageProvider + if (-not (Install-Scoop)) { + Write-Error "Failed to install Scoop PackageProvider" + return $false + } + } else { + Write-Warning "Skipping Windows-only installations on non-Windows platform" + } + + return $true +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Core/Install-GitRepository.ps1 b/DevSetup/Private/Providers/Core/Install-GitRepository.ps1 new file mode 100644 index 0000000..19e68e1 --- /dev/null +++ b/DevSetup/Private/Providers/Core/Install-GitRepository.ps1 @@ -0,0 +1,173 @@ +<# +.SYNOPSIS + Clones or updates a Git repository to a specified local destination. + +.DESCRIPTION + This function clones a Git repository from a remote URL to a local destination path. It includes + intelligent Git detection, handles existing repositories with update or replace options, supports + branch specification, and provides comprehensive error handling. The function automatically detects + Git installation in both PATH and common installation locations. + +.PARAMETER RepositoryUrl + The URL of the Git repository to clone. + This parameter is mandatory and must be a valid, non-empty string representing a Git repository URL. + +.PARAMETER DestinationPath + The local path where the repository should be cloned. + This parameter is mandatory and must be a valid, non-empty string representing a local directory path. + +.PARAMETER Branch + The specific branch to clone from the repository. + Optional parameter that specifies which branch to clone. If not provided, the default branch is used. + +.PARAMETER UpdateExisting + Switch parameter that controls behavior when the destination path already exists. + When specified, performs a git pull to update the existing repository instead of removing and re-cloning. + +.OUTPUTS + [System.Boolean] + Returns $true if the repository was successfully cloned or updated. + Returns $false if the operation failed or Git is not available. + +.EXAMPLE + Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" + + Clones a repository to the specified path using the default branch. + +.EXAMPLE + Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" -Branch "develop" + + Clones a specific branch of the repository. + +.EXAMPLE + Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" -UpdateExisting + + Updates an existing repository instead of removing and re-cloning. + +.EXAMPLE + $success = Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" + if ($success) { + Write-Host "Repository ready for use" + } else { + Write-Host "Failed to clone repository" + } + + Demonstrates capturing the return value to check operation success. + +.NOTES + - Requires Git to be installed on the system + - Automatically detects Git in PATH using Get-Command + - Falls back to common Git installation path: "C:\Program Files\Git\cmd\git.exe" + - Uses $LASTEXITCODE to verify Git command execution success + - Handles existing destinations in two ways: + * UpdateExisting: Performs git pull to update existing repository + * Default: Removes existing directory and performs fresh clone + - Uses Push-Location/Pop-Location for safe directory operations during updates + - Provides color-coded console output for different operation types + - Includes comprehensive try-catch error handling + - Uses parameter splatting for reliable Git command execution + +.LINK + +.COMPONENT + DevSetup.Providers.Core + +.FUNCTIONALITY + Version Control, Repository Management, Git Operations +#> + +Function Install-GitRepository { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$RepositoryUrl, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DestinationPath, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Branch, + + [Parameter(Mandatory = $false)] + [switch]$UpdateExisting = $false + ) + + # Check if Git is installed + $gitCommand = Get-Command git -ErrorAction SilentlyContinue + if (-not $gitCommand) { + # Check common Git installation path + $gitPath = "C:\Program Files\Git\cmd\git.exe" + if (Test-Path $gitPath) { + Write-Host "Using Git from: $gitPath" -ForegroundColor Gray + # Use the full path for git commands + $gitExecutable = $gitPath + } else { + Write-Error "Git is not installed or not found in PATH. Please install Git and try again." + return $false + } + } else { + $gitExecutable = "git" + Write-Host "Git found in PATH" -ForegroundColor Gray + } + + try { + # Check if destination already exists + if (Test-Path -Path $DestinationPath) { + if ($UpdateExisting) { + Write-Host "Updating existing repository at $DestinationPath" -ForegroundColor Yellow + + # Change to the repository directory and pull updates + Push-Location $DestinationPath + try { + & $gitExecutable pull + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to update repository at $DestinationPath" + return $false + } + Write-Host "Repository updated successfully" -ForegroundColor Green + return $true + } + finally { + Pop-Location + } + } else { + Write-Host "Removing existing directory to perform fresh clone: $DestinationPath" -ForegroundColor Yellow + Remove-Item -Path $DestinationPath -Recurse -Force + } + } + + # Build git clone command + $gitArgs = @("clone") + + # Add branch parameter only if specified + if (-not [string]::IsNullOrWhiteSpace($Branch)) { + $gitArgs += "--branch" + $gitArgs += $Branch + Write-Host "Cloning repository from $RepositoryUrl (branch: $Branch) to $DestinationPath" -ForegroundColor Cyan + } else { + Write-Host "Cloning repository from $RepositoryUrl (default branch) to $DestinationPath" -ForegroundColor Cyan + } + + # Add repository URL and destination path + $gitArgs += $RepositoryUrl + $gitArgs += $DestinationPath + + # Execute git clone command + & $gitExecutable @gitArgs + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to clone repository from $RepositoryUrl to $DestinationPath" + return $false + } + + Write-Host "Repository cloned successfully to $DestinationPath" -ForegroundColor Green + return $true + } + catch { + Write-Error "Error cloning repository: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Core/Install-Nuget.Tests.ps1 b/DevSetup/Private/Providers/Core/Install-Nuget.Tests.ps1 new file mode 100644 index 0000000..597915d --- /dev/null +++ b/DevSetup/Private/Providers/Core/Install-Nuget.Tests.ps1 @@ -0,0 +1,114 @@ +BeforeAll { + . $PSScriptRoot\Install-Nuget.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-Host { } + Mock Write-Error { } + Mock Write-StatusMessage { } +} + +Describe "Install-Nuget" { + + Context "When not running on Windows" { + It "Should skip installation and return true" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $false } else { return $true } } + $result = Install-Nuget + $result | Should -Be $true + } + } + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $false } + $result = Install-Nuget + $result | Should -Be $false + } + } + + Context "When NuGet PackageProvider is already installed" { + It "Should return true and not install again" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $true } + Mock Get-PackageProvider { + [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } + } + $result = Install-Nuget + $result | Should -Be $true + } + } + + Context "When NuGet PackageProvider is not installed and installation succeeds" { + It "Should install and return true" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $true } + $script:installCalled = $false + $script:providerCallCount = 0 + Mock Get-PackageProvider -MockWith { + param($Name) + $script:providerCallCount++ + if ($script:providerCallCount -eq 1) { return $null } + else { return [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } + } + Mock Install-PackageProvider -MockWith { + param($Name, $MinimumVersion, $Force, $Scope) + $script:installCalled = $true + } + $result = Install-Nuget + $result | Should -Be $true + $script:installCalled | Should -Be $true + } + } + + Context "When NuGet PackageProvider installation fails" { + It "Should return false and write error" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $true } + $script:providerCallCount = 0 + Mock Get-PackageProvider -MockWith { + param($Name) + $script:providerCallCount++ + return $null + } + Mock Install-PackageProvider { } + $result = Install-Nuget + $result | Should -Be $false + } + } + + Context "When NuGet CLI is available and version is detected" { + It "Should check CLI version and return true" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $true } + Mock Get-PackageProvider { [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } + Mock Get-Command { [PSCustomObject]@{ Name = "nuget" } } + Mock Invoke-Expression { "NuGet Version: 6.0.0" } + Mock Select-String { [PSCustomObject]@{ Line = "NuGet Version: 6.0.0" } } + Mock ForEach-Object { "6.0.0" } + $result = Install-Nuget + $result | Should -Be $true + } + } + + Context "When NuGet CLI is available but version detection fails" { + It "Should still return true" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { return $true } + Mock Get-PackageProvider { [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } + Mock Get-Command { [PSCustomObject]@{ Name = "nuget" } } + Mock Invoke-Expression { throw "CLI error" } + $result = Install-Nuget + $result | Should -Be $true + } + } + + Context "When an unexpected error occurs" { + It "Should return false and write error" { + Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } + Mock Test-RunningAsAdmin { throw "Unexpected error" } + $result = Install-Nuget + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Core/Install-Nuget.ps1 b/DevSetup/Private/Providers/Core/Install-Nuget.ps1 new file mode 100644 index 0000000..e2e52af --- /dev/null +++ b/DevSetup/Private/Providers/Core/Install-Nuget.ps1 @@ -0,0 +1,118 @@ +<# +.SYNOPSIS + Installs the NuGet PackageProvider for PowerShell package management. + +.DESCRIPTION + This function installs the NuGet PackageProvider which is required for PowerShell package management + operations. It validates platform compatibility (Windows-only), administrator privileges, and existing + installations before proceeding. The function also detects and reports on the availability of the + NuGet CLI tool if present on the system. + +.OUTPUTS + [System.Boolean] + Returns $true if NuGet PackageProvider is successfully installed or already exists. + Returns $false if the installation fails or system requirements are not met. + +.EXAMPLE + Install-Nuget + + Installs the NuGet PackageProvider on the current system. + +.EXAMPLE + if (Install-Nuget) { + Write-Host "NuGet PackageProvider is ready for use" + # Proceed with PowerShell module installations + } else { + Write-Host "Failed to install NuGet PackageProvider" + # Handle installation failure + } + + Demonstrates conditional logic based on installation success. + +.EXAMPLE + $nugetReady = Install-Nuget + if ($nugetReady) { + Install-Module -Name SomeModule -Force + } + + Shows using the function result to proceed with module operations. + +.NOTES + - Requires administrator privileges on Windows systems + - Uses Test-RunningAsAdmin to validate privileges before proceeding + - Throws an exception if not running as administrator + - Windows-only functionality - automatically skips installation on non-Windows platforms + - Installs minimum version 2.8.5.201 of the NuGet PackageProvider + - Uses CurrentUser scope for installation to minimize system impact + - Verifies successful installation by re-querying the PackageProvider + - Detects and reports NuGet CLI availability if present + - Uses -Force flag to bypass confirmation prompts + - Includes comprehensive try-catch error handling with descriptive error messages + - Returns $true for successful installation or if already installed (idempotent behavior) + +.LINK + +.COMPONENT + DevSetup.Providers.Core + +.FUNCTIONALITY + Package Management Setup, NuGet Installation, Prerequisites Management +#> + +Function Install-Nuget { + [CmdletBinding()] + Param() + + try { + # Check if we're on Windows - NuGet PackageProvider is Windows-only + if (-not (Test-OperatingSystem -Windows)) { + Write-Host "NuGet PackageProvider is not available on this platform. Skipping installation." -ForegroundColor Yellow + return $true + } + + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "NuGet installation requires administrator privileges. Please run as administrator." + } + + # Check if NuGet PackageProvider is installed + $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue + Write-StatusMessage "- Installing NuGet PackageProvider" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if ($nugetProvider) { + #Write-Host "NuGet PackageProvider is already installed (version: $($nugetProvider.Version))" -ForegroundColor Green + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + #Write-Host "Installing NuGet PackageProvider..." -ForegroundColor Cyan + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser + + # Verify installation + $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue + if ($nugetProvider) { + #Write-Host "NuGet PackageProvider successfully installed (version: $($nugetProvider.Version))" -ForegroundColor Green + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + throw "Failed to install NuGet PackageProvider" + } + } + + # Check if nuget.exe CLI is also available + $nugetExe = Get-Command nuget -ErrorAction SilentlyContinue + if ($nugetExe) { + try { + $nugetVersion = (Invoke-Expression "& nuget help" 2>$null | Select-String "NuGet Version" | ForEach-Object { $_.Line.Split(':')[1].Trim() }) + if ($nugetVersion) { + #Write-Host "NuGet CLI is also available: $nugetVersion" -ForegroundColor Yellow + } + } + catch { + # Silently ignore CLI version check errors + } + } + + return $true + } + catch { + Write-Error "Error checking/installing NuGet: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 new file mode 100644 index 0000000..439119f --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 @@ -0,0 +1,139 @@ +BeforeAll { + function ConvertTo-Yaml { } + . $PSScriptRoot\Export-InstalledPowershellModules.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1 + Mock Test-RunningAsAdmin { $true } + Mock Get-InstalledModule { @( + @{ Name = "ModuleA"; Version = [version]"1.0.0" }, + @{ Name = "ModuleB"; Version = [version]"2.0.0" } + ) } + Mock Get-DevSetupManifest { @{ RequiredModules = @("ModuleA") } } + Mock Get-Module { param($Name) @{ Name = $Name; ModuleBase = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\$Name"; Version = [version]"1.0.0" } } + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @() } } } } } + Mock ConvertTo-Yaml { param($obj) "yaml-output" } + Mock ConvertTo-Json { param($obj) "json-output" } + Mock Out-File { } + Mock Write-Host { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Write-Debug { } + Mock Write-Verbose { } +} + +Describe "Export-InstalledPowershellModules" { + + Context "When not running as administrator" { + It "Should throw and return false" { + Mock Test-RunningAsAdmin { $false } + $result = Export-InstalledPowershellModules -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "requires administrator privileges" } + } + } + + Context "When no modules are found" { + It "Should warn and return true" { + Mock Get-InstalledModule { @() } + $result = Export-InstalledPowershellModules -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No PowerShell modules found" } + } + } + + Context "When core dependency modules are present" { + It "Should skip core dependency modules" { + Mock Get-InstalledModule { @( + @{ Name = "ModuleA"; Version = [version]"1.0.0" }, + @{ Name = "ModuleB"; Version = [version]"2.0.0" } + ) } + Mock Get-DevSetupManifest { @{ RequiredModules = @("ModuleA") } } + $result = Export-InstalledPowershellModules -Config "test.yaml" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding module: ModuleB" } + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -notmatch "Adding module: ModuleA" } + } + } + + Context "When modules are found and added to config" { + It "Should add new modules to YAML data" { + $result = Export-InstalledPowershellModules -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding module: ModuleB" } + } + } + + Context "When module version changes" { + It "Should update the module version in the config" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; minimumVersion = "1.0.0"; scope = "CurrentUser" }) } } } } } + Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } + $result = Export-InstalledPowershellModules -Config "test.yaml" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating module: ModuleB" } + } + } + + Context "When module exists but has no version" { + It "Should add minimumVersion to the module" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; scope = "CurrentUser" }) } } } } } + Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } + $result = Export-InstalledPowershellModules -Config "test.yaml" + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating module version: ModuleB" } + } + } + + Context "When module is unchanged" { + It "Should skip updating the module" { + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; minimumVersion = "2.0.0"; scope = "CurrentUser" }) } } } } } + Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } + $result = Export-InstalledPowershellModules -Config "test.yaml" + #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Skipping module (No Change): ModuleB" } + } + } + + Context "When DryRun is used" { + It "Should display YAML output and not write to file" { + $result = Export-InstalledPowershellModules -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Times 0 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } + } + } + + Context "When OutFile is specified" { + It "Should write YAML output to the specified file" { + $result = Export-InstalledPowershellModules -Config "test.yaml" -OutFile "out.yaml" + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } + } + } + + Context "When YAML conversion fails" { + It "Should fallback to JSON output" { + Mock ConvertTo-Yaml { throw "YAML error" } + $result = Export-InstalledPowershellModules -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Json -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } + } + } + + Context "When Out-File fails" { + It "Should write error and return false" { + Mock Out-File { throw "File error" } + $result = Export-InstalledPowershellModules -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } + } + } + + Context "When an unexpected error occurs" { + It "Should write error and return false" { + Mock Get-InstalledModule { throw "Unexpected error" } + $result = Export-InstalledPowershellModules -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error converting PowerShell modules" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 new file mode 100644 index 0000000..65b1f91 --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 @@ -0,0 +1,264 @@ +<# +.SYNOPSIS + Exports installed PowerShell modules to a YAML configuration file. + +.DESCRIPTION + This function scans the system for installed PowerShell modules and exports them to a YAML + configuration file in DevSetup format. It uses Get-InstalledModule to retrieve comprehensive + module information including versions and installation scope. The function intelligently skips + core dependency modules defined in the DevSetup manifest and can update existing configuration + files by merging new modules with existing ones. + +.PARAMETER Config + The path to the YAML configuration file to read from and write to. + This parameter is mandatory and specifies both the input and output file unless OutFile is specified. + +.PARAMETER OutFile + The path to save the updated YAML configuration. + Optional parameter that allows saving to a different file than the input Config file. + +.PARAMETER DryRun + Switch parameter that prevents writing to files and displays the resulting configuration to the console. + Useful for previewing changes before committing them to a file. + +.OUTPUTS + [System.Boolean] + Returns $true if the export completes successfully or if no modules are found. + Returns $false if there are errors during the export process. + +.EXAMPLE + Export-InstalledPowershellModules -Config "environment.yaml" + + Exports installed PowerShell modules to the existing environment.yaml configuration file. + +.EXAMPLE + Export-InstalledPowershellModules -Config "current.yaml" -OutFile "backup.yaml" + + Reads from current.yaml and saves the updated configuration with installed modules to backup.yaml. + +.EXAMPLE + Export-InstalledPowershellModules -Config "dev-env.yaml" -DryRun + + Shows what the configuration would look like without actually saving to file. + +.NOTES + - Requires administrator privileges to access all installed modules + - Uses Get-InstalledModule to retrieve module information from PowerShell Gallery + - Automatically skips core dependency modules listed in the DevSetup manifest + - Handles both CurrentUser and AllUsers scope modules using path analysis + - Merges with existing YAML configuration, preserving other sections + - Supports both simple string format and complex object format for modules + - Updates existing modules when versions have changed + - Converts string entries to hashtable format when additional properties are needed + - Tracks installation scope (CurrentUser/AllUsers) for each module + - Creates the devsetup.dependencies.powershell structure if it doesn't exist + - Provides detailed console output with color-coded status messages + - Includes comprehensive error handling for module scanning and file operations + - Preserves existing module properties while updating changed values + +.LINK + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Configuration Export, Module Discovery, YAML Generation +#> + +Function Export-InstalledPowershellModules { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Config, + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$OutFile, + [switch]$DryRun + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "This operation requires administrator privileges. Please run as administrator." + } + + # Get installed PowerShell modules + Write-Host "- Getting list of installed PowerShell modules..." -ForegroundColor Gray + $installedModules = Get-InstalledModule -ErrorAction SilentlyContinue + + if (-not $installedModules) { + Write-Warning "No PowerShell modules found or PowerShellGet is not available." + return $true + } + + $powershellModules = @() + + # Get core dependency modules to skip from DevSetup manifest + $manifest = Get-DevSetupManifest + $coreModulesToSkip = @() + if ($manifest -and $manifest.RequiredModules) { + $coreModulesToSkip = $manifest.RequiredModules | ForEach-Object { + if ($_ -is [string]) { + $_ + } elseif ($_ -is [hashtable] -and $_.ModuleName) { + $_.ModuleName + } elseif ($_ -is [hashtable] -and $_.name) { + $_.name + } + } + } + + foreach ($module in $installedModules) { + # Skip core dependency modules + if ($module.Name -in $coreModulesToSkip) { + Write-Verbose "Skipping core dependency module: $($module.Name)" + continue + } + + # Get module scope information + $moduleInfo = Get-Module -Name $module.Name -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 + + # Check if module is in CurrentUser or AllUsers scope + $modulePath = $moduleInfo.ModuleBase + $scope = "Unknown" + + if ($modulePath -like "*\WindowsPowerShell\Modules\*" -or $modulePath -like "*\PowerShell\Modules\*") { + if ($modulePath -like "*$env:USERPROFILE*") { + $scope = "CurrentUser" + } else { + $scope = "AllUsers" + } + } + + if ($scope -eq "CurrentUser" -or $scope -eq "AllUsers") { + Write-Debug "Found module: $($module.Name) (version: $($module.Version), scope: $scope)" + $powershellModules += @{ + name = $module.Name + version = $module.Version.ToString() + scope = $scope + } + } else { + Write-Verbose "Skipping module with unknown scope: $($module.Name)" + } + } + + Write-Debug " - Found $($powershellModules.Count) PowerShell modules in CurrentUser or AllUsers scope (excluding core dependencies)" + + # Read existing YAML configuration + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure powershellModules section exists + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.powershell) { $YamlData.devsetup.dependencies.powershell = @{} } + if (-not $YamlData.devsetup.dependencies.powershell.modules) { $YamlData.devsetup.dependencies.powershell.modules = @() } + + # Add modules to YAML data + foreach ($module in $powershellModules) { + # Check if module already exists + $existingModule = $YamlData.devsetup.dependencies.powershell.modules | Where-Object { + ($_ -is [string] -and $_ -eq $module.name) -or + ($_ -is [hashtable] -and $_.name -eq $module.name) + } + + if (-not $existingModule) { + Write-Host " - Adding module: $($module.name) ($($module.version), $($module.scope))" -ForegroundColor Gray + $YamlData.devsetup.dependencies.powershell.modules += @{ + name = $module.name + minimumVersion = $module.version + scope = $module.scope + } + } else { + # Module exists, check if version has changed + $existingVersion = $null + if ($existingModule -is [hashtable] -and $existingModule.minimumVersion) { + $existingVersion = $existingModule.minimumVersion + } elseif ($existingModule -is [hashtable] -and $existingModule.version) { + $existingVersion = $existingModule.version + } + + if ($existingVersion -and $existingVersion -ne $module.version) { + Write-Host " - Updating module: $($module.name) ($existingVersion -> $($module.version))" -ForegroundColor Gray + + # Find index and update + $index = $YamlData.devsetup.dependencies.powershell.modules.IndexOf($existingModule) + + # Preserve existing module structure but update version + if ($existingModule -is [string]) { + # Convert string to hashtable with version + $YamlData.devsetup.dependencies.powershell.modules[$index] = @{ + name = $module.name + minimumVersion = $module.version + scope = $module.scope + } + } else { + # Update existing hashtable + $YamlData.devsetup.dependencies.powershell.modules[$index].minimumVersion = $module.version + if (-not $existingModule.scope) { + $YamlData.devsetup.dependencies.powershell.modules[$index].scope = $module.scope + } + } + } elseif (-not $existingVersion) { + Write-Host " - Updating module version: $($module.name)" -ForegroundColor Gray + + # Find index and add version + $index = $YamlData.devsetup.dependencies.powershell.modules.IndexOf($existingModule) + + if ($existingModule -is [string]) { + # Convert string to hashtable with version + $YamlData.devsetup.dependencies.powershell.modules[$index] = @{ + name = $module.name + minimumVersion = $module.version + scope = $module.scope + } + } else { + # Add version to existing hashtable + $YamlData.devsetup.dependencies.powershell.modules[$index].minimumVersion = $module.version + if (-not $existingModule.scope) { + $YamlData.devsetup.dependencies.powershell.modules[$index].scope = $module.scope + } + } + } else { + Write-Host " - Skipping module (No Change): $($module.name) ($($module.version))" -ForegroundColor Gray + } + } + } + + # Convert to YAML + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + } + catch { + Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" + $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 + } + + # Handle output based on parameters + if ($DryRun) { + Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan + Write-Host $yamlOutput -ForegroundColor White + Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow + } else { + # Determine output file + $outputFile = if ($OutFile) { $OutFile } else { $Config } + + try { + Write-Debug "`nSaving configuration to: $outputFile" + $yamlOutput | Out-File -FilePath $outputFile + Write-Debug "Configuration saved successfully!" + } + catch { + Write-Error "Failed to save configuration to $outputFile`: $_" + return $false + } + } + + Write-Host "PowerShell modules conversion completed!" -ForegroundColor Green + return $true + } + catch { + Write-Error "Error converting PowerShell modules: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 new file mode 100644 index 0000000..6880c74 --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 @@ -0,0 +1,154 @@ +BeforeAll { + . $PSScriptRoot\Install-PowershellModule.ps1 + . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 + . $PSScriptRoot\Uninstall-PowershellModule.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + Mock Write-Error {} + Mock Write-Warning {} +} + +Describe "Install-PowershellModule" { + + Context "When installing for AllUsers without admin privileges" { + It "Should return false" { + Mock Test-RunningAsAdmin { return $false } + $result = Install-PowershellModule -ModuleName "Az" -Scope "AllUsers" + $result | Should -Be $false + } + } + + Context "When module is already installed with correct version and scope" { + It "Should return true and not call Uninstall-PowershellModule or Install-Module" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::Pass + } + Mock Uninstall-PowershellModule { throw "Should not be called" } + Mock Install-Module { throw "Should not be called" } + $result = Install-PowershellModule -ModuleName "Az" + $result | Should -Be $true + } + } + + Context "When module is installed but needs to be uninstalled and reinstalled" { + It "Should uninstall and install the module, returning true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::Installed + } + $script:uninstallCalled = $false + Mock Uninstall-PowershellModule -MockWith { + param( + [string]$ModuleName, + [string]$Scope + ) + $script:uninstallCalled = $true + } + $script:installCalled = $false + Mock Install-Module -MockWith { + param( + [string]$Name, + [switch]$Force, + [string]$Scope, + [switch]$AllowClobber, + [string]$RequiredVersion + ) + $script:installCalled = $true + } + $result = Install-PowershellModule -ModuleName "Az" + $result | Should -Be $true + $uninstallCalled | Should -Be $true + $installCalled | Should -Be $true + } + } + + Context "When module is not installed" { + It "Should install the module and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::NotInstalled + } + $script:installCalled = $false + Mock Install-Module -MockWith { + param( + [string]$Name, + [switch]$Force, + [string]$Scope, + [switch]$AllowClobber, + [string]$RequiredVersion + ) + $script:installCalled = $true + } + $result = Install-PowershellModule -ModuleName "Az" + $result | Should -Be $true + $installCalled | Should -Be $true + } + } + + Context "When Install-Module throws an exception" { + It "Should return false" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::NotInstalled + } + Mock Install-Module { throw "Install failed" } + $result = Install-PowershellModule -ModuleName "Az" + $result | Should -Be $false + } + } + + Context "When Uninstall-PowershellModule throws an exception" { + It "Should return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::Installed + } + Mock Uninstall-PowershellModule { throw "Uninstall failed" } + Mock Install-Module -MockWith { + param( + [string]$Name + ) + $script:installParams = @{ + ModuleName = $Name + } + } + $result = Install-PowershellModule -ModuleName "Az" + $result | Should -Be $true + } + } + + Context "When installing with Version, Force, and AllowClobber" { + It "Should pass correct parameters and return true" { + Mock Test-RunningAsAdmin { return $true } + Mock Test-PowershellModuleInstalled { + return [InstalledState]::NotInstalled + } + Mock Install-Module -MockWith { + param( + [string]$Name, + [switch]$Force, + [string]$Scope, + [switch]$AllowClobber, + [string]$RequiredVersion + ) + $script:installParams = @{ + ModuleName = $Name + Force = $Force + Scope = $Scope + AllowClobber = $AllowClobber + RequiredVersion = $RequiredVersion + } + } + $result = Install-PowershellModule -ModuleName "Az" -Version "9.0.1" -Force -AllowClobber -Scope "CurrentUser" + $result | Should -Be $true + + $installParams | Should -Not -Be $null + $installParams.ModuleName | Should -Be "Az" + $installParams.Force | Should -Be $true + $installParams.Scope | Should -Be "CurrentUser" + $installParams.AllowClobber | Should -Be $true + $installParams.RequiredVersion | Should -Be "9.0.1" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModule.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModule.ps1 new file mode 100644 index 0000000..bd4b9ce --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModule.ps1 @@ -0,0 +1,147 @@ +<# +.SYNOPSIS + Installs a PowerShell module with specified parameters and scope validation. + +.DESCRIPTION + Installs a PowerShell module using `Install-Module` with comprehensive validation and scope management. + Checks for existing installations and handles version/scope conflicts by intelligently uninstalling and reinstalling as needed. + Supports both `CurrentUser` and `AllUsers` scopes, with privilege validation for `AllUsers`. + +.PARAMETER ModuleName + The name of the PowerShell module to install. + Mandatory and must be a valid, non-empty string. + +.PARAMETER Version + The specific version of the module to install. + Optional; installs the latest version if not provided. + +.PARAMETER Force + Switch to force installation even if the module already exists. + Optional; passes the `-Force` flag to `Install-Module`. + +.PARAMETER AllowClobber + Switch to allow installation of modules that contain cmdlets with the same names as existing cmdlets. + Optional; passes the `-AllowClobber` flag to `Install-Module`. + +.PARAMETER Scope + The installation scope for the module. + Optional; valid values are `'CurrentUser'` or `'AllUsers'`. Defaults to `'CurrentUser'`. + `AllUsers` scope requires administrator privileges. + +.OUTPUTS + `[System.Boolean]` + Returns `$true` if the module was successfully installed or already meets requirements. + Returns `$false` if the installation failed. + +.EXAMPLE + Install-PowershellModule -ModuleName "posh-git" + # Installs the latest version of posh-git module for the current user. + +.EXAMPLE + Install-PowershellModule -ModuleName "PSReadLine" -Version "2.2.6" + # Installs a specific version of PSReadLine module for the current user. + +.EXAMPLE + Install-PowershellModule -ModuleName "PowerShellGet" -Scope "AllUsers" -Force + # Installs PowerShellGet module for all users with force flag (requires administrator privileges). + +.EXAMPLE + Install-PowershellModule -ModuleName "Az" -AllowClobber -Scope "CurrentUser" + # Installs the Az module allowing cmdlet name conflicts for the current user. + +.NOTES + **Scope Requirements:** + - Administrator privileges required for `AllUsers` scope. + - Uses `Test-PowershellModuleInstalled` to check existing installations. + + **Installation Logic:** + - Returns immediately if module with correct version and scope exists. + - Uninstalls and reinstalls if version matches but scope differs. + - Reinstalls in-place if scope matches but version differs. + - Uninstalls and reinstalls if both version and scope differ. + + **Error Handling:** + - Uses try/catch for robust error handling. + - Returns `$false` on any failure. + + **Parameter Splatting:** + - Uses parameter splatting for reliable `Install-Module` execution. + +.LINK + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Module Management, Package Installation, Scope Validation +#> + +Function Install-PowershellModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [String] $ModuleName, + + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [String] $Version, + + [Parameter(Mandatory=$false)] + [Switch] $Force = $false, + + [Parameter(Mandatory=$false)] + [Switch] $AllowClobber = $false, + + [Parameter(Mandatory=$false)] + [ValidateSet('CurrentUser', 'AllUsers')] + [String] $Scope = 'CurrentUser' + ) + + try { + # Check if running as administrator only when installing for all users + if ($Scope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { + throw "PowerShell module installation to AllUsers scope requires administrator privileges. Please run as administrator or use CurrentUser scope." + } + + $installParams = @{ + Name = $ModuleName + Force = $Force + Scope = $Scope + AllowClobber = $AllowClobber + SkipPublisherCheck = $true + } + + $testParams = @{ + ModuleName = $ModuleName + Scope = $Scope + } + + if($PSBoundParameters.ContainsKey('Version')) { + $testParams.Version = $Version + $installParams.RequiredVersion = $Version + } + + $testResult = Test-PowershellModuleInstalled @testParams + + if($testResult.HasFlag([InstalledState]::Pass)) { + return $true + } + + if($testResult.HasFlag([InstalledState]::Installed)) { + try { + Uninstall-PowershellModule -ModuleName $ModuleName + } catch { + # Uninstall might have failed, we keep going anyways + Write-Debug "Failed to uninstall existing module '$ModuleName': $_" + } + } + + # Install the PowerShell module + Install-Module @installParams + return $true + } + catch { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 new file mode 100644 index 0000000..4ec0b6d --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 @@ -0,0 +1,170 @@ +BeforeAll { + . $PSScriptRoot\Install-PowershellModules.ps1 + . $PSScriptRoot\Install-PowerShellModule.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + Mock Write-StatusMessage { } + Mock Test-RunningAsAdmin { return $true } + Mock Write-Error {} + Mock Write-Warning {} + Mock Write-Host {} +} + +Describe "Install-PowershellModules" { + + Context "When YAML configuration is missing PowerShell modules" { + It "Should return false" { + $yamlData = @{ devsetup = @{ dependencies = @{ powershell = @{ } } } } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When YAML configuration is missing dependencies" { + It "Should return false" { + $yamlData = @{ devsetup = @{ } } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When AllUsers scope is specified but not running as admin" { + It "Should return false" { + Mock Test-RunningAsAdmin { return $false } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + scope = "AllUsers" + modules = @("posh-git") + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When modules are installed successfully (string format)" { + It "Should install all modules and return true" { + $script:installCalls = @() + Mock Install-PowerShellModule -MockWith { + param($ModuleName, $Force, $AllowClobber, $Scope, $Version) + $script:installCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git", "PSReadLine") + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $installCalls | Should -Contain "posh-git" + $installCalls | Should -Contain "PSReadLine" + } + } + + Context "When modules are installed successfully (object format)" { + It "Should install all modules and return true" { + $script:installCalls = @() + Mock Install-PowerShellModule -MockWith { + param($ModuleName, $Force, $AllowClobber, $Scope, $Version) + $script:installCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @( + @{ name = "posh-git"; minimumVersion = "1.0.0"; scope = "CurrentUser"; force = $true; allowClobber = $true }, + @{ name = "PSReadLine"; minimumVersion = "2.2.6"; scope = "AllUsers"; force = $false; allowClobber = $false } + ) + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $installCalls | Should -Contain "posh-git" + $installCalls | Should -Contain "PSReadLine" + } + } + + Context "When some modules fail to install" { + It "Should continue and return true" { + $script:installCalls = @() + Mock Install-PowerShellModule -MockWith { + param($ModuleName, $Force, $AllowClobber, $Scope, $Version) + $script:installCalls += $ModuleName + if ($ModuleName -eq "PSReadLine") { return $false } + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git", "PSReadLine", "PowerShellGet") + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $installCalls | Should -Contain "posh-git" + $installCalls | Should -Contain "PSReadLine" + $installCalls | Should -Contain "PowerShellGet" + } + } + + Context "When module entry is empty or missing name" { + It "Should skip invalid entries and return true" { + $script:installCalls = @() + Mock Install-PowerShellModule -MockWith { + param($ModuleName, $Force, $AllowClobber, $Scope, $Version) + $script:installCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @( + $null, + @{ minimumVersion = "1.0.0" }, + "posh-git" + ) + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $installCalls | Should -Contain "posh-git" + $installCalls.Count | Should -Be 1 + } + } + + Context "When an exception occurs during installation" { + It "Should catch and return false" { + Mock Install-PowerShellModule { throw "Unexpected error" } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git") + } + } + } + } + $result = Install-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 new file mode 100644 index 0000000..2b18c4e --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Install-PowershellModules.ps1 @@ -0,0 +1,162 @@ +<# +.SYNOPSIS + Installs PowerShell modules from YAML configuration data. + +.DESCRIPTION + This function processes YAML configuration data to install PowerShell modules using Install-PowerShellModule. + It supports both simple string formats and complex object formats for modules, allowing for detailed + configuration including versions, installation scope, and module-specific parameters. The function validates + administrator privileges when AllUsers scope is specified and provides comprehensive error handling and + progress reporting throughout the installation process. + +.PARAMETER YamlData + The YAML configuration data containing PowerShell module definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.powershell.modules and optionally devsetup.dependencies.powershell.scope + +.OUTPUTS + [System.Boolean] + Returns $true if installation completes successfully (even if individual modules fail). + Returns $false if configuration is invalid or critical errors occur. + +.EXAMPLE + $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml + Install-PowershellModules -YamlData $yamlData + + Installs PowerShell modules from a YAML configuration file. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + scope = "CurrentUser" + modules = @( + "posh-git", + @{ + name = "PSReadLine" + minimumVersion = "2.2.6" + }, + @{ + name = "PowerShellGet" + scope = "AllUsers" + force = $true + allowClobber = $false + } + ) + } + } + } + } + Install-PowershellModules -YamlData $yamlData + + Demonstrates the PSCustomObject structure and installs the configured modules. + +.NOTES + - Requires the YAML configuration to have devsetup.dependencies.powershell.modules structure + - Returns $false immediately if PowerShell modules configuration is missing or invalid + - Supports global scope setting with module-specific overrides + - Default scope is 'CurrentUser' if not specified + - Validates administrator privileges when AllUsers scope is requested + - Supports both string and object formats for module definitions + - Module object format supports: name (required), minimumVersion (optional), scope (optional), force (optional), allowClobber (optional) + - Skips empty or invalid entries in the configuration without stopping execution + - Uses Install-PowerShellModule function for actual installation + - Provides detailed progress reporting with color-coded status messages + - Individual installation failures do not stop the overall process + - Tracks and reports installation counts for all processed modules + - Uses parameter splatting for reliable module installation + +.LINK + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Bulk Installation, Configuration Processing, Module Management +#> + +Function Install-PowershellModules { + Param( + [Parameter(Mandatory=$true, Position=0)] + [ValidateNotNullOrEmpty()] + [PSCustomObject]$YamlData + ) + + try { + Write-StatusMessage "- Installing PowerShell modules from configuration:" -ForegroundColor Cyan + # Check if PowerShell modules dependencies exist + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.powershell -or -not $YamlData.devsetup.dependencies.powershell.modules) { + Write-Debug "PowerShell modules not found in YAML configuration. Skipping installation." + Write-StatusMessage "- PowerShell modules installation completed! Processed 0 modules." -ForegroundColor Green + Write-Host "" + return $false + } + + $modules = $YamlData.devsetup.dependencies.powershell.modules + + # Get global scope setting from YAML, default to CurrentUser + $globalScope = 'AllUsers' + if ($YamlData.devsetup.dependencies.powershell.scope) { + $globalScope = $YamlData.devsetup.dependencies.powershell.scope + } + + # Check if running as administrator when global scope is AllUsers + if ($globalScope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { + throw "PowerShell module installation to AllUsers scope requires administrator privileges. Please run as administrator or set powershellModuleScope to CurrentUser." + } + + + $moduleCount = 0 + + foreach ($module in $modules) { + if (-not $module) { continue } + + $moduleCount++ + + # Normalize module to object format + if ($module -is [string]) { + $moduleObj = @{ name = $module } + } else { + $moduleObj = $module + } + + # Validate module name + if ([string]::IsNullOrEmpty($moduleObj.name)) { + Write-Warning "Module entry #$moduleCount has no name specified, skipping" + continue + } + + # Determine scope for this module (module-specific overrides global) + $moduleScope = if ($moduleObj.scope) { $moduleObj.scope } else { $globalScope } + + # Set defaults and build parameters + $installParams = @{ + ModuleName = $moduleObj.name + Force = if ($moduleObj.force -is [bool]) { $moduleObj.force } else { $true } + AllowClobber = if ($moduleObj.allowClobber -is [bool]) { $moduleObj.allowClobber } else { $true } + Scope = $moduleScope + } + + if ($moduleObj.minimumVersion) { + $installParams.Version = $moduleObj.minimumVersion + Write-StatusMessage "- Installing PowerShell module: $($moduleObj.name) (version: $($moduleObj.minimumVersion), scope: $moduleScope)" -ForegroundColor Gray -Width 112 -NoNewLine -Indent 2 + } else { + Write-StatusMessage "- Installing PowerShell module: $($moduleObj.name) (latest version) to $moduleScope scope" -ForegroundColor Gray -Width 112 -NoNewLine -Indent 2 + } + + if ((Install-PowerShellModule @installParams)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } + } + Write-StatusMessage "- PowerShell modules installation completed! Processed $moduleCount modules." -ForegroundColor Green + Write-Host "" + return $true + } + catch { + Write-Error "Error installing PowerShell modules: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 new file mode 100644 index 0000000..4c18e17 --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 @@ -0,0 +1,98 @@ +BeforeAll { + . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 +} + +Describe "Test-PowershellModuleInstalled" { + + Context "When module is not installed" { + It "Should return NotInstalled" { + Mock Get-Module { return $null } + $result = Test-PowershellModuleInstalled -ModuleName "notfound" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } + + Context "When module is installed (any version, any scope)" { + It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { + Mock Get-Module { + [PSCustomObject]@{ + Name = "posh-git" + Version = "1.0.0" + Path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\posh-git" + } + } + $result = Test-PowershellModuleInstalled -ModuleName "posh-git" + $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When module is installed with matching version" { + It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { + Mock Get-Module { + [PSCustomObject]@{ + Name = "PSReadLine" + Version = "2.2.6" + Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" + } + } + $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" + $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When module is installed but version does not match" { + It "Should return Installed + GlobalVersionMet" { + Mock Get-Module { + [PSCustomObject]@{ + Name = "PSReadLine" + Version = "2.2.5" + Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" + } + } + $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" + $expected = [InstalledState]::Installed + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When module is installed in AllUsers scope" { + It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { + Mock Get-Module { + [PSCustomObject]@{ + Name = "PowerShellGet" + Version = "2.2.5" + Path = "$env:ProgramFiles\PowerShell\Modules\PowerShellGet" + } + } + $result = Test-PowershellModuleInstalled -ModuleName "PowerShellGet" -Scope "AllUsers" + $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When module is installed in CurrentUser scope" { + It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { + Mock Get-Module { + [PSCustomObject]@{ + Name = "Az" + Version = "9.0.1" + Path = "$env:USERPROFILE\Documents\PowerShell\Modules\Az" + } + } + $result = Test-PowershellModuleInstalled -ModuleName "Az" -Scope "CurrentUser" + $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When Get-Module throws an exception" { + It "Should return NotInstalled" { + Mock Get-Module { throw "Unexpected error" } + $result = Test-PowershellModuleInstalled -ModuleName "Az" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 new file mode 100644 index 0000000..cc5e943 --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS + Tests whether a PowerShell module is installed, with optional version and scope validation. + +.DESCRIPTION + Checks if a PowerShell module is installed on the system and optionally validates + specific version requirements and installation scope. Uses `Get-Module -ListAvailable` + to find installed modules and examines their installation paths to determine scope + (`CurrentUser` vs `AllUsers`). Supports multiple parameter sets to check different + combinations of module existence, version matching, and scope validation. + +.PARAMETER ModuleName + The name of the PowerShell module to check. + Mandatory for all parameter sets. + +.PARAMETER Version + The specific version of the module to validate. + Optional; only used in version-related parameter sets. + +.PARAMETER Scope + The installation scope to validate (`CurrentUser` or `AllUsers`). + Optional; only used in scope-related parameter sets. + +.OUTPUTS + `[InstalledState]` + Returns an InstalledState enum value indicating installation status and version/scope match. + Returns `[InstalledState]::NotInstalled` if not found or criteria are not met. + +.EXAMPLE + Test-PowershellModuleInstalled -ModuleName "posh-git" + # Checks if the posh-git module is installed (any version, any scope). + +.EXAMPLE + Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" + # Checks if PSReadLine module version 2.2.6 is installed. + +.EXAMPLE + Test-PowershellModuleInstalled -ModuleName "PowerShellGet" -Scope "AllUsers" + # Checks if PowerShellGet module is installed in AllUsers scope. + +.EXAMPLE + Test-PowershellModuleInstalled -ModuleName "Az" -Version "9.0.1" -Scope "CurrentUser" + # Checks if Az module version 9.0.1 is installed in CurrentUser scope. + +.NOTES + **Module Paths:** + - CurrentUser (PS5.1): `$HOME\Documents\WindowsPowerShell\Modules` + - CurrentUser (PS7+): `$HOME\Documents\PowerShell\Modules` + - AllUsers (PS5.1): `$Env:ProgramFiles\WindowsPowerShell\Modules` + - AllUsers (PS7+): `$Env:ProgramFiles\PowerShell\Modules` + + **Parameter Sets:** + - `ModuleCheck`: Checks if module exists. + - `ModuleVersionCheck`: Checks existence and exact version match. + - `ModuleScopeCheck`: Checks existence and scope match. + - `ModuleVersionAndScopeCheck`: Checks existence, version, and scope match. + + **Behavior:** + - Returns the highest version when multiple versions are installed. + - Uses `[InstalledState]` enum for detailed status. + - Includes error handling and debug logging. + +.LINK + Get-Module + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Module Detection, Installation Verification, Scope Validation +#> + +Function Test-PowershellModuleInstalled { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, ParameterSetName='ModuleCheck')] + [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionCheck')] + [Parameter(Mandatory=$true, ParameterSetName='ModuleScopeCheck')] + [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] + [ValidateNotNullOrEmpty()] + [string]$ModuleName, + + [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionCheck')] + [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] + [string]$Version, + + [Parameter(Mandatory=$true, ParameterSetName='ModuleScopeCheck')] + [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] + [ValidateSet("CurrentUser", "AllUsers")] + [string]$Scope + ) + + # CurrentUser ps5.1 + # $HOME\Documents\WindowsPowerShell\Modules + # CurrentUser ps7 + # $HOME\Documents\PowerShell\Modules + + # AllUsers ps5.1 + # $Env:ProgramFiles\WindowsPowerShell\Modules + # AllUsers ps7 + # $Env:ProgramFiles\PowerShell\Modules + $InstallPaths = @( + @{ + Path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules"; + Scope = "CurrentUser" + }, + @{ + Path = "$env:USERPROFILE\Documents\PowerShell\Modules"; + Scope = "CurrentUser" + } + @{ + Path = "$env:ProgramFiles\PowerShell\Modules"; + Scope = "AllUsers" + }, + @{ + Path = "$env:ProgramFiles\WindowsPowerShell\Modules"; + Scope = "AllUsers" + } + ) + + [InstalledState]$installedState = [InstalledState]::NotInstalled + + try { + $module = Get-Module -Name $ModuleName -ListAvailable -ErrorAction Stop | + Sort-Object Version -Descending | + Select-Object -First 1 + + if ($module) { + $installedState = [InstalledState]::Installed + + if($PSBoundParameters.ContainsKey('Scope')) { + $InstallPaths | ForEach-Object { + if ($module.Path -like "$($_.Path)\*") { + if ($_.Scope -eq $Scope) { + $installedState += [InstalledState]::GlobalVersionMet + } + } + } + } else { + $installedState += [InstalledState]::GlobalVersionMet + } + + if ($PSBoundParameters.ContainsKey('Version')) { + if([Version]$module.Version -eq [Version]$Version) { + $installedState += [InstalledState]::MinimumVersionMet + $installedState += [InstalledState]::RequiredVersionMet + } + } else { + $installedState += [InstalledState]::MinimumVersionMet + $installedState += [InstalledState]::RequiredVersionMet + } + } + return $installedState + } catch { + return [InstalledState]::NotInstalled + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 new file mode 100644 index 0000000..1ebb2af --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 @@ -0,0 +1,109 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-PowershellModule.ps1 + . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 + Mock Test-RunningAsAdmin { return $true } + Mock Write-Warning { } + Mock Write-Error { } + Mock Write-Debug { } +} + +Describe "Uninstall-PowershellModule" { + + Context "When module is not installed" { + It "Should return true and warn" { + Mock Test-PowershellModuleInstalled { return [InstalledState]::NotInstalled } + $result = Uninstall-PowershellModule -ModuleName "notfound" + $result | Should -Be $true + } + } + + Context "When module is installed for AllUsers but not running as admin" { + It "Should return false and warn" { + $callCount = 0 + Mock Test-PowershellModuleInstalled -MockWith { + param($ModuleName, $Scope) + $callCount++ + if ($callCount -eq 1) { return [InstalledState]::Installed } + if ($callCount -eq 2) { return [InstalledState]::Pass } + return [InstalledState]::NotInstalled + } + Mock Test-RunningAsAdmin { return $false } + $result = Uninstall-PowershellModule -ModuleName "Az" + $result | Should -Be $false + } + } + + Context "When module is installed and uninstall succeeds" { + It "Should remove and uninstall the module, returning true" { + $script:callCount = 0 + Mock Test-PowershellModuleInstalled -MockWith { + param($ModuleName, $Scope) + $script:callCount++ + if ($script:callCount -eq 1) { return [InstalledState]::Installed } + if ($script:callCount -eq 2) { return [InstalledState]::Installed } + if ($script:callCount -eq 3) { return [InstalledState]::NotInstalled } + return [InstalledState]::NotInstalled + } + $script:removeCalled = $false + $script:uninstallCalled = $false + Mock Remove-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + $script:removeCalled = $true + } + Mock Uninstall-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + $script:uninstallCalled = $true + } + $result = Uninstall-PowershellModule -ModuleName "posh-git" + $removeCalled | Should -Be $true + $uninstallCalled | Should -Be $true + $result | Should -Be $true + } + } + + Context "When uninstall fails with exception" { + It "Should return false and write error" { + $script:callCount = 0 + Mock Test-PowershellModuleInstalled -MockWith { + param($ModuleName, $Scope) + $script:callCount++ + if ($script:callCount -eq 1) { return [InstalledState]::Installed } + if ($script:callCount -eq 2) { return [InstalledState]::Installed } + return [InstalledState]::NotInstalled + } + Mock Remove-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + } + Mock Uninstall-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + throw "Uninstall failed" + } + $result = Uninstall-PowershellModule -ModuleName "PSReadLine" + $result | Should -Be $false + } + } + + Context "When module is installed but still present after uninstall" { + It "Should return false" { + $script:callCount = 0 + Mock Test-PowershellModuleInstalled -MockWith { + param($ModuleName, $Scope) + $script:callCount++ + if ($script:callCount -eq 1) { return [InstalledState]::Installed } + if ($script:callCount -eq 2) { return [InstalledState]::Installed } + if ($script:callCount -eq 3) { return [InstalledState]::Installed } + return [InstalledState]::NotInstalled + } + Mock Remove-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + } + Mock Uninstall-Module -MockWith { + param([string]$Name, [switch]$Force, [string]$ErrorAction) + } + $result = Uninstall-PowershellModule -ModuleName "PowerShellGet" + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 new file mode 100644 index 0000000..44e563d --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Uninstalls a PowerShell module from the system. + +.DESCRIPTION + This function removes a PowerShell module from the system by first removing it from the current session + using Remove-Module, then uninstalling it completely using Uninstall-Module. The function includes + validation to check if the module is installed before attempting removal, validates administrator + privileges for AllUsers scope modules, and provides comprehensive error handling throughout the + uninstallation process. + +.PARAMETER ModuleName + The name of the PowerShell module to uninstall. + This parameter is mandatory and must be a valid string representing an installed PowerShell module name. + +.OUTPUTS + [System.Boolean] + Returns $true if the module was successfully uninstalled or was not installed. + Returns $false if the uninstallation failed or insufficient privileges for AllUsers modules. + +.EXAMPLE + Uninstall-PowershellModule -ModuleName "posh-git" + + Uninstalls the posh-git module from the system. + +.EXAMPLE + $result = Uninstall-PowershellModule -ModuleName "PSReadLine" + if ($result) { + Write-Host "PSReadLine module removed successfully" + } else { + Write-Host "Failed to remove PSReadLine module" + } + + Demonstrates capturing the return value to check uninstallation success. + +.EXAMPLE + @("Module1", "Module2", "Module3") | ForEach-Object { + Uninstall-PowershellModule -ModuleName $_ + } + + Shows bulk uninstallation of multiple modules. + +.NOTES + - Uses Test-PowershellModuleInstalled to verify module existence before attempting removal + - Returns $true if module is not installed (considered successful since goal is achieved) + - Validates administrator privileges for AllUsers scope modules using Test-RunningAsAdmin + - Returns $false immediately if AllUsers module requires elevation but session is not elevated + - Performs two-step removal process: + 1. Remove-Module: Removes from current PowerShell session (with -Force flag) + 2. Uninstall-Module: Completely removes from system (with -Force flag) + - Uses -ErrorAction Stop for proper exception handling + - Includes comprehensive try-catch error handling with descriptive error messages + - Provides detailed debug logging for troubleshooting uninstallation issues + - Uses Write-Warning for non-critical issues (module not found, privilege issues) + - Uses Write-Error for actual uninstallation failures + +.LINK + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Module Management, Package Removal, System Cleanup +#> + +Function Uninstall-PowershellModule { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [String] $ModuleName + ) + + $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName + if ($installedState -eq [InstalledState]::NotInstalled) { + Write-Warning "PowerShell module '$ModuleName' is not installed. No action taken." + return $true + } + + $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName -Scope 'AllUsers' + if ($installedState.HasFlag([InstalledState]::Pass) -and (-not (Test-RunningAsAdmin))) { + Write-Warning "PowerShell module '$ModuleName' is installed for AllUsers but current session is not elevated. Cannot uninstall." + return $false + } + + try { + Write-Debug "Uninstalling PowerShell module '$ModuleName'..." + Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue + Uninstall-Module -Name $ModuleName -Force -ErrorAction Stop + Write-Debug "PowerShell module '$ModuleName' uninstalled successfully." + $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName + return ($installedState -eq [InstalledState]::NotInstalled) + } catch { + Write-Error "Failed to uninstall PowerShell module '$ModuleName': $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 new file mode 100644 index 0000000..17471fb --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 @@ -0,0 +1,170 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-PowershellModules.ps1 + . $PSScriptRoot\Uninstall-PowerShellModule.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Test-RunningAsAdmin { return $true } + Mock Write-Host { } +} + +Describe "Uninstall-PowershellModules" { + + Context "When YAML configuration is missing PowerShell modules" { + It "Should return false and warn" { + $yamlData = @{ devsetup = @{ dependencies = @{ powershell = @{ } } } } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When YAML configuration is missing dependencies" { + It "Should return false and warn" { + $yamlData = @{ devsetup = @{ } } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When AllUsers scope is specified but not running as admin" { + It "Should return false" { + Mock Test-RunningAsAdmin { return $false } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + scope = "AllUsers" + modules = @("posh-git") + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When modules are uninstalled successfully (string format)" { + It "Should uninstall all modules and return true" { + $script:uninstallCalls = @() + Mock Uninstall-PowerShellModule -MockWith { + param($ModuleName) + $script:uninstallCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git", "PSReadLine") + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $uninstallCalls | Should -Contain "posh-git" + $uninstallCalls | Should -Contain "PSReadLine" + } + } + + Context "When modules are uninstalled successfully (object format)" { + It "Should uninstall all modules and return true" { + $script:uninstallCalls = @() + Mock Uninstall-PowerShellModule -MockWith { + param($ModuleName) + $script:uninstallCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @( + @{ name = "posh-git"; minimumVersion = "1.0.0"; scope = "CurrentUser" }, + @{ name = "PSReadLine"; minimumVersion = "2.2.6"; scope = "AllUsers" } + ) + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $uninstallCalls | Should -Contain "posh-git" + $uninstallCalls | Should -Contain "PSReadLine" + } + } + + Context "When some modules fail to uninstall" { + It "Should continue and return true" { + $script:uninstallCalls = @() + Mock Uninstall-PowerShellModule -MockWith { + param($ModuleName) + $script:uninstallCalls += $ModuleName + if ($ModuleName -eq "PSReadLine") { return $false } + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git", "PSReadLine", "PowerShellGet") + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $uninstallCalls | Should -Contain "posh-git" + $uninstallCalls | Should -Contain "PSReadLine" + $uninstallCalls | Should -Contain "PowerShellGet" + } + } + + Context "When module entry is empty or missing name" { + It "Should skip invalid entries and return true" { + $script:uninstallCalls = @() + Mock Uninstall-PowerShellModule -MockWith { + param($ModuleName) + $script:uninstallCalls += $ModuleName + return $true + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @( + $null, + @{ minimumVersion = "1.0.0" }, + "posh-git" + ) + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $true + $uninstallCalls | Should -Contain "posh-git" + $uninstallCalls.Count | Should -Be 1 + } + } + + Context "When an exception occurs during uninstallation" { + It "Should catch and return false" { + Mock Uninstall-PowerShellModule { throw "Unexpected error" } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + modules = @("posh-git") + } + } + } + } + $result = Uninstall-PowershellModules -YamlData $yamlData + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 new file mode 100644 index 0000000..50b4563 --- /dev/null +++ b/DevSetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 @@ -0,0 +1,157 @@ +<# +.SYNOPSIS + Uninstalls multiple PowerShell modules from the system based on YAML configuration. + +.DESCRIPTION + This function removes multiple PowerShell modules specified in a DevSetup YAML configuration. + It validates administrator privileges when required, parses the configuration for PowerShell + module definitions, and systematically uninstalls each module. The function supports both + simple string format and complex object format for module specifications, handles scope + settings, and provides comprehensive progress reporting during the uninstallation process. + +.PARAMETER YamlData + The parsed YAML configuration data containing PowerShell module definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.powershell.modules containing an array of module specifications. + +.OUTPUTS + [System.Boolean] + Returns $true if all modules are successfully processed (even if some individual uninstalls fail). + Returns $false if the operation encounters critical errors or cannot proceed. + +.EXAMPLE + $config = Read-ConfigurationFile -Path "environment.yaml" + Uninstall-PowershellModules -YamlData $config + + Uninstalls all PowerShell modules defined in the environment.yaml configuration. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + powershell = @{ + scope = "CurrentUser" + modules = @("PSReadLine", "Pester", "PowerShellGet") + } + } + } + } + Uninstall-PowershellModules -YamlData $yamlData + + Demonstrates uninstalling modules using a programmatically created configuration. + +.EXAMPLE + if (Uninstall-PowershellModules -YamlData $config) { + Write-Host "All PowerShell modules processed successfully" + } else { + Write-Host "PowerShell module uninstallation encountered errors" + } + + Shows checking the return value to verify uninstallation completion. + +.NOTES + - Requires administrator privileges when uninstalling from AllUsers scope + - Uses Test-RunningAsAdmin to validate privileges when scope is AllUsers + - Throws an exception if AllUsers scope is specified without administrator privileges + - Skips uninstallation gracefully if no PowerShell modules are found in configuration + - Supports two module specification formats: + * Simple string: "ModuleName" + * Complex object: @{ name = "ModuleName"; minimumVersion = "1.0.0"; scope = "CurrentUser" } + - Global scope setting defaults to CurrentUser if not specified in configuration + - Module-specific scope settings override the global scope setting + - Validates module names and skips entries with missing names + - Uses Uninstall-PowerShellModule for individual module removal + - Provides detailed progress reporting with module counts and version information + - Uses color-coded console output: Cyan for progress, Gray for module status, Green/Red for results + - Continues processing remaining modules even if individual uninstalls fail + - Returns $true for overall success even with individual module failures + - Includes comprehensive try-catch error handling with descriptive error messages + +.LINK + +.COMPONENT + DevSetup.Providers.PowerShell + +.FUNCTIONALITY + Package Management, Batch Uninstallation, Configuration Processing, Module Management +#> + +Function Uninstall-PowershellModules { + Param( + [Parameter(Mandatory=$true, Position=0)] + [ValidateNotNullOrEmpty()] + [PSCustomObject]$YamlData + ) + + try { + # Check if PowerShell modules dependencies exist + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.powershell -or -not $YamlData.devsetup.dependencies.powershell.modules) { + Write-Warning "PowerShell modules not found in YAML configuration. Skipping uninstallation." + return $false + } + + $modules = $YamlData.devsetup.dependencies.powershell.modules + + # Get global scope setting from YAML, default to CurrentUser + $globalScope = if ($YamlData.devsetup.dependencies.powershell.scope) { + $YamlData.devsetup.dependencies.powershell.scope + } else { + 'CurrentUser' + } + + # Check if running as administrator when global scope is AllUsers + if ($globalScope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { + throw "PowerShell module uninstallation to AllUsers scope requires administrator privileges. Please run as administrator or set powershellModuleScope to CurrentUser." + } + + Write-StatusMessage "- Uninstalling PowerShell modules from configuration:" -ForegroundColor Cyan + + $moduleCount = 0 + + foreach ($module in $modules) { + if (-not $module) { continue } + + $moduleCount++ + + # Normalize module to object format + if ($module -is [string]) { + $moduleObj = @{ name = $module } + } else { + $moduleObj = $module + } + + # Validate module name + if ([string]::IsNullOrEmpty($moduleObj.name)) { + Write-Warning "Module entry #$moduleCount has no name specified, skipping" + continue + } + + # Determine scope for this module (module-specific overrides global) + $moduleScope = if ($moduleObj.scope) { $moduleObj.scope } else { $globalScope } + + # Set defaults and build parameters + $installParams = @{ + ModuleName = $moduleObj.name + } + + if ($moduleObj.minimumVersion) { + Write-StatusMessage "- Uninstalling PowerShell module: $($moduleObj.name) (version: $($moduleObj.minimumVersion), scope: $moduleScope)" -ForegroundColor Gray -Width 100 -NoNewLine -Indent 2 + } else { + Write-StatusMessage "- Uninstalling PowerShell module: $($moduleObj.name) (latest version) to $moduleScope scope" -ForegroundColor Gray -Width 100 -NoNewLine -Indent 2 + } + + if ((Uninstall-PowerShellModule @installParams)) { + Write-StatusMessage "[OK]" -ForegroundColor Green + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } + } + Write-StatusMessage "- PowerShell modules uninstallation completed! Processed $moduleCount modules." -ForegroundColor Green + Write-Host "" + return $true + } + catch { + Write-Error "Error uninstalling PowerShell modules: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 new file mode 100644 index 0000000..c45ee0b --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 @@ -0,0 +1,107 @@ +BeforeAll { + function ConvertTo-Yaml { } + . $PSScriptRoot\Export-InstalledScoopPackages.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 + Mock Test-ScoopInstalled { $true } + Mock Find-Scoop { "scoop" } + Mock Invoke-Expression { '{"buckets":[{"Name":"extras","Source":"https://github.com/ScoopInstaller/Extras"}],"apps":[{"Name":"git","Version":"2.40.0","Source":"extras","Info":"Global install"}]}' } + Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @(); buckets = @() } } } } } + Mock ConvertTo-Yaml { param($obj) "yaml-output" } + Mock ConvertTo-Json { param($obj) "json-output" } + Mock Out-File { $true } + Mock Write-Host { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Write-Debug { } + Mock Write-Verbose { } +} + +Describe "Export-InstalledScoopPackages" { + + Context "When Scoop is not installed" { + It "Should warn and return false" { + Mock Test-ScoopInstalled { $false } + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Scoop is not installed" } + } + } + + Context "When Scoop command is not found" { + It "Should warn and return false" { + Mock Find-Scoop { $null } + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to find Scoop command" } + } + } + + Context "When no Scoop packages are found" { + It "Should warn and return true" { + Mock Invoke-Expression { $null } + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No Scoop packages found" } + } + } + + Context "When Scoop export JSON is invalid" { + It "Should warn and show raw output" { + Mock Invoke-Expression { "not-json" } + Mock ConvertFrom-Json { throw "JSON error" } + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to parse scoop export JSON" } + } + } + + Context "When buckets and packages are found" { + It "Should add buckets and packages to YAML data" { + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeTrue + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding bucket: extras" } + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding package: git" } + } + } + + Context "When DryRun is used" { + It "Should display YAML output and not write to file" { + $result = Export-InstalledScoopPackages -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Times 0 -Scope It + Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } + } + } + + Context "When OutFile is specified" { + It "Should write YAML output to the specified file" { + $result = Export-InstalledScoopPackages -Config "test.yaml" -OutFile "out.yaml" + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Yaml -Scope It + Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } + } + } + + Context "When YAML conversion fails" { + It "Should fallback to JSON output" { + Mock ConvertTo-Yaml { throw "YAML error" } + $result = Export-InstalledScoopPackages -Config "test.yaml" -DryRun + $result | Should -BeTrue + Assert-MockCalled ConvertTo-Json -Scope It + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } + } + } + + Context "When Out-File fails" { + It "Should write error and return false" { + Mock Out-File { throw "File error" } + $result = Export-InstalledScoopPackages -Config "test.yaml" + $result | Should -BeFalse + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 new file mode 100644 index 0000000..db2107d --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 @@ -0,0 +1,412 @@ +<# +.SYNOPSIS + Exports installed Scoop packages and buckets to a YAML configuration file. + +.DESCRIPTION + This function scans the system for installed Scoop packages and buckets, then exports them to a YAML + configuration file in DevSetup format. It uses 'scoop export' to retrieve comprehensive package information + including versions, buckets, and global installation status. The function can update existing configuration + files by merging new packages with existing ones, or create new configurations from scratch. + +.PARAMETER Config + The path to the YAML configuration file to read from and write to. + This parameter is mandatory and specifies both the input and output file unless OutFile is specified. + +.PARAMETER OutFile + The path to save the updated YAML configuration. + Optional parameter that allows saving to a different file than the input Config file. + +.PARAMETER DryRun + Switch parameter that prevents writing to files and displays the resulting configuration to the console. + Useful for previewing changes before committing them to a file. + +.OUTPUTS + [System.Boolean] + Returns $true if the export completes successfully or if Scoop is not installed (skipped). + Returns $false if there are errors during the export process. + +.EXAMPLE + Export-InstalledScoopPackages -Config "environment.yaml" + + Exports installed Scoop packages to the existing environment.yaml configuration file. + +.EXAMPLE + Export-InstalledScoopPackages -Config "current.yaml" -OutFile "backup.yaml" + + Reads from current.yaml and saves the updated configuration with installed packages to backup.yaml. + +.EXAMPLE + Export-InstalledScoopPackages -Config "dev-env.yaml" -DryRun + + Shows what the configuration would look like without actually saving to file. + +.NOTES + - Requires Scoop to be installed on the system (gracefully skips if not found) + - Uses 'scoop export' command to retrieve package and bucket information in JSON format + - Handles both local and global package installations using Info field detection + - Automatically skips the 'main' bucket as it's installed by default with Scoop + - Merges with existing YAML configuration, preserving other sections and structure + - Supports both simple string format and complex object format for packages and buckets + - Updates existing packages/buckets when versions or sources have changed + - Tracks global installation status and bucket information for each package + - Provides detailed console output with color-coded status messages for all operations + - Creates the devsetup.dependencies.scoop structure if it doesn't exist + - Processes buckets before packages to ensure proper dependency order + - Converts string entries to hashtable format when additional properties are needed + - Preserves existing package properties while updating changed values + - Includes comprehensive error handling for JSON parsing and file operations + - Returns $true even when no packages are found (successful empty result) + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Configuration Export, Package Discovery, YAML Generation +#> + +Function Export-InstalledScoopPackages { + Param( + [Parameter(Mandatory = $true)] + [string]$Config, + [string]$OutFile, + [switch]$DryRun + ) + + try { + # Check if Scoop is installed + if(-Not (Test-ScoopInstalled)) { + Write-Warning "Scoop is not installed. Cannot check for components." + return $false + } + + $scoopCommand = Find-Scoop + if (-not $scoopCommand) { + Write-Warning "Failed to find Scoop command. Cannot check for components." + return $false + } + + # Get list of installed Scoop packages + Write-Host "- Getting list of installed Scoop packages..." -ForegroundColor Gray + + # Get all packages (both local and global) using scoop export + $scoopListLocal = "" + + try { + # Use scoop export - it returns JSON with both local and global packages + $command = "& '$scoopCommand' export" + $scoopListLocal = Invoke-Expression $command 6>$null + if (-not $scoopListLocal) { + Write-Warning "No Scoop packages found or scoop export command failed." + return $true + } + } catch { + Write-Verbose "Could not get Scoop packages: $_" + } + + # TODO: + # scoop kinda sucks, they do so many weird things, for instance scoop install helm works fine and produces what you'd expect + # scoop install main/helm, totally kills what the source was in scoop list and provides a path to a json file + # scoop install main/helm@3.17.4 is even worse, it provides for the source + # in order to make sure we dont have problems with exported configurations we need to "look up" each package and see what bucket + # it actually belongs in while exporting so when someone imports it back in later, it provides a valid bucket to install from + # scoop search '^helm$' + + $scoopPackages = @() + $scoopBuckets = @() + + # Parse packages from scoop export JSON + if ($scoopListLocal) { + try { + # Convert JSON output to PowerShell object + $exportData = $scoopListLocal | ConvertFrom-Json + + # Parse buckets from the JSON structure + if ($exportData.buckets -and $exportData.buckets.Count -gt 0) { + foreach ($bucket in $exportData.buckets) { + # Skip the 'main' bucket as it's automatically installed with Scoop + if ($bucket.Name -eq "main") { + Write-Debug "Skipping 'main' bucket (automatically installed with Scoop)" + continue + } + + $bucketInfo = @{ + name = $bucket.Name + source = $bucket.Source + } + $scoopBuckets += $bucketInfo + Write-Debug "Found bucket: $($bucket.Name) (source: $($bucket.Source))" + } + } + + # Parse apps from the JSON structure + if ($exportData.apps -and $exportData.apps.Count -gt 0) { + foreach ($app in $exportData.apps) { + $packageName = $app.Name + $version = $app.Version + $bucket = $app.Source + + # Determine if this is a global install based on the Info field + $isGlobal = $app.Info -eq "Global install" + + Write-Debug "Found package from JSON export: $packageName (version: $version, bucket: $bucket, global: $isGlobal)" + $packageInfo = @{ + name = $packageName + version = $version + global = $isGlobal + } + + # Always include bucket information for clarity + if ($bucket) { + $packageInfo.bucket = $bucket + } + + $scoopPackages += $packageInfo + } + } else { + Write-Verbose "No apps found in scoop export JSON" + } + } catch { + Write-Warning "Failed to parse scoop export JSON: $_" + Write-Verbose "Raw export output: $scoopListLocal" + } + } + + if ($scoopPackages.Count -eq 0) { + Write-Warning "No Scoop packages found." + return $true + } + + Write-Debug "Found $($scoopPackages.Count) Scoop packages and $($scoopBuckets.Count) buckets" + + # Read existing YAML configuration + $YamlData = Read-ConfigurationFile -Config $Config + + # Ensure scoopPackages and scoopBuckets sections exist + if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } + if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } + if (-not $YamlData.devsetup.dependencies.scoop) { $YamlData.devsetup.dependencies.scoop = @{} } + if (-not $YamlData.devsetup.dependencies.scoop.packages) { $YamlData.devsetup.dependencies.scoop.packages = @() } + if (-not $YamlData.devsetup.dependencies.scoop.buckets) { $YamlData.devsetup.dependencies.scoop.buckets = @() } + + # Add buckets to YAML data first (packages may depend on these buckets) + foreach ($bucket in $scoopBuckets) { + # Check if bucket already exists + $existingBucket = $YamlData.devsetup.dependencies.scoop.buckets | Where-Object { + ($_ -is [string] -and $_ -eq $bucket.name) -or + ($_ -is [hashtable] -and $_.name -eq $bucket.name) + } + + if (-not $existingBucket) { + Write-Host " - Adding bucket: $($bucket.name) ($($bucket.source))" -ForegroundColor Gray + + # Create bucket object + $bucketObj = @{ + name = $bucket.name + source = $bucket.source + } + + $YamlData.devsetup.dependencies.scoop.buckets += $bucketObj + } else { + # Bucket exists, check if source has changed + $existingSource = $null + + if ($existingBucket -is [hashtable]) { + $existingSource = $existingBucket.source + } + + if ($existingSource -and $existingSource -ne $bucket.source) { + Write-Host " - Updating bucket: $($bucket.name) ($existingSource -> $($bucket.source))" -ForegroundColor Cyan + + # Find index and update + $index = $YamlData.devsetup.dependencies.scoop.buckets.IndexOf($existingBucket) + + if ($existingBucket -is [string]) { + # Convert string to hashtable with source + $bucketObj = @{ + name = $bucket.name + source = $bucket.source + } + + $YamlData.devsetup.dependencies.scoop.buckets[$index] = $bucketObj + } else { + # Update existing hashtable + $YamlData.devsetup.dependencies.scoop.buckets[$index].source = $bucket.source + } + } elseif (-not $existingSource) { + Write-Host " - Updating bucket: $($bucket.name)" -ForegroundColor Yellow + + # Find index and add source + $index = $YamlData.devsetup.dependencies.scoop.buckets.IndexOf($existingBucket) + + if ($existingBucket -is [string]) { + # Convert string to hashtable with source + $bucketObj = @{ + name = $bucket.name + source = $bucket.source + } + + $YamlData.devsetup.dependencies.scoop.buckets[$index] = $bucketObj + } else { + # Add source to existing hashtable + $YamlData.devsetup.dependencies.scoop.buckets[$index].source = $bucket.source + } + } else { + Write-Host " - Skipping bucket (No Change): $($bucket.name) ($($bucket.source))" -ForegroundColor Gray + } + } + } + + # Add packages to YAML data + foreach ($package in $scoopPackages) { + # Check if package already exists + $existingPackage = $YamlData.devsetup.dependencies.scoop.packages | Where-Object { + ($_ -is [string] -and $_ -eq $package.name) -or + ($_ -is [hashtable] -and $_.name -eq $package.name) + } + + if (-not $existingPackage) { + Write-Host " - Adding package: $($package.name) ($($package.version))" -ForegroundColor Gray + + # Create package object with all relevant properties + $packageObj = @{ + name = $package.name + version = $package.version + } + + if ($package.bucket) { + $packageObj.bucket = $package.bucket + } + + if ($package.global) { + $packageObj.global = $package.global + } + + $YamlData.devsetup.dependencies.scoop.packages += $packageObj + } else { + # Package exists, check if version has changed + $existingVersion = $null + $existingGlobal = $false + $existingBucket = $null + + if ($existingPackage -is [hashtable]) { + $existingVersion = $existingPackage.version + $existingGlobal = $existingPackage.global + $existingBucket = $existingPackage.bucket + } + + if ($existingVersion -and $existingVersion -ne $package.version) { + Write-Host " - Updating package: $($package.name) ($existingVersion -> $($package.version))" -ForegroundColor Cyan + + # Find index and update + $index = $YamlData.devsetup.dependencies.scoop.packages.IndexOf($existingPackage) + + # Preserve existing package structure but update version + if ($existingPackage -is [string]) { + # Convert string to hashtable with version and other properties + $packageObj = @{ + name = $package.name + version = $package.version + } + + if ($package.bucket) { + $packageObj.bucket = $package.bucket + } + + if ($package.global) { + $packageObj.global = $package.global + } + + $YamlData.devsetup.dependencies.scoop.packages[$index] = $packageObj + } else { + # Update existing hashtable + $YamlData.devsetup.dependencies.scoop.packages[$index].version = $package.version + + # Update bucket if changed + if ($package.bucket -and (-not $existingBucket -or $existingBucket -ne $package.bucket)) { + $YamlData.devsetup.dependencies.scoop.packages[$index].bucket = $package.bucket + } + + # Update global flag if changed + if ($package.global -ne $existingGlobal) { + $YamlData.devsetup.dependencies.scoop.packages[$index].global = $package.global + } + } + } elseif (-not $existingVersion) { + Write-Host " - Updating package: $($package.name)" -ForegroundColor Yellow + + # Find index and add version and other properties + $index = $YamlData.devsetup.dependencies.scoop.packages.IndexOf($existingPackage) + + if ($existingPackage -is [string]) { + # Convert string to hashtable with version and properties + $packageObj = @{ + name = $package.name + version = $package.version + } + + if ($package.bucket) { + $packageObj.bucket = $package.bucket + } + + if ($package.global) { + $packageObj.global = $package.global + } + + $YamlData.devsetup.dependencies.scoop.packages[$index] = $packageObj + } else { + # Add version and other properties to existing hashtable + $YamlData.devsetup.dependencies.scoop.packages[$index].version = $package.version + + if ($package.bucket -and -not $existingBucket) { + $YamlData.devsetup.dependencies.scoop.packages[$index].bucket = $package.bucket + } + + if ($package.global -and -not $existingGlobal) { + $YamlData.devsetup.dependencies.scoop.packages[$index].global = $package.global + } + } + } else { + Write-Host " - Skipping package (No Change): $($package.name) ($($package.version))" -ForegroundColor Gray + } + } + } + + # Convert to YAML + try { + $yamlOutput = $YamlData | ConvertTo-Yaml + } + catch { + Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" + $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 + } + + # Handle output based on parameters + if ($DryRun) { + Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan + Write-Host $yamlOutput -ForegroundColor White + Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow + } else { + # Determine output file + $outputFile = if ($OutFile) { $OutFile } else { $Config } + + try { + Write-Debug "`nSaving configuration to: $outputFile" + $yamlOutput | Out-File -FilePath $outputFile + Write-Debug "Configuration saved successfully!" + } + catch { + Write-Error "Failed to save configuration to $outputFile`: $_" + return $false + } + } + + Write-Host "Scoop packages conversion completed!" -ForegroundColor Green + return $true + } + catch { + Write-Error "Error converting Scoop packages: $_" + return $false + } +} diff --git a/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 new file mode 100644 index 0000000..8b5c4b7 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 @@ -0,0 +1,66 @@ +BeforeAll { + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 +} + +Describe "Find-Scoop" { + Context "When scoop is found by Get-Command" { + BeforeEach { + Mock Get-Command { return 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' } + } + It "should return scoop" { + $scoop = Find-Scoop + $scoop | Should -Be "scoop" + } + } + + Context "When scoop is not found by Get-Command or any other option it should return null" { + BeforeEach { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + } + It "should return null" { + $scoop = Find-Scoop + $scoop | Should -BeNullOrEmpty + } + } + + Context "When scoop is not found by Get-Command but scoop.ps1 is found" { + BeforeEach { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null + Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' -Value 'Scoop PowerShell Script' + } + It "should return TestDrive:\Users\Test User\scoop\shims\scoop.ps1" { + $scoop = Find-Scoop + $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.ps1" + } + } + + Context "When scoop is not found by Get-Command but scoop.cmd is found" { + BeforeEach { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null + Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.cmd' -Value 'Scoop Command Script' + } + It "should return TestDrive:\Users\Test User\scoop\shims\scoop.cmd" { + $scoop = Find-Scoop + $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.cmd" + } + } + + Context "When scoop is not found by Get-Command but scoop is found" { + BeforeEach { + Mock Get-Command { return $null } + Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null + Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop' -Value 'Scoop Command Script' + } + It "should return TestDrive:\Users\Test User\scoop\shims\scoop" { + $scoop = Find-Scoop + $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Find-Scoop.ps1 b/DevSetup/Private/Providers/Scoop/Find-Scoop.ps1 new file mode 100644 index 0000000..2acb5ee --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Find-Scoop.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + Locates the Scoop package manager executable on the system. + +.DESCRIPTION + This function searches for the Scoop package manager executable using multiple detection methods. + It first attempts to find 'scoop' in the system PATH, and if not found, searches for Scoop + installation files in the default user profile directory. The function returns the appropriate + command or file path that can be used to execute Scoop operations. + +.OUTPUTS + [System.String] + Returns "scoop" if found in PATH, the full file path to the Scoop executable if found in the user profile, + or $null if Scoop cannot be located. + +.EXAMPLE + Find-Scoop + + Locates the Scoop executable on the current system. + +.EXAMPLE + $scoopCommand = Find-Scoop + if ($scoopCommand) { + & $scoopCommand list + } else { + Write-Warning "Scoop not found" + } + + Demonstrates using the returned command to execute Scoop operations. + +.EXAMPLE + switch (Find-Scoop) { + "scoop" { "Scoop found in PATH" } + { $_ -like "*scoop.ps1" } { "Found PowerShell script: $_" } + { $_ -like "*scoop.cmd" } { "Found batch file: $_" } + { $_ -like "*scoop" } { "Found executable: $_" } + $null { "Scoop not found" } + } + + Shows handling different types of Scoop installations. + +.NOTES + - Performs multiple checks to locate Scoop installations: + 1. Checks if 'scoop' command is available in PATH using Get-Command + 2. Searches for ~\scoop\shims\scoop.ps1 (PowerShell script) + 3. Searches for ~\scoop\shims\scoop.cmd (Command batch file) + 4. Searches for ~\scoop\shims\scoop (Executable) + - Returns the most accessible form first (PATH command before file paths) + - Suppresses errors when checking for the scoop command to avoid console output + - The returned value can be used directly with the call operator (&) or Invoke-Expression + - Does not verify that the found executable is functional, only that it exists + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Executable Location, Path Resolution +#> +Function Find-Scoop { + [CmdletBinding()] + Param () + + if (Get-Command scoop -ErrorAction SilentlyContinue) { + return "scoop" + } else { + # Check for Scoop in user profile directory + $userProfilePath = (Get-EnvironmentVariable USERPROFILE) + $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop.ps1" + if (Test-Path $scoopPath) { + return $scoopPath + } + + $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop.cmd" + if (Test-Path $scoopPath) { + return $scoopPath + } + + $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop" + if (Test-Path $scoopPath) { + return $scoopPath + } + } + return $null +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 new file mode 100644 index 0000000..617a9d7 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 @@ -0,0 +1,16 @@ +BeforeAll { + . $PSScriptRoot\Get-ScoopCacheFile.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1 +} + +Describe "Get-ScoopCacheFile" { + Context "When scoop is found by Get-Command" { + BeforeEach { + Mock Get-DevSetupCachePath { return 'TestDrive:\Users\Test User\.devsetup\.cache' } + } + It "should return the correct scoop cache file path" { + $scoopCacheFile = Get-ScoopCacheFile + $scoopCacheFile | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache\scoop.cache" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 new file mode 100644 index 0000000..755c406 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 @@ -0,0 +1,61 @@ +<# +.SYNOPSIS + Gets the file path for the Scoop package cache file. + +.DESCRIPTION + This function constructs and returns the full path to the Scoop package cache file within the DevSetup + cache directory. The cache file is used to store information about installed Scoop packages and their + versions for performance optimization and offline reference. The function uses Get-DevSetupCachePath + to ensure the cache directory exists before returning the file path. + +.OUTPUTS + [System.String] + Returns the full path to the Scoop cache file (scoop.cache) within the DevSetup cache directory. + +.EXAMPLE + Get-ScoopCacheFile + + Returns the path to the Scoop cache file, e.g., "C:\Users\Username\.devsetup\.cache\scoop.cache" + +.EXAMPLE + $scoopCacheFile = Get-ScoopCacheFile + if (Test-Path $scoopCacheFile) { + $cachedData = Get-Content $scoopCacheFile + } + + Gets the cache file path and checks if it exists before reading cached data. + +.EXAMPLE + $cacheFile = Get-ScoopCacheFile + Export-Clixml -Path $cacheFile -InputObject $scoopPackages + + Uses the cache file path to save Scoop package information. + +.NOTES + - Uses Get-DevSetupCachePath to ensure the cache directory exists + - Returns a consistent file path (scoop.cache) within the DevSetup cache structure + - The cache file is used for storing Scoop package metadata and version information + - Does not create the cache file itself - only returns the path where it should be located + - Used by other Scoop-related functions for performance optimization and data persistence + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Path Management, Cache Management, File System Operations +#> + +Function Get-ScoopCacheFile { + [CmdletBinding()] + Param() + + # Get the DevSetup cache path + $cachePath = Get-DevSetupCachePath + + # Construct the full path to the cache file + $cacheFilePath = Join-Path -Path $cachePath -ChildPath "scoop.cache" + + return $cacheFilePath +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 new file mode 100644 index 0000000..2e1807d --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 @@ -0,0 +1,112 @@ +BeforeAll { + . $PSScriptRoot\Get-ScoopVersion.ps1 + . $PSScriptRoot\Find-Scoop.ps1 +} + +Describe "Get-ScoopVersion" { + Context "When scoop is not found" { + BeforeEach { + Mock Find-Scoop { return $null } + } + It "should return null" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be $null + } + } + + Context "When scoop is found but returns no version info" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + return '' + } + Mock Get-Content { + return $null + } + Mock Remove-Item { + return $null + } + } + It "should return null" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be $null + } + } + + Context "When scoop is found and returns version info" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + return "b588a06e (HEAD -> master, origin/master, origin/HEAD) chore(release): Bump to version 0.5.3 (resync) (#6436) +945371469 (HEAD -> master, origin/master, origin/HEAD) tailwindcss: Update to version 4.1.12 +7ecbe6adcc (HEAD -> master, origin/master, origin/HEAD) processing4: Update to version 1306-4.4.6" + } + Mock Remove-Item {} + } + It "should return the scoop version" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be "0.5.3" + } + } + + Context "When scoop is found and returns git hash info" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + return "b588a06e (HEAD -> master, origin/master, origin/HEAD) chore(release): +945371469 (HEAD -> master, origin/master, origin/HEAD) tailwindcss: +7ecbe6adcc (HEAD -> master, origin/master, origin/HEAD) processing4:" + } + Mock Remove-Item {} + } + It "should return the scoop git hash" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be "b588a06e" + } + } + + Context "When scoop is found and but the version format changed" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + return "(HEAD -> master, origin/master, origin/HEAD) chore(release): +(HEAD -> master, origin/master, origin/HEAD) tailwindcss: +(HEAD -> master, origin/master, origin/HEAD) processing4:" + } + Mock Remove-Item {} + } + It "should return installed" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be "installed" + } + } + + Context "When scoop is found and but the version format changed to not using git release rendering" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + return "Current Scoop version: +v0.5.3 - Released at 2025-08-11" + } + Mock Remove-Item {} + } + It "should return the scoop version" { + $scoopVersion = Get-ScoopVersion + $scoopVersion | Should -Be "0.5.3" + } + } + + Context "When scoop is found and but an error is thrown" { + BeforeEach { + Mock Find-Scoop { return 'scoop' } + Mock Invoke-Expression { + throw "This is a sample error" + } + Mock Remove-Item {} + } + It "should return installed" { + $scoopVersion = Get-ScoopVersion -WarningAction SilentlyContinue + $scoopVersion | Should -Be "installed" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 b/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 new file mode 100644 index 0000000..b619966 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 @@ -0,0 +1,103 @@ +<# +.SYNOPSIS + Retrieves the version information for the installed Scoop package manager. + +.DESCRIPTION + This function queries the installed Scoop package manager to determine its version. It uses the 'scoop --version' + command and parses the output to extract version information. The function handles both tagged releases + (e.g., "v0.5.3") and development builds identified by commit hashes. Output is completely suppressed during + execution to avoid console clutter. + +.OUTPUTS + [System.String] + Returns the Scoop version string if found, "installed" if version cannot be determined but Scoop is present, + or $null if Scoop is not installed or cannot be found. + +.EXAMPLE + Get-ScoopVersion + + Retrieves the version of the currently installed Scoop package manager. + +.EXAMPLE + $version = Get-ScoopVersion + if ($version) { + Write-Host "Scoop version: $version" + } else { + Write-Host "Scoop is not installed" + } + + Demonstrates checking if Scoop is installed and displaying its version. + +.EXAMPLE + switch (Get-ScoopVersion) { + $null { "Scoop not found" } + "installed" { "Scoop is installed but version unknown" } + default { "Scoop version: $_" } + } + + Shows handling different return scenarios from the function. + +.NOTES + - Requires Scoop to be installed and accessible via Find-Scoop function + - Uses Start-Process with output redirection to completely suppress console output + - Parses version output with two fallback strategies: + 1. Tagged release format: "v0.5.3 - Released at..." + 2. Development build format: "ebd8c036 (HEAD -> master..." + - Creates temporary files for output capture which are automatically cleaned up + - Returns "installed" if Scoop responds but version cannot be parsed + - Returns $null if Scoop is not found or accessible + - Handles errors gracefully without stopping execution + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Version Detection, System Information +#> +Function Get-ScoopVersion { + [CmdletBinding()] + Param () + + $scoopVersion = $null + $scoopCommand = Find-Scoop + if ($scoopCommand) { + try { + # Use Start-Process with PowerShell to completely suppress console output + # Scoop is a PowerShell script, so we always need to run it through PowerShell + $command = "& '$scoopCommand' --version" + + $scoopVersionOutput = Invoke-Expression $command 6>$null + + if ($scoopVersionOutput) { + # Try to find version tag format first (e.g., "v0.5.3 - Released at...") + $outputLines = $scoopVersionOutput -split "`n" | Where-Object { $_ -and $_.Trim() } + $versionLine = $outputLines | Where-Object { $_ -match "[0-9]+\-?[0-9]?\.[0-9]+\.[0-9]+" } | Select-Object -First 1 + + if ($versionLine) { + if ($versionLine -match "([0-9]+\-?[0-9]?\.[0-9]+\.[0-9]+)") { + $scoopVersion = $matches[1] + } + } else { + # Fallback to commit hash format (e.g., "ebd8c036 (HEAD -> master...") + $hashLine = $outputLines | Where-Object { $_ -match "^[a-f0-9]{8,12}" } | Select-Object -First 1 + if ($hashLine) { + $hashParts = $hashLine.Split(' ') + if ($hashParts -and $hashParts.Length -gt 0) { + $scoopVersion = $hashParts[0] # Get just the commit hash + } + } else { + $scoopVersion = "installed" + } + } + } + } catch { + Write-Warning "Could not get Scoop version: $_" + $scoopVersion = "installed" + } + return $scoopVersion + } else { + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 new file mode 100644 index 0000000..ff0990f --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 @@ -0,0 +1,56 @@ +BeforeAll { + . $PSScriptRoot\Install-Scoop.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Get-ScoopVersion.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } +} + +Describe "Install-Scoop" { + Context "When scoop is installed" { + BeforeEach { + Mock Get-ScoopVersion { return '0.5.3' } + Mock Test-ScoopInstalled { return $true } + } + It "Should return true" { + Install-Scoop | Should -Be $true + } + } + + Context "When scoop is installed but the version cant be found" { + BeforeEach { + Mock Get-ScoopVersion { return $null } + Mock Test-ScoopInstalled { return $true } + } + + It "Should return false" { + Install-Scoop | Should -Be $false + } + } + + Context "When scoop is not installed" { + BeforeEach { + Mock Test-ScoopInstalled { return $false } + Mock Get-ScoopVersion { return '0.5.3' } + Mock Set-ExecutionPolicy { return $true } + Mock Invoke-RestMethod { return $true } + Mock Invoke-Expression { return $true } + } + It "Should install it and return true" { + Install-Scoop | Should -Be $true + } + } + + Context "When scoop is not installed" { + BeforeEach { + Mock Test-ScoopInstalled { return $false } + Mock Get-ScoopVersion { return '0.5.3' } + Mock Set-ExecutionPolicy { return $true } + Mock Invoke-RestMethod { return $true } + Mock Invoke-Expression { throw "Failed" } + } + It "Should try to install it and throw an error when it fails" { + { Install-Scoop } | Should -Throw "Failed to install scoop: Failed" + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-Scoop.ps1 b/DevSetup/Private/Providers/Scoop/Install-Scoop.ps1 new file mode 100644 index 0000000..5baa4cf --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-Scoop.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Installs the Scoop package manager on the system. + +.DESCRIPTION + This function installs Scoop package manager by downloading and executing the official installation script + from get.scoop.sh. It automatically configures PowerShell execution policy settings and validates the + installation success. The function performs pre-installation checks to avoid duplicate installations + and uses Get-ScoopVersion to verify successful installation completion. + +.OUTPUTS + [System.Boolean] + Returns $true if Scoop was successfully installed or was already installed. + Returns $false if the installation verification fails. + +.EXAMPLE + Install-Scoop + + Installs Scoop package manager on the current system. + +.EXAMPLE + if (-not (Test-ScoopInstalled)) { + Install-Scoop + Write-Host "Scoop is now available for package management" + } + + Shows conditional installation only when Scoop is not already present. + +.NOTES + **Installation Process:** + - Checks if Scoop is already installed using Test-ScoopInstalled + - Sets execution policy to RemoteSigned for script download + - Downloads and executes installation script from get.scoop.sh with -RunAs parameter + - Sets execution policy to Bypass after installation + - Verifies installation using Get-ScoopVersion + + **Requirements:** + - Internet connection to download the installation script + - PowerShell execution policy modification permissions + + **Installation Method:** + - Uses `Invoke-RestMethod get.scoop.sh` to download the installation script + - Executes with `-RunAs` parameter for non-elevated user installation from elevated PowerShell + - Automatically handles execution policy configuration (RemoteSigned → Bypass) + + **Verification:** + - Uses Get-ScoopVersion to confirm successful installation + - Returns boolean based on version retrieval success + - Performs same verification check whether installing or if already installed + + **Error Handling:** + - Throws exception if installation script execution fails + - Uses SilentlyContinue for execution policy to avoid errors + - Suppresses installation output using Out-Null for clean console experience + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Manager Installation, System Setup +#> +Function Install-Scoop { + [CmdletBinding()] + Param () + + Write-StatusMessage "- Installing Scoop package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + if(-not (Test-ScoopInstalled)) { + try { + Invoke-Expression "& {$(Invoke-RestMethod get.scoop.sh)} -RunAs" | Out-Null + } catch { + throw "Failed to install scoop: $_" + } + } + + $scoopVersion = Get-ScoopVersion + if(-not ([string]::IsNullOrEmpty($scoopVersion))) { + Write-StatusMessage "[OK]" -ForegroundColor Green + return $true + } else { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 new file mode 100644 index 0000000..24a4ac9 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 @@ -0,0 +1,90 @@ +BeforeAll { + . $PSScriptRoot\Install-ScoopBucket.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 +} + +Describe "Install-ScoopBucket" { + Context "When scoop is not installed" { + It "Should return false" { + Mock Test-ScoopInstalled { return $false } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + Context "When scoop is not found" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return $null } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + Context "When a Bucket is already installed" { + It "Should return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $true + } + } + Context "When a Bucket is not already installed and it fails to install it" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Invoke-Command { + $global:LASTEXITCODE = 1 + return $null + } -Verifiable + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + + Context "When a Bucket is not already installed and it gets installed but fails to write the cache" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return $null + } -Verifiable + Mock Write-ScoopCache { return $false } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + Context "When a Bucket is not already installed and installing it causes an error to be thrown" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Invoke-Command { + throw 'Failed' + } -Verifiable + Mock Write-ScoopCache { return $true } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + Context "When a Bucket is not already installed and it gets installed and writes the cache" { + It "Should return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Invoke-Command { + $global:LASTEXITCODE = 0 + return $null + } -Verifiable + Mock Write-ScoopCache { return $true } + $result = Install-ScoopBucket -Name "extras" + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 new file mode 100644 index 0000000..d37f655 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 @@ -0,0 +1,116 @@ +<# +.SYNOPSIS + Adds a Scoop bucket to the system. + +.DESCRIPTION + This function adds a specified Scoop bucket by executing the 'scoop bucket add' command. + It includes validation to ensure Scoop is installed and available before attempting the bucket addition. + The function supports adding both official buckets (by name only) and custom buckets (with source URL). + It checks if the bucket is already installed before attempting to add it and provides error handling + with a boolean result indicating success or failure. + +.PARAMETER Name + The name of the Scoop bucket to add. + This parameter is mandatory and must be a valid string representing a bucket name. + +.PARAMETER Source + The source URL or Git repository for the bucket. + Optional parameter used for adding custom buckets. If not specified, Scoop will attempt to add an official bucket by name. + +.OUTPUTS + [System.Boolean] + Returns $true if the bucket was successfully added or is already installed, $false if the operation failed. + +.EXAMPLE + Install-ScoopBucket -Name "extras" + + Adds the official 'extras' bucket to Scoop. + +.EXAMPLE + Install-ScoopBucket -Name "nonportable" + + Adds the official 'nonportable' bucket to Scoop. + +.EXAMPLE + Install-ScoopBucket -Name "custom-bucket" -Source "https://github.com/user/scoop-bucket" + + Adds a custom bucket from a GitHub repository. + +.EXAMPLE + $result = Install-ScoopBucket -Name "games" + if ($result) { + Write-Host "Games bucket added successfully" + } else { + Write-Host "Failed to add games bucket" + } + + Demonstrates capturing the return value to check bucket addition success. + +.NOTES + - Requires Scoop to be installed on the system + - Uses Test-ScoopComponentInstalled to check if bucket is already installed before attempting to add it + - Returns $true if bucket is already installed (considered successful since goal is achieved) + - Returns $false immediately if Scoop is not installed or cannot be found + - Uses $LASTEXITCODE to verify command execution success + - Provides warning messages for common failure scenarios + - Uses try-catch error handling for robust failure management + - Official buckets can be added by name only (extras, nonportable, games, etc.) + - Custom buckets require both name and source URL parameters + - Suppresses command output using Out-Null to avoid console clutter + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Bucket Management, Repository Addition +#> +Function Install-ScoopBucket { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$Name, + [string]$Source + ) + + if(-Not (Test-ScoopInstalled)) { + return $false + } + + $scoopCommand = Find-Scoop + if (-Not ($scoopCommand)) { + return $false + } + + try { + [InstalledState]$bucketState = Test-ScoopComponentInstalled -Bucket -Name $Name + if ($bucketState -ne [InstalledState]::Pass) { + $installArgs = @("bucket", "add", $Name) + + # If a source is provided, add it to the command arguments + if ($Source) { + $installArgs += $Source + } + + # Execute the command to add the bucket + $command = { + & $scoopCommand @installArgs *> $null + } + Invoke-Command -ScriptBlock $command | Out-Null + if ($LASTEXITCODE -ne 0) { + return $false + } + + if (-not (Write-ScoopCache)) { + return $false + } + + return $true + } else { + return $true + } + } catch { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 new file mode 100644 index 0000000..d59c1c3 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 @@ -0,0 +1,123 @@ +BeforeAll { + . $PSScriptRoot\Install-ScoopComponents.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\Install-ScoopBucket.ps1 + . $PSScriptRoot\Install-ScoopPackage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } + Mock Write-Host {} + Mock Write-Error {} +} + +Describe "Install-ScoopComponents" { + + Context "When Scoop is not installed" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras") + packages = @("git") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When Scoop configuration is missing" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When Write-ScoopCache fails" { + It "Should return false and error" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras") + packages = @("git") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When only buckets are present and all install succeed" { + It "Should return true and process all buckets" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Install-ScoopBucket { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ buckets = @("extras", "versions") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When only packages are present and all install succeed" { + It "Should return true and process all packages" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Install-ScoopPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When buckets and packages are present and some installs fail" { + It "Should return true and report failures" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + $bucketCallCount = 0 + Mock Install-ScoopBucket -MockWith { + $bucketCallCount++ + if ($bucketCallCount -eq 1) { return $false } else { return $true } + } + $packageCallCount = 0 + Mock Install-ScoopPackage -MockWith { + $packageCallCount++ + if ($packageCallCount -eq 2) { return $false } else { return $true } + } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras", "versions") + packages = @("git", "nodejs") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When no buckets or packages are present" { + It "Should return true and skip package installation" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When an exception occurs during package install" { + It "Should catch and continue, returning true" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Install-ScoopBucket { return $true } + Mock Install-ScoopPackage { throw "Unexpected error" } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When an exception occurs in the main try block" { + It "Should return false" { + Mock Test-ScoopInstalled { throw "Critical error" } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras") + packages = @("git") } } } } + $result = Install-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 new file mode 100644 index 0000000..699d8da --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 @@ -0,0 +1,256 @@ +<# +.SYNOPSIS + Installs Scoop buckets and packages from YAML configuration data. + +.DESCRIPTION + This function processes YAML configuration data to install Scoop buckets and packages in sequence. + It validates Scoop installation, updates the cache before proceeding, and processes buckets before + packages to ensure bucket availability. The function supports both simple string formats and complex + object formats for buckets and packages, allowing for detailed configuration including versions, + custom sources, and global installation scope. Progress is tracked and reported for both buckets + and packages using color-coded status messages. + +.PARAMETER YamlData + The YAML configuration data containing Scoop bucket and package definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.scoop.buckets and/or devsetup.dependencies.scoop.packages + +.OUTPUTS + [System.Boolean] + Returns $false if Scoop is not installed, cannot be found, configuration is invalid, or cache update fails. + Returns $true if installation completes successfully (even if individual items fail). + +.EXAMPLE + $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml + Install-ScoopComponents -YamlData $yamlData + + Installs Scoop buckets and packages from a YAML configuration file. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + scoop = @{ + buckets = @( + "extras", + @{ + name = "custom-bucket" + source = "https://github.com/user/scoop-bucket" + } + ) + packages = @( + "git", + @{ + name = "nodejs" + version = "18.17.0" + }, + @{ + name = "7zip" + global = $true + }, + @{ + name = "firefox" + bucket = "extras" + } + ) + } + } + } + } + Install-ScoopComponents -YamlData $yamlData + + Demonstrates the PSCustomObject structure and installs the configured components. + +.EXAMPLE + if (Install-ScoopComponents -YamlData $config) { + Write-Host "Scoop components installation completed" + } else { + Write-Host "Scoop components installation failed" + } + + Shows checking the return value to verify installation completion. + +.NOTES + - Requires Scoop to be installed on the system using Test-ScoopInstalled + - Returns $false immediately if Scoop is not installed or cannot be found + - Returns $false if YAML configuration structure is invalid or missing scoop section + - Updates Scoop cache using Write-ScoopCache before installation begins + - Returns $false if cache update fails to ensure accurate installation state + - Processes buckets before packages to ensure bucket availability for package installations + - Gracefully handles missing buckets or packages sections in configuration + - Supports two bucket specification formats: + * Simple string: "bucketname" + * Complex object: @{ name = "bucketname"; source = "https://github.com/user/scoop-bucket" } + - Supports two package specification formats: + * Simple string: "packagename" + * Complex object: @{ name = "packagename"; version = "1.0.0"; bucket = "extras"; global = $true } + - Validates component names and skips entries with missing names + - Uses Install-ScoopBucket and Install-ScoopPackage functions for actual installation + - Provides detailed progress reporting with component counts and property information + - Uses color-coded console output: Cyan for headers, Gray for items, Green/Red for status + - Displays formatted component information including version, bucket, and global flags + - Continues processing remaining components even if individual installations fail + - Returns $true for overall success even with individual component failures + - Includes comprehensive try-catch error handling with descriptive error messages + - Tracks and reports separate counts for buckets and packages processed + +.LINK + +.COMPONENT + DevSetup.Scoop + +.FUNCTIONALITY + Bulk Installation, Configuration Processing, Package Management +#> +Function Install-ScoopComponents { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [PSCustomObject]$YamlData + ) + + try { + if(-Not (Test-ScoopInstalled)) { + Write-StatusMessage "Scoop is not installed. Cannot check for components." -Verbosity "Warning" + return $false + } + + # Check if scoop packages exist in configuration + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.scoop) { + Write-StatusMessage "Scoop configuration not found in YAML. Skipping installation." -Verbosity "Warning" + return $false + } + + if (-not (Write-ScoopCache)) { + Write-Error "Failed to write Scoop cache file: $CacheFilePath" + return $false + } + + $bucketCount = 0 + Write-StatusMessage "- Installing Scoop buckets from configuration:" -ForegroundColor Cyan + # Handle buckets first if they exist in configuration + if ($YamlData.devsetup.dependencies.scoop.buckets) { + foreach ($bucket in $YamlData.devsetup.dependencies.scoop.buckets) { + if (-not $bucket) { continue } + + # Handle both string format and object format + $bucketName = if ($bucket -is [string]) { $bucket } else { $bucket.name } + $bucketSource = if ($bucket -is [hashtable] -and $bucket.source) { $bucket.source } else { $null } + + $installParams = @{ + Name = $bucketName + } + + if ($bucketSource) { + $installParams.Source = $bucketSource + } + + # Use Install-ScoopBucket function to handle bucket installation + if ($bucketName -and $bucketSource) { + Write-StatusMessage "- Adding Scoop bucket: $bucketName (source: $bucketSource)" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine + } else { + Write-StatusMessage "- Adding Scoop bucket: $bucketName" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine + } + + $installationStatus = Install-ScoopBucket @installParams + + if (-not $installationStatus) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } else { + $bucketCount++ + Write-StatusMessage "[OK]" -ForegroundColor Green + } + } + } + + Write-StatusMessage "- Scoop buckets installation completed! Processed $bucketCount buckets." -ForegroundColor Green + + Write-Host "" + + # Check if scoop packages exist in configuration + if (-not $YamlData.devsetup.dependencies.scoop.packages) { + Write-StatusMessage "Scoop packages not found in YAML configuration. Skipping package installation." -Verbosity "Warning" + return $true + } + + $scoopPackages = $YamlData.devsetup.dependencies.scoop.packages + Write-StatusMessage "- Installing Scoop packages from configuration:" -ForegroundColor Cyan + + $packageCount = 0 + + # Install packages + foreach ($package in $scoopPackages) { + if (-not $package) { continue } + + $packageCount++ + + # Normalize package to object format + if ($package -is [string]) { + $packageObj = @{ name = $package } + } else { + $packageObj = $package + } + + # Validate package name + if ([string]::IsNullOrEmpty($packageObj.name)) { + Write-StatusMessage "- Skipping package entry, No name specified" -Verbosity "Warning" -Indent 2 -Width 112 + continue + } + + # Use Install-ScoopPackage function to handle the installation + try { + $displayName = $packageObj.name + $installParams = @{ + PackageName = $packageObj.name + } + + $versionDisplay = "" + if ($packageObj.version) { + $versionDisplay = "version: $($packageObj.version)" + $installParams.Version = $packageObj.version + } + + $bucketDisplay = "" + if ($packageObj.bucket) { + $bucketDisplay = "bucket: '$($packageObj.bucket)'" + $installParams.Bucket = $packageObj.bucket + } + + $globalDisplay = "" + if ($packageObj.global -eq $true) { + $globalDisplay = "global: true" + $installParams.Global = $true + } else { + $installParams.Global = $false + } + + if($versionDisplay -or $bucketDisplay -or $globalDisplay) { + $parts = @($versionDisplay, $bucketDisplay, $globalDisplay) | Where-Object { $_ } + $displayName += " (" + ($parts -join ", ") + ")" + } + Write-StatusMessage "- Installing Scoop package: $displayName" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine + + ($result = Install-ScoopPackage @installParams) | Out-Null + + if (-not $result) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } else { + Write-StatusMessage "[OK]" -ForegroundColor Green + } + } catch { + Write-StatusMessage "Failed to install Scoop package '$($packageObj.name)': $_" -Verbosity "Error" + continue + } + } + + Write-StatusMessage "- Scoop packages installation completed! Processed $packageCount packages." -ForegroundColor Green + + Write-Host "" + + return $true + } + catch { + Write-StatusMessage "Error installing Scoop packages: $_" -Verbosity "Error" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 new file mode 100644 index 0000000..7c3f0df --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 @@ -0,0 +1,121 @@ +BeforeAll { + . $PSScriptRoot\Install-ScoopPackage.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 + . $PSScriptRoot\Uninstall-ScoopPackage.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\Read-ScoopCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 +} + +Describe "Install-ScoopPackage" { + + Context "When Scoop is not installed" { + It "Should return false" { + Mock Test-ScoopInstalled { return $false } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When Scoop command cannot be found" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return $null } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When package is already installed with correct version and scope" { + It "Should return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $true + } + } + + Context "When package is installed but version/scope does not match" { + It "Should uninstall and reinstall the package" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + $callCount = 0 + Mock Test-ScoopComponentInstalled -MockWith { + $callCount++ + if ($callCount -eq 1) { [InstalledState]::Installed } + else { [InstalledState]::Pass } + } + Mock Read-ScoopCache { + return @{ + apps = @( + [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } + ) + } + } + Mock Uninstall-ScoopPackage { return $true } + Mock Invoke-Command { $global:LASTEXITCODE = 0 } + Mock Write-ScoopCache { return $true } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $true + } + } + + Context "When install command fails" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Uninstall-ScoopPackage { return $true } + Mock Invoke-Command { $global:LASTEXITCODE = 1 } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When Write-ScoopCache fails after install" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Uninstall-ScoopPackage { return $true } + Mock Invoke-Command { $global:LASTEXITCODE = 0 } + Mock Write-ScoopCache { return $false } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When installing with version, bucket, and global" { + It "Should pass correct arguments and return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + $callCount = 0 + Mock Test-ScoopComponentInstalled -MockWith { + $callCount++ + if ($callCount -eq 1) { [InstalledState]::Installed } + else { [InstalledState]::Pass } + } + Mock Uninstall-ScoopPackage { return $true } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + # Optionally, check the arguments passed to scoop + return $null + } + Mock Write-ScoopCache { return $true } + $result = Install-ScoopPackage -PackageName "python" -Version "3.11.5" -Bucket "main" -Global + $result | Should -Be $true + } + } + + Context "When an exception occurs" { + It "Should return false" { + Mock Test-ScoopInstalled { throw "Unexpected error" } + $result = Install-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 b/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 new file mode 100644 index 0000000..9d8a1c5 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 @@ -0,0 +1,163 @@ +<# +.SYNOPSIS + Installs a Scoop package on the system. + +.DESCRIPTION + This function installs a specified Scoop package by executing the 'scoop install' command. + It includes validation to ensure Scoop is installed and available before attempting the installation. + The function supports package versioning, bucket specification, and global installation scope. + If the package is already installed and the global scope matches, it will be uninstalled first to ensure a clean installation. + The function verifies successful installation using Test-ScoopComponentInstalled with version and scope validation. + +.PARAMETER PackageName + The name of the Scoop package to install. + This parameter is mandatory and must be a valid string representing a Scoop package. + +.PARAMETER Version + The specific version of the package to install. + Optional parameter that appends version specification to the package name (e.g., "package@1.2.3"). + +.PARAMETER Bucket + The bucket name where the package is located. + Optional parameter that prepends bucket specification to the package name (e.g., "extras/package"). + +.PARAMETER Global + Switch parameter to install the package globally. + When specified, adds the --global flag to the scoop install command. + +.OUTPUTS + [System.Boolean] + Returns $true if the package was successfully installed and verified, $false if the installation failed. + +.EXAMPLE + Install-ScoopPackage -PackageName "git" + + Installs the 'git' package from the main bucket. + +.EXAMPLE + Install-ScoopPackage -PackageName "nodejs" -Version "18.17.0" + + Installs a specific version of the 'nodejs' package. + +.EXAMPLE + Install-ScoopPackage -PackageName "7zip" -Global + + Installs the '7zip' package globally for all users. + +.EXAMPLE + Install-ScoopPackage -PackageName "firefox" -Bucket "extras" + + Installs the 'firefox' package from the 'extras' bucket. + +.EXAMPLE + Install-ScoopPackage -PackageName "python" -Version "3.11.5" -Bucket "main" -Global + + Installs a specific version of Python from the main bucket globally. + +.NOTES + - Requires Scoop to be installed on the system + - Only uninstalls existing package if it's already installed AND global scope matches exactly + - Uses Test-ScoopComponentInstalled to verify installation success with version and scope validation + - Supports bucket/package@version syntax for package specification + - Returns $false immediately if Scoop is not installed or cannot be found + - Provides detailed warning and error messages for failure scenarios + - Uses proper argument splatting for reliable command execution + - Includes comprehensive try-catch error handling for robust failure management + - Installation verification checks name, version (if specified), and global scope (if specified) + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Management, Package Installation +#> +Function Install-ScoopPackage { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$PackageName, + + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Version, + + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string]$Bucket, + + [Parameter(Mandatory=$false)] + [switch]$Global + ) + + try { + if(-Not (Test-ScoopInstalled)) { + return $false + } + + $scoopCommand = Find-Scoop + if (-not $scoopCommand) { + return $false + } + + $Params = @{ + Package = $true + Name = $PackageName + } + + if($PSBoundParameters.ContainsKey('Version') -and $Version) { + $Params.Version = $Version + } + + if($Global) { + $Params.Global = $Global + } + + [InstalledState]$packageState = Test-ScoopComponentInstalled @Params + if ($packageState -eq [InstalledState]::Pass) { + Write-Debug "Scoop package '$PackageName' is already installed with the specified version and global scope." + return $true + } + + if($packageState.HasFlag([InstalledState]::Installed)) { + Write-Debug "Scoop package '$PackageName' is installed but does not meet the global scope and/or version requirements. Reinstalling..." + Uninstall-ScoopPackage -PackageName $PackageName | Out-null + } + + $fullPackageName = $PackageName + if ($PSBoundParameters.ContainsKey('Bucket')) { + $fullPackageName = "$Bucket/$PackageName" + } + + # Add version if specified + if ($PSBoundParameters.ContainsKey('Version')) { + $fullPackageName += "@$Version" + } + + # Build arguments array for installation + $installArgs = @("install", $fullPackageName) + + # Add global flag if specified + if ($Global) { + $installArgs += "--global" + } + + # Execute the install command with proper argument parsing + $command = { + & $scoopCommand @installArgs *> $null + } + + Invoke-Command -ScriptBlock $command | Out-Null + if ($LASTEXITCODE -ne 0) { + return $false + } + + if (-not (Write-ScoopCache)) { + return $false + } + return Test-ScoopComponentInstalled @Params + } catch { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 new file mode 100644 index 0000000..aae6571 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 @@ -0,0 +1,68 @@ +BeforeAll { + . $PSScriptRoot\Read-ScoopCache.ps1 + . $PSScriptRoot\Get-ScoopCacheFile.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 +} + +Describe "Read-ScoopCache" { + + Context "When cache file exists and contains valid JSON" { + It "Should return deserialized object" { + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Test-Path { return $true } + $json = '{"apps":[{"name":"git","version":"2.42.0"}]}' + Mock Get-Content { $json } + $result = Read-ScoopCache + $result.apps[0].name | Should -Be "git" + $result.apps[0].version | Should -Be "2.42.0" + } + } + + Context "When cache file does not exist and Write-ScoopCache succeeds" { + It "Should create cache and return deserialized object" { + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + $testPathCallCount = 0 + Mock Test-Path -MockWith { + $testPathCallCount++ + if ($testPathCallCount -eq 1) { return $false } + else { return $true } + } + Mock Write-ScoopCache { return $true } + $json = '{"apps":[{"name":"git","version":"2.42.0"}]}' + Mock Get-Content { $json } + # Do NOT mock ConvertFrom-Json here! + $result = Read-ScoopCache + $result.apps[0].name | Should -Be "git" + } + } + + Context "When cache file does not exist and Write-ScoopCache fails" { + It "Should throw an exception" { + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Test-Path { return $false } + Mock Write-ScoopCache { return $false } + { Read-ScoopCache } | Should -Throw "Failed to create Scoop cache file: C:\fakepath\scoop.cache" + } + } + + Context "When cache file contains invalid JSON" { + It "Should return null and write error" { + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Test-Path { return $true } + Mock Get-Content { return "not-json" } + Mock ConvertFrom-Json { throw "Invalid JSON" } + $result = Read-ScoopCache + $result | Should -Be $null + } + } + + Context "When Get-Content throws an exception" { + It "Should return null and write error" { + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Test-Path { return $true } + Mock Get-Content { throw "File read error" } + $result = Read-ScoopCache + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Read-ScoopCache.ps1 b/DevSetup/Private/Providers/Scoop/Read-ScoopCache.ps1 new file mode 100644 index 0000000..5b619a9 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Read-ScoopCache.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + Reads cached Scoop package information from the DevSetup cache file. + +.DESCRIPTION + This function reads and deserializes cached Scoop package data from the DevSetup cache system. + It automatically handles cache file creation if the file doesn't exist by calling Write-ScoopCache, + and provides comprehensive error handling for file operations and JSON parsing. The function + returns the cached data as a PowerShell object for use by other Scoop-related functions. + +.OUTPUTS + [System.Object] + Returns the deserialized cache data as a PowerShell object if successful. + Returns $null if the cache file cannot be read or parsed. + +.EXAMPLE + Read-ScoopCache + + Reads the Scoop cache data and returns it as a PowerShell object. + +.EXAMPLE + $scoopCache = Read-ScoopCache + if ($scoopCache) { + Write-Host "Found $($scoopCache.Count) cached packages" + } else { + Write-Host "No cache data available" + } + + Demonstrates reading cache data and checking for successful retrieval. + +.EXAMPLE + $cachedPackages = Read-ScoopCache + $gitPackage = $cachedPackages | Where-Object { $_.name -eq "git" } + + Shows reading cache data and filtering for specific package information. + +.NOTES + - Uses Get-ScoopCacheFile to determine the cache file location + - Automatically creates cache file if it doesn't exist using Write-ScoopCache + - Throws an exception if cache file creation fails + - Uses ConvertFrom-Json to deserialize the cached data + - Provides comprehensive error handling for both file operations and JSON parsing + - Returns $null on any error to allow calling functions to handle gracefully + - Used by other Scoop functions to avoid repeated system queries for performance + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Cache Management, Data Deserialization, Performance Optimization +#> + +Function Read-ScoopCache { + [CmdletBinding()] + Param() + + $CacheFilePath = Get-ScoopCacheFile + + if (-Not (Test-Path $CacheFilePath)) { + Write-Debug "Scoop cache file not found: $CacheFilePath" + if (-not (Write-ScoopCache)) { + throw "Failed to create Scoop cache file: $CacheFilePath" + } + } + + try { + $cacheData = Get-Content $CacheFilePath | ConvertFrom-Json + return $cacheData + } catch { + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 new file mode 100644 index 0000000..bb7e421 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 @@ -0,0 +1,135 @@ +BeforeAll { + . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 + . $PSScriptRoot\Read-ScoopCache.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 +} + +Describe "Test-ScoopComponentInstalled" { + + Context "When Scoop is not installed" { + It "Should return NotInstalled for package" { + Mock Test-ScoopInstalled { return $false } + $result = Test-ScoopComponentInstalled -Package -Name "git" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + It "Should return NotInstalled for bucket" { + Mock Test-ScoopInstalled { return $false } + $result = Test-ScoopComponentInstalled -Bucket -Name "extras" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } + + Context "When cache cannot be read" { + It "Should return NotInstalled for package" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { return $null } + $result = Test-ScoopComponentInstalled -Package -Name "git" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + It "Should return NotInstalled for bucket" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { return $null } + $result = Test-ScoopComponentInstalled -Bucket -Name "extras" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } + + Context "When package is not found in cache" { + It "Should return NotInstalled" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { return @{ apps = @(); buckets = @() } } + $result = Test-ScoopComponentInstalled -Package -Name "git" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } + + Context "When bucket is not found in cache" { + It "Should return NotInstalled" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { return @{ apps = @(); buckets = @() } } + $result = Test-ScoopComponentInstalled -Bucket -Name "extras" + $result | Should -BeExactly ([InstalledState]::NotInstalled) + } + } + + Context "When package is found without version or global" { + It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { + return @{ + apps = @( + [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } + ) + } + } + $result = Test-ScoopComponentInstalled -Package -Name "git" + $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When package is found with matching version" { + It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { + return @{ + apps = @( + [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } + ) + } + } + $result = Test-ScoopComponentInstalled -Package -Name "git" -Version "2.42.0" + $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When package is found but version does not match" { + It "Should return Installed + GlobalVersionMet" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { + return @{ + apps = @( + [PSCustomObject]@{ Name = "git"; Version = "2.41.0"; Info = "Local Install" } + ) + } + } + $result = Test-ScoopComponentInstalled -Package -Name "git" -Version "2.42.0" + $expected = [InstalledState]::Installed + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When package is found with Global switch and global install" { + It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { + return @{ + apps = @( + [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Global Install" } + ) + } + } + $result = Test-ScoopComponentInstalled -Package -Name "git" -Global + $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } + + Context "When bucket is found in cache" { + It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { + Mock Test-ScoopInstalled { return $true } + Mock Read-ScoopCache { + return @{ + buckets = @( + [PSCustomObject]@{ Name = "extras" } + ) + } + } + $result = Test-ScoopComponentInstalled -Bucket -Name "extras" + $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet + $result | Should -BeExactly $expected + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 new file mode 100644 index 0000000..0e29fcc --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 @@ -0,0 +1,155 @@ +<# +.SYNOPSIS + Tests whether a Scoop package or bucket is installed on the system. + +.DESCRIPTION + Checks if a specified Scoop package or bucket is installed by querying Scoop export data. + For packages, verifies installation status, version match, and global/local scope. + For buckets, verifies if the bucket is present in the Scoop configuration. + +.PARAMETER Package + Indicates checking for a package installation. + Cannot be used with `-Bucket`. + +.PARAMETER Bucket + Indicates checking for a bucket installation. + Cannot be used with `-Package`. + +.PARAMETER Name + The name of the package or bucket to check. + Required for all parameter sets. + +.PARAMETER Version + The specific version to check for when validating package installation. + Optional for package checks; not applicable for bucket checks. + +.PARAMETER Global + Specifies checking for global package installation. + Optional for package checks; not applicable for bucket checks. + +.OUTPUTS + `[InstalledState]` + Returns an InstalledState enum value indicating installation status and version match. + Returns `[InstalledState]::NotInstalled` if not found or Scoop is unavailable. + +.EXAMPLE + Test-ScoopComponentInstalled -Package -Name "git" + # Checks if the 'git' package is installed via Scoop. + +.EXAMPLE + Test-ScoopComponentInstalled -Package -Name "nodejs" -Version "18.17.0" + # Checks if the 'nodejs' package version 18.17.0 is installed via Scoop. + +.EXAMPLE + Test-ScoopComponentInstalled -Package -Name "7zip" -Global + # Checks if the '7zip' package is installed globally via Scoop. + +.EXAMPLE + Test-ScoopComponentInstalled -Bucket -Name "extras" + # Checks if the 'extras' bucket is added to Scoop. + +.NOTES + **Requirements:** + - Scoop must be installed. + - Uses `Read-ScoopCache` for cached export data. + + **Behavior:** + - Returns `[InstalledState]::NotInstalled` if Scoop is not installed. + - For packages, checks name, version, and global install status. + - For buckets, checks if the bucket name exists in the configuration. + - Returns an InstalledState enum value for detailed status. + + **Error Handling:** + - Provides debug and warning messages for missing Scoop or cache data. + - Returns `[InstalledState]::NotInstalled` for missing components. + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Management, Installation Verification +#> +Function Test-ScoopComponentInstalled { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] + [switch]$Package, + + [Parameter(Mandatory=$true, ParameterSetName='BucketCheck')] + [switch]$Bucket, + + [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] + [Parameter(Mandatory=$true, ParameterSetName='BucketCheck')] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] + [ValidateNotNullOrEmpty()] + [string]$Version, + + [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] + [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] + [switch]$Global + ) + + if(-Not (Test-ScoopInstalled)) { + return [InstalledState]::NotInstalled + } + + $scoopComponents = Read-ScoopCache + if (-not $scoopComponents) { + return [InstalledState]::NotInstalled + } + if ($Package) { + $packageName = $Name + [InstalledState]$packageState = [InstalledState]::NotInstalled + if ($scoopComponents.apps) { + $scoopComponents.apps | ForEach-Object { + if ($_.Name -eq $packageName) { + $packageState += [InstalledState]::Installed + if ($PSBoundParameters.ContainsKey('Version')) { + if([Version]$_.Version -eq [Version]$Version) { + $packageState += [InstalledState]::RequiredVersionMet + $packageState += [InstalledState]::MinimumVersionMet + } + } else { + $packageState += [InstalledState]::RequiredVersionMet + $packageState += [InstalledState]::MinimumVersionMet + } + + if($Global) { + if ($_.Info -eq "Global Install") { + $packageState += [InstalledState]::GlobalVersionMet + } + } else { + $packageState += [InstalledState]::GlobalVersionMet + } + } + } + } + return $packageState + } elseif ($Bucket) { + [InstalledState]$bucketState = [InstalledState]::NotInstalled + if ($scoopComponents.buckets) { + $scoopComponents.buckets | ForEach-Object { + if ($_.Name -eq $Name) { + Write-Debug "Scoop bucket '$Name' is installed." + $bucketState += [InstalledState]::Installed + $bucketState += [InstalledState]::MinimumVersionMet + $bucketState += [InstalledState]::RequiredVersionMet + $bucketState += [InstalledState]::GlobalVersionMet + } + } + } + return $bucketState + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 new file mode 100644 index 0000000..2cd4774 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 @@ -0,0 +1,50 @@ +BeforeAll { + . $PSScriptRoot\Test-ScoopInstalled.ps1 +} + +Describe "Test-ScoopInstalled" { + + Context "When scoop command is available in PATH" { + It "Should return true" { + Mock Get-Command { return @{ Name = "scoop" } } + $result = Test-ScoopInstalled + $result | Should -Be $true + } + } + + Context "When scoop command is not available but scoop.ps1 exists" { + It "Should return true" { + Mock Get-Command { return $null } + Mock Test-Path { param($path) if ($path -like "*scoop.ps1") { return $true } else { return $false } } + $result = Test-ScoopInstalled + $result | Should -Be $true + } + } + + Context "When scoop command is not available but scoop.cmd exists" { + It "Should return true" { + Mock Get-Command { return $null } + Mock Test-Path { param($path) if ($path -like "*scoop.cmd") { return $true } else { return $false } } + $result = Test-ScoopInstalled + $result | Should -Be $true + } + } + + Context "When scoop command is not available but scoop executable exists" { + It "Should return true" { + Mock Get-Command { return $null } + Mock Test-Path { param($path) if ($path -like "*scoop") { return $true } else { return $false } } + $result = Test-ScoopInstalled + $result | Should -Be $true + } + } + + Context "When scoop is not installed at all" { + It "Should return false" { + Mock Get-Command { return $null } + Mock Test-Path { return $false } + $result = Test-ScoopInstalled + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 new file mode 100644 index 0000000..047594e --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + Tests whether Scoop package manager is installed on the system. + +.DESCRIPTION + This function checks if Scoop is installed and available on the system by first attempting to locate + the scoop command in the PATH, and if not found, checking for Scoop installation files in the default + user profile directory. It provides a comprehensive check for both standard installations and cases + where Scoop may not be properly added to the PATH environment variable. + +.OUTPUTS + [System.Boolean] + Returns $true if Scoop is installed and available, $false otherwise. + +.EXAMPLE + Test-ScoopInstalled + + Checks if Scoop is installed on the current system. + +.EXAMPLE + if (Test-ScoopInstalled) { + Write-Host "Scoop is available" + # Proceed with Scoop operations + } else { + Write-Host "Scoop is not installed" + # Install Scoop or handle the missing dependency + } + + Demonstrates using the function result to conditionally execute Scoop-dependent code. + +.EXAMPLE + $scoopAvailable = Test-ScoopInstalled + switch ($scoopAvailable) { + $true { "Scoop package manager detected" } + $false { "Scoop package manager not found" } + } + + Shows capturing the boolean result for later use. + +.NOTES + - Performs multiple checks to ensure reliable detection + - First checks if 'scoop' command is available in PATH using Get-Command + - Falls back to checking specific file paths in the user profile directory: + * ~\scoop\shims\scoop.ps1 (PowerShell script) + * ~\scoop\shims\scoop.cmd (Command batch file) + * ~\scoop\shims\scoop (Executable) + - Does not verify that Scoop is functional, only that installation files exist + - Suppresses errors when checking for the scoop command to avoid console output + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Installation Detection, Environment Validation +#> +Function Test-ScoopInstalled { + [CmdletBinding()] + Param () + + if (Get-Command scoop -ErrorAction SilentlyContinue) { + return $true + } else { + # Check for Scoop in user profile directory + $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.ps1" + if (Test-Path $scoopPath) { + return $true + } + + $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.cmd" + if (Test-Path $scoopPath) { + return $true + } + + $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop" + if (Test-Path $scoopPath) { + return $true + } + } + return $false +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 new file mode 100644 index 0000000..bb9d66f --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 @@ -0,0 +1,84 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-ScoopBucket.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 +} + +Describe "Uninstall-ScoopBucket" { + + Context "When Scoop is not installed" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $false } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + + Context "When Scoop command cannot be found" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return $null } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + + Context "When bucket is already uninstalled" { + It "Should return true and debug" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $true + } + } + + Context "When bucket uninstall command fails" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Write-ScoopCache { return $true } + Mock Invoke-Expression { $global:LASTEXITCODE = 1 } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + + Context "When Write-ScoopCache fails after uninstall" { + It "Should return false and error" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Write-ScoopCache { return $false } + Mock Invoke-Expression { $global:LASTEXITCODE = 0 } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } + + Context "When bucket is successfully uninstalled" { + It "Should return true and debug" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } + Mock Write-ScoopCache { return $true } + Mock Invoke-Expression { $global:LASTEXITCODE = 0 } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $true + } + } + + Context "When an exception occurs during uninstall" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { throw "Unexpected error" } + $result = Uninstall-ScoopBucket -Name "extras" + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 new file mode 100644 index 0000000..14c2e15 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + Uninstalls a Scoop bucket from the system. + +.DESCRIPTION + This function removes a Scoop bucket using the 'scoop bucket rm' command. It validates + Scoop installation, locates the Scoop command, and checks if the bucket is currently + installed before attempting removal. The function provides comprehensive error handling + and updates the Scoop cache after successful removal operations. + +.PARAMETER Name + The name of the Scoop bucket to uninstall. + This parameter is mandatory and must be a valid, non-empty string representing an installed Scoop bucket name. + +.OUTPUTS + [System.Boolean] + Returns $true if the bucket is successfully uninstalled or already uninstalled. + Returns $false if the uninstallation fails or Scoop is not available. + +.EXAMPLE + Uninstall-ScoopBucket -Name "extras" + + Uninstalls the "extras" bucket from Scoop. + +.EXAMPLE + $result = Uninstall-ScoopBucket -Name "java" + if ($result) { + Write-Host "Java bucket removed successfully" + } else { + Write-Host "Failed to remove Java bucket" + } + + Demonstrates capturing the return value to check uninstallation success. + +.EXAMPLE + @("extras", "versions", "java") | ForEach-Object { + Uninstall-ScoopBucket -Name $_ + } + + Shows bulk uninstallation of multiple Scoop buckets. + +.NOTES + - Requires Scoop to be installed on the system + - Uses Test-ScoopInstalled to validate Scoop availability + - Uses Find-Scoop to locate the Scoop command executable + - Returns $false immediately if Scoop is not available or cannot be found + - Uses Test-ScoopComponentInstalled to check if bucket is currently installed + - Returns $true if bucket is already uninstalled (idempotent behavior) + - Executes 'scoop bucket rm' command with output suppression + - Uses $LASTEXITCODE to verify command execution success + - Updates Scoop cache using Write-ScoopCache after successful removal + - Provides debug logging for successful and skipped operations + - Includes comprehensive try-catch error handling with descriptive error messages + - Suppresses all command output using *> $null to avoid console clutter + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Management, Bucket Management, Repository Removal +#> + +Function Uninstall-ScoopBucket { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$Name + ) + + if(-Not (Test-ScoopInstalled)) { + return $false + } + + $scoopCommand = Find-Scoop + if (-not $scoopCommand) { + return $false + } + + try { + $bucketState = Test-ScoopComponentInstalled -Bucket -Name $Name + if (-not ($bucketState.HasFlag([InstalledState]::Pass))) { + # If a source is provided, add it to the command arguments + Write-Debug "Removing Scoop bucket: $Name without source" + + # Execute the command to add the bucket + Invoke-Expression "& $scoopCommand bucket rm $Name" *> $null + if ($LASTEXITCODE -ne 0) { + return $false + } + + if (-not (Write-ScoopCache)) { + return $false + } + + Write-Debug "Scoop bucket '$Name' removed successfully." + return $true + } else { + Write-Debug "Scoop bucket '$Name' is already uninstalled." + return $true + } + } catch { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 new file mode 100644 index 0000000..f5e5d05 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 @@ -0,0 +1,137 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-ScoopComponents.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\Uninstall-ScoopBucket.ps1 + . $PSScriptRoot\Uninstall-ScoopPackage.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 + Mock Write-StatusMessage { } + Mock Write-Host {} + Mock Write-Error {} +} + +Describe "Uninstall-ScoopComponents" { + + Context "When Scoop is not installed" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras") + packages = @("git") } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When Scoop configuration is missing" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When Write-ScoopCache fails" { + It "Should return false and error" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $false } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ + buckets = @("extras") + packages = @("git") } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } + + Context "When only buckets are present and all uninstall succeed" { + It "Should return true and process all buckets" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Uninstall-ScoopBucket { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ buckets = @("extras", "versions") } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When only packages are present and all uninstall succeed" { + It "Should return true and process all packages" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Uninstall-ScoopPackage { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When buckets and packages are present and some uninstalls fail" { + It "Should return true and report failures" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + $bucketCallCount = 0 + Mock Uninstall-ScoopBucket -MockWith { + $bucketCallCount++ + if ($bucketCallCount -eq 1) { return $false } else { return $true } + } + $packageCallCount = 0 + Mock Uninstall-ScoopPackage -MockWith { + $packageCallCount++ + if ($packageCallCount -eq 2) { return $false } else { return $true } + } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + scoop = @{ + buckets = @("extras", "versions") + packages = @("git", "nodejs") + } + } + } + } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When no buckets or packages are present" { + It "Should return true and skip package uninstallation" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When an exception occurs during package uninstall" { + It "Should catch and continue, returning true" { + Mock Test-ScoopInstalled { return $true } + Mock Write-ScoopCache { return $true } + Mock Uninstall-ScoopBucket { return $true } + Mock Uninstall-ScoopPackage { throw "Unexpected error" } + $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $true + } + } + + Context "When an exception occurs in the main try block" { + It "Should return false" { + Mock Test-ScoopInstalled { throw "Critical error" } + $yamlData = @{ + devsetup = @{ + dependencies = @{ + scoop = @{ + buckets = @("extras") + packages = @("git") + } + } + } + } + $result = Uninstall-ScoopComponents -YamlData $yamlData + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 new file mode 100644 index 0000000..d2b2f6e --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 @@ -0,0 +1,225 @@ +<# +.SYNOPSIS + Uninstalls multiple Scoop components (buckets and packages) from the system based on YAML configuration. + +.DESCRIPTION + This function removes multiple Scoop components specified in a DevSetup YAML configuration. + It validates Scoop installation, parses the configuration for bucket and package definitions, + and systematically uninstalls components in the correct order (buckets first, then packages). + The function supports both simple string format and complex object format for component + specifications, handles global installations, and provides comprehensive progress reporting + during the uninstallation process. + +.PARAMETER YamlData + The parsed YAML configuration data containing Scoop component definitions. + This parameter is mandatory and must be a PSCustomObject with the structure: + devsetup.dependencies.scoop containing buckets and/or packages arrays. + +.OUTPUTS + [System.Boolean] + Returns $true if all components are successfully processed (even if some individual uninstalls fail). + Returns $false if the operation encounters critical errors, Scoop is not installed, or cannot proceed. + +.EXAMPLE + $config = Read-ConfigurationFile -Path "environment.yaml" + Uninstall-ScoopComponents -YamlData $config + + Uninstalls all Scoop buckets and packages defined in the environment.yaml configuration. + +.EXAMPLE + $yamlData = @{ + devsetup = @{ + dependencies = @{ + scoop = @{ + buckets = @("extras", "versions") + packages = @("git", "nodejs", "python") + } + } + } + } + Uninstall-ScoopComponents -YamlData $yamlData + + Demonstrates uninstalling components using a programmatically created configuration. + +.EXAMPLE + if (Uninstall-ScoopComponents -YamlData $config) { + Write-Host "All Scoop components processed successfully" + } else { + Write-Host "Scoop component uninstallation encountered errors" + } + + Shows checking the return value to verify uninstallation completion. + +.NOTES + - Requires Scoop to be installed on the system + - Uses Test-ScoopInstalled to validate Scoop availability before proceeding + - Updates Scoop cache using Write-ScoopCache before uninstallation begins + - Processes components in specific order: buckets first, then packages + - Skips uninstallation gracefully if Scoop configuration sections are not found + - Supports two component specification formats for both buckets and packages: + * Simple string: "componentname" + * Complex object: @{ name = "componentname"; version = "1.0.0"; bucket = "extras"; global = $true } + - Bucket objects support: name and source properties + - Package objects support: name, version, bucket, and global properties + - Validates component names and skips entries with missing names + - Uses Uninstall-ScoopBucket and Uninstall-ScoopPackage for individual component removal + - Provides detailed progress reporting with component counts and property information + - Uses color-coded console output: Cyan for progress, Gray for component status, Green/Red for results + - Continues processing remaining components even if individual uninstalls fail + - Returns $true for overall success even with individual component failures + - Includes comprehensive try-catch error handling with descriptive error messages + - Displays formatted component information including version, bucket, and global flags + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Management, Batch Uninstallation, Configuration Processing, Component Management +#> + +Function Uninstall-ScoopComponents { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [PSCustomObject]$YamlData + ) + + try { + if(-Not (Test-ScoopInstalled)) { + Write-StatusMessage "Scoop is not installed. Cannot check for components." -Verbosity "Warning" + return $false + } + + # Check if scoop packages exist in configuration + if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.scoop) { + Write-StatusMessage "Scoop configuration not found in YAML. Skipping uninstallation." -Verbosity "Warning" + return $false + } + + if (-not (Write-ScoopCache)) { + Write-Error "Failed to write Scoop cache file: $CacheFilePath" + return $false + } + + $bucketCount = 0 + # Handle buckets first if they exist in configuration + if ($YamlData.devsetup.dependencies.scoop.buckets) { + Write-StatusMessage "- Uninstalling Scoop buckets from configuration:" -ForegroundColor Cyan + foreach ($bucket in $YamlData.devsetup.dependencies.scoop.buckets) { + if (-not $bucket) { continue } + + # Handle both string format and object format + $bucketName = if ($bucket -is [string]) { $bucket } else { $bucket.name } + $bucketSource = if ($bucket -is [hashtable] -and $bucket.source) { $bucket.source } else { $null } + + $installParams = @{ + Name = $bucketName + } + + # Use Install-ScoopBucket function to handle bucket installation + if ($bucketName -and $bucketSource) { + Write-StatusMessage "- Removing Scoop bucket: $bucketName (source: $bucketSource)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine + } else { + Write-StatusMessage "- Removing Scoop bucket: $bucketName" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine + } + + $installationStatus = Uninstall-ScoopBucket @installParams + + if (-not $installationStatus) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } else { + $bucketCount++ + Write-StatusMessage "[OK]" -ForegroundColor Green + } + } + } + + Write-StatusMessage "- Scoop buckets uninstallation completed! Processed $bucketCount buckets." -ForegroundColor Green + + Write-Host "" + + # Check if scoop packages exist in configuration + if (-not $YamlData.devsetup.dependencies.scoop.packages) { + Write-StatusMessage "Scoop packages not found in YAML configuration. Skipping package uninstallation." -Verbosity "Warning" + return $true + } + + $scoopPackages = $YamlData.devsetup.dependencies.scoop.packages + Write-StatusMessage "- Uninstalling Scoop packages from configuration:" -ForegroundColor Cyan + + $packageCount = 0 + + # Install packages + foreach ($package in $scoopPackages) { + if (-not $package) { continue } + + $packageCount++ + + # Normalize package to object format + if ($package -is [string]) { + $packageObj = @{ name = $package } + } else { + $packageObj = $package + } + + # Validate package name + if ([string]::IsNullOrEmpty($packageObj.name)) { + Write-StatusMessage "- Skipping package entry, No name specified" -Verbosity "Warning" -Indent 2 -Width 100 + continue + } + + # Use Install-ScoopPackage function to handle the installation + try { + $displayName = $packageObj.name + $installParams = @{ + PackageName = $packageObj.name + } + + $versionDisplay = "" + if ($packageObj.version) { + $versionDisplay = "version: $($packageObj.version)" + } + + $bucketDisplay = "" + if ($packageObj.bucket) { + $bucketDisplay = "bucket: '$($packageObj.bucket)'" + } + + $globalDisplay = "" + if ($packageObj.global -eq $true) { + $globalDisplay = "global: true" + $installParams.Global = $true + } + + if($versionDisplay -or $bucketDisplay -or $globalDisplay) { + $parts = @($versionDisplay, $bucketDisplay, $globalDisplay) | Where-Object { $_ } + $displayName += " (" + ($parts -join ", ") + ")" + } + Write-StatusMessage "- Uninstalling Scoop package: $displayName" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine + + $result = Uninstall-ScoopPackage @installParams + + if (-not $result) { + Write-StatusMessage "[FAILED]" -ForegroundColor Red + } else { + Write-StatusMessage "[OK]" -ForegroundColor Green + } + } catch { + Write-StatusMessage "Failed to uninstall Scoop package '$($packageObj.name)': $_" -Verbosity "Error" + continue + } + } + + Write-StatusMessage "- Scoop packages uninstallation completed! Processed $packageCount packages." -ForegroundColor Green + + Write-Host "" + + return $true + } + catch { + Write-StatusMessage "Error uninstalling Scoop packages: $_" -Verbosity "Error" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 new file mode 100644 index 0000000..1c7cc21 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 @@ -0,0 +1,97 @@ +BeforeAll { + . $PSScriptRoot\Uninstall-ScoopPackage.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 + . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 + +} + +Describe "Uninstall-ScoopPackage" { + + Context "When Scoop is not installed" { + It "Should return false" { + Mock Test-ScoopInstalled { return $false } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When Scoop command cannot be found" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return $null } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When package is not installed" { + It "Should return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { + return [InstalledState]::NotInstalled + } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $true + } + } + + Context "When uninstall succeeds" { + It "Should return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { + return [InstalledState]::Pass + } + Mock Invoke-Command { $global:LASTEXITCODE = 0 } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $true + } + } + + Context "When uninstall fails" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { + return [InstalledState]::Pass + } + Mock Invoke-Command { $global:LASTEXITCODE = 1 } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When uninstall throws an exception" { + It "Should return false" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { + return [InstalledState]::Pass + } + Mock Invoke-Command { throw "Unexpected error" } + $result = Uninstall-ScoopPackage -PackageName "git" + $result | Should -Be $false + } + } + + Context "When uninstalling a global package" { + It "Should pass --global and return true" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Test-ScoopComponentInstalled { + return [InstalledState]::Pass + } + Mock Invoke-Command { + param($ScriptBlock) + $global:LASTEXITCODE = 0 + # Optionally, you could inspect $ScriptBlock here + return $null + } + $result = Uninstall-ScoopPackage -PackageName "git" -Global + $result | Should -Be $true + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 new file mode 100644 index 0000000..b411ba6 --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS + Uninstalls a Scoop package from the system. + +.DESCRIPTION + This function removes a specified Scoop package from the system by executing the 'scoop uninstall' command. + It includes validation to ensure Scoop is installed and available before attempting the uninstall operation. + The function checks if the package is installed before attempting removal and provides error handling with + a boolean result indicating success or failure. + +.PARAMETER PackageName + The name of the Scoop package to uninstall. + This parameter is mandatory and must be a valid string representing an installed Scoop package. + +.OUTPUTS + [System.Boolean] + Returns $true if the package was successfully uninstalled or if the package was not installed, + $false if the uninstall operation failed. + +.EXAMPLE + Uninstall-ScoopPackage -PackageName "git" + + Uninstalls the 'git' package from Scoop. + +.EXAMPLE + Uninstall-ScoopPackage -PackageName "nodejs" + + Removes the 'nodejs' package from the system via Scoop. + +.EXAMPLE + $result = Uninstall-ScoopPackage -PackageName "7zip" + if ($result) { + Write-Host "7zip successfully removed or was not installed" + } else { + Write-Host "Failed to remove 7zip" + } + + Demonstrates capturing the return value to check uninstall success. + +.NOTES + - Requires Scoop to be installed on the system + - Uses Test-ScoopPackageInstalled function to verify package existence before uninstall + - Returns $true if package is not installed (considered successful since goal is achieved) + - Returns $false immediately if Scoop is not installed or cannot be found + - Provides warning messages for common failure scenarios + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Package Management, Package Removal +#> +Function Uninstall-ScoopPackage { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true)] + [string]$PackageName, + [switch]$Global + ) + + if(-Not (Test-ScoopInstalled)) { + Write-Debug "Scoop is not installed. Cannot check for components." + return $false + } + + $scoopCommand = Find-Scoop + if (-not $scoopCommand) { + Write-Debug "Failed to find Scoop command. Cannot check for components." + return $false + } + + $packageState = Test-ScoopComponentInstalled -Package -Name $PackageName + if (-not ($packageState.HasFlag([InstalledState]::Pass))) { + Write-Debug "Package not installed, can not remove." + return $true + } + + try { + $uninstallArgs = @('uninstall', $PackageName) + if($Global) { + $uninstallArgs += '--global' + } + + $command = { + & $scoopCommand @uninstallArgs *> $null + } + + Invoke-Command -ScriptBlock $command | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Debug "Uninstalled Scoop package: $PackageName" + return $true + } else { + Write-Debug "Failed to uninstall Scoop package: $PackageName" + return $false + } + } catch { + Write-Debug "Failed to remove Scoop Package: $PackageName" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 b/DevSetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 new file mode 100644 index 0000000..992761e --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 @@ -0,0 +1,63 @@ +BeforeAll { + . $PSScriptRoot\Write-ScoopCache.ps1 + . $PSScriptRoot\Test-ScoopInstalled.ps1 + . $PSScriptRoot\Find-Scoop.ps1 + . $PSScriptRoot\Get-ScoopCacheFile.ps1 +} + +Describe "Write-ScoopCache" { + + Context "When Scoop is not installed" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $false } + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + $result = Write-ScoopCache + $result | Should -Be $false + } + } + + Context "When Scoop command cannot be found" { + It "Should return false and warn" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return $null } + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + $result = Write-ScoopCache + $result | Should -Be $false + } + } + + Context "When cache file is written successfully" { + It "Should return true and debug" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Invoke-Expression { "exported data" } + Mock Set-Content { param($Path, $Value, $Force) return $null } + $result = Write-ScoopCache + $result | Should -Be $true + } + } + + Context "When writing cache file fails" { + It "Should return false and error" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Invoke-Expression { "exported data" } + Mock Set-Content { throw "Failed to write file" } + $result = Write-ScoopCache + $result | Should -Be $false + } + } + + Context "When scoop export throws an exception" { + It "Should return false and error" { + Mock Test-ScoopInstalled { return $true } + Mock Find-Scoop { return "scoop" } + Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } + Mock Invoke-Expression { throw "export failed" } + $result = Write-ScoopCache + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Providers/Scoop/Write-ScoopCache.ps1 b/DevSetup/Private/Providers/Scoop/Write-ScoopCache.ps1 new file mode 100644 index 0000000..c1d978e --- /dev/null +++ b/DevSetup/Private/Providers/Scoop/Write-ScoopCache.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + Writes current Scoop package information to the DevSetup cache file. + +.DESCRIPTION + This function exports the current Scoop package installation data and writes it to the DevSetup + cache file for performance optimization and offline reference. It validates Scoop installation, + locates the Scoop command, and uses 'scoop export' to generate package data before saving it + to the cache file. The function provides comprehensive error handling and validation throughout + the process. + +.OUTPUTS + [System.Boolean] + Returns $true if the cache file is successfully written. + Returns $false if Scoop is not installed, cannot be found, or the write operation fails. + +.EXAMPLE + Write-ScoopCache + + Exports current Scoop packages and writes them to the cache file. + +.EXAMPLE + if (Write-ScoopCache) { + Write-Host "Scoop cache updated successfully" + } else { + Write-Host "Failed to update Scoop cache" + } + + Demonstrates checking the return value to verify cache update success. + +.EXAMPLE + $cacheUpdated = Write-ScoopCache + if ($cacheUpdated) { + $cachedData = Read-ScoopCache + } + + Shows writing cache data and then reading it back for use. + +.NOTES + - Requires Scoop to be installed on the system + - Uses Test-ScoopInstalled to validate Scoop availability + - Uses Find-Scoop to locate the Scoop command executable + - Executes 'scoop export' to generate current package data + - Uses Get-ScoopCacheFile to determine the cache file location + - Overwrites existing cache file using -Force flag + - Provides debug logging for successful cache operations + - Returns $false immediately if Scoop is not available + - Includes comprehensive try-catch error handling for file operations + +.LINK + +.COMPONENT + DevSetup.Providers.Scoop + +.FUNCTIONALITY + Cache Management, Data Serialization, Performance Optimization +#> + +Function Write-ScoopCache { + [CmdletBinding()] + Param() + + $CacheFilePath = Get-ScoopCacheFile + if(-Not (Test-ScoopInstalled)) { + return $false + } + + $scoopCommand = Find-Scoop + if (-not $scoopCommand) { + return $false + } + + try { + Invoke-Expression "& $scoopCommand export" | Set-Content -Path $CacheFilePath -Force | Out-Null + Write-Debug "Scoop cache written successfully: $CacheFilePath" + return $true + } catch { + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 b/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 new file mode 100644 index 0000000..5554228 --- /dev/null +++ b/DevSetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 @@ -0,0 +1,43 @@ +BeforeAll { + . $PSScriptRoot\ConvertFrom-Base64.ps1 + Mock Write-Error { } +} + +Describe "ConvertFrom-Base64" { + + Context "When EncodedString is empty" { + It "Should write error and return false" { + $result = ConvertFrom-Base64 -EncodedString "" + $result | Should -Be $false + } + } + + Context "When EncodedString is valid and OutputFile is not provided" { + It "Should decode and return the string" { + $plainText = "Hello, world!" + $base64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainText)) + $result = ConvertFrom-Base64 -EncodedString $base64 + $result | Should -Be $plainText + } + } + + Context "When EncodedString is valid and OutputFile is provided" { + It "Should decode and write to file, returning true" { + $plainText = "Test file output" + $base64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainText)) + $testFile = "$PSScriptRoot\test_output.txt" + if (Test-Path $testFile) { Remove-Item $testFile } + $result = ConvertFrom-Base64 -EncodedString $base64 -OutputFile $testFile + $result | Should -Be $true + (Get-Content $testFile -Raw) | Should -Be $plainText + Remove-Item $testFile + } + } + + Context "When EncodedString is invalid base64" { + It "Should write error and return false" { + $result = ConvertFrom-Base64 -EncodedString "not_base64!" + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/ConvertFrom-Base64.ps1 b/DevSetup/Private/Utils/ConvertFrom-Base64.ps1 new file mode 100644 index 0000000..a06d4d3 --- /dev/null +++ b/DevSetup/Private/Utils/ConvertFrom-Base64.ps1 @@ -0,0 +1,29 @@ +Function ConvertFrom-Base64 { + param ( + [string]$EncodedString, + [string]$OutputFile + ) + + if (-not $EncodedString) { + Write-Error "Base64 string is empty." + return $false + } + + try { + # Decode the base64 string + $decodedBytes = [System.Convert]::FromBase64String($EncodedString) + + if ($OutputFile) { + # Write to file if OutputFile is provided + [System.IO.File]::WriteAllBytes($OutputFile, $decodedBytes) + return $true + } else { + # Return the decoded string if no OutputFile is provided + $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) + return $decodedString + } + } catch { + Write-Error "Failed to convert Base64: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 b/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 new file mode 100644 index 0000000..0f457b2 --- /dev/null +++ b/DevSetup/Private/Utils/ConvertTo-Base64.Tests.ps1 @@ -0,0 +1,45 @@ +BeforeAll { + . $PSScriptRoot\ConvertTo-Base64.ps1 + Mock Write-Error { } +} + +Describe "ConvertTo-Base64" { + + Context "When converting a string to Base64" { + It "Should return the correct Base64 string" { + $inputString = "Hello, world!" + $stringBytes = [System.Text.Encoding]::UTF8.GetBytes($inputString) + $expected = [System.Convert]::ToBase64String($stringBytes) + $result = ConvertTo-Base64 -InputString $inputString + $result | Should -Be $expected + } + } + + Context "When converting a file to Base64" { + It "Should return the correct Base64 string" { + $inputString = "File content" + $testFile = "${TestDrive}\test_input.txt" + Set-Content -Path $testFile -Value $inputString + $stringBytes = [System.IO.File]::ReadAllBytes($testFile) + $expected = [System.Convert]::ToBase64String($stringBytes) + $result = ConvertTo-Base64 -FilePath $testFile + $result | Should -Be $expected + Remove-Item $testFile + } + } + + Context "When file does not exist" { + It "Should write error and return null" { + $result = ConvertTo-Base64 -FilePath "nonexistent.txt" + $result | Should -Be $null + } + } + + Context "When an exception occurs" { + It "Should write error and return null" { + Mock Test-Path { throw "Unexpected error" } + $result = ConvertTo-Base64 -FilePath "anyfile.txt" + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/ConvertTo-Base64.ps1 b/DevSetup/Private/Utils/ConvertTo-Base64.ps1 new file mode 100644 index 0000000..47d2a8a --- /dev/null +++ b/DevSetup/Private/Utils/ConvertTo-Base64.ps1 @@ -0,0 +1,32 @@ +Function ConvertTo-Base64 { + param ( + [Parameter(ParameterSetName = "File", Mandatory = $true)] + [string]$FilePath, + + [Parameter(ParameterSetName = "String", Mandatory = $true)] + [string]$InputString + ) + + try { + if ($PSCmdlet.ParameterSetName -eq "String") { + # Convert string to Base64 + $stringBytes = [System.Text.Encoding]::UTF8.GetBytes($InputString) + $base64String = [System.Convert]::ToBase64String($stringBytes) + return $base64String + } + else { + # Convert file to Base64 (existing functionality) + if (-not (Test-Path -Path $FilePath)) { + Write-Error "File not found: $FilePath" + return $null + } + + $fileBytes = [System.IO.File]::ReadAllBytes($FilePath) + $base64String = [System.Convert]::ToBase64String($fileBytes) + return $base64String + } + } catch { + Write-Error "Failed to convert to Base64: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Find-GitRepositories.ps1 b/DevSetup/Private/Utils/Find-GitRepositories.ps1 new file mode 100644 index 0000000..dd7aa66 --- /dev/null +++ b/DevSetup/Private/Utils/Find-GitRepositories.ps1 @@ -0,0 +1,121 @@ +Function Find-GitRepositories { + [CmdletBinding()] + Param( + [Parameter( + Position = 0, + HelpMessage = "The top level path to search" + )] + [ValidateScript({ + if (Test-Path $_) { + $True + } + else { + Throw "Cannot validate path $_" + } + })] + [string]$Path = "." + ) + + Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" + Write-Verbose "[PROCESS] Searching $(Convert-Path -path $path) for Git repositories" + + # Define directories to exclude from search (just the folder names) + $ExcludeFolders = @('Windows', 'Program Files', 'Program Files (x86)', '$RECYCLE.BIN') + + Write-Verbose "[PROCESS] Excluding system folders: $($ExcludeFolders -join ', ')" + + # Use a more efficient search strategy + function Search-GitRepos { + param([string]$SearchPath, [string[]]$ExcludeFolders) + + try { + # Get all directories first, excluding system folders at the top level + $directories = Get-ChildItem -Path $SearchPath -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notin $ExcludeFolders } + + foreach ($dir in $directories) { + # Check if this directory IS a git repo + $gitDir = Join-Path $dir.FullName ".git" + if (Test-Path $gitDir) { + # Found a git repo, yield it + Get-Item $gitDir -Force -ErrorAction SilentlyContinue + } + + # Recursively search subdirectories (but don't exclude here since we're deeper) + Search-GitRepos -SearchPath $dir.FullName -ExcludeFolders @() + } + } + catch { + # Silently continue on errors + } + } + + # Collect all repositories in an array + $repositories = @() + + Search-GitRepos -SearchPath $Path -ExcludeFolders $ExcludeFolders | + ForEach-Object { + $gitItem = $_ + $repoPath = Split-Path $gitItem.FullName -Parent + Write-Verbose "Found repository at: $repoPath" + + # Get the branch information + $branchName = "unknown" + $remoteUrl = "none" + if ($repoPath -and (Test-Path $repoPath)) { + $originalLocation = Get-Location + try { + Write-Verbose "Changing to repository: $repoPath" + Set-Location -Path $repoPath + + # Get current branch + $branchOutput = & git rev-parse --abbrev-ref HEAD 2>$null + if ($LASTEXITCODE -eq 0 -and $branchOutput) { + $branchName = $branchOutput.Trim() + } + + # Get remote origin URL + $remoteOutput = & git remote get-url origin 2>$null + if ($LASTEXITCODE -eq 0 -and $remoteOutput) { + $remoteUrl = $remoteOutput.Trim() + } + } + catch { + Write-Verbose "Branch/Remote detection error for $repoPath`: $_" + $branchName = "error" + $remoteUrl = "error" + } + finally { + Set-Location -Path $originalLocation + } + } else { + Write-Verbose "Invalid repository path: '$repoPath'" + $branchName = "invalid-path" + $remoteUrl = "invalid-path" + } + + # Add to repositories collection + $repositories += [PSCustomObject]@{ + Repository = $repoPath + Branch = $branchName + RemoteUrl = $remoteUrl + } + } + + # Output formatted table + if ($repositories.Count -gt 0) { + Write-Host "`nFound $($repositories.Count) Git repositories:" -ForegroundColor Green + Write-Host "=" * 80 -ForegroundColor Gray + + $repositories | Sort-Object Repository | Format-Table -AutoSize -Wrap @( + @{Label="Repository"; Expression={$_.Repository}; Width=40}, + @{Label="Branch"; Expression={$_.Branch}; Width=20}, + @{Label="Remote URL"; Expression={$_.RemoteUrl}; Width=50} + ) + } else { + Write-Host "No Git repositories found in the specified path." -ForegroundColor Yellow + } + + Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" + +} #end function \ No newline at end of file diff --git a/DevSetup/Private/Utils/Format-PrettyTable.ps1 b/DevSetup/Private/Utils/Format-PrettyTable.ps1 new file mode 100644 index 0000000..d897df9 --- /dev/null +++ b/DevSetup/Private/Utils/Format-PrettyTable.ps1 @@ -0,0 +1,130 @@ +Function Format-PrettyTable { + [cmdletbinding()] + Param( + [Parameter(Mandatory=$true)] + $Columns, + [Parameter(Mandatory=$true)] + [array]$Rows, + [Parameter(Mandatory=$true)] + [hashtable]$TableFormat + ) + + # Double-line for outer edges + $edgeV = [char]0x2551 # ║ + $edgeH = [char]0x2550 # ═ + $edgeTL = [char]0x2554 # ╔ + $edgeTR = [char]0x2557 # ╗ + $edgeBL = [char]0x255A # ╚ + $edgeBR = [char]0x255D # ╝ + + $sepTD = [char]0x2564 # ╥ + $sepBU = [char]0x2567 # ╧ + $sepMD = [char]0x256A # ╪ + + # Light single-line for inner separators + $sepV = [char]0x2502 # │ + $sepH = [char]0x2500 # ─ + $sepT = [char]0x252C # ┬ + $sepM = [char]0x253C # ┼ + $sepB = [char]0x2534 # ┴ + + function Repeat-Char($char, $count) { -join (1..$count | ForEach-Object { $char }) } + function Center-Text($text, $width) { + $text = "$text" + $pad = $width - $text.Length + if ($pad -le 0) { return $text } + $left = [math]::Floor($pad / 2) + $right = $pad - $left + (' ' * $left) + $text + (' ' * $right) + } + + function Left-Text($text, $width) { + $text = " $text" + if ($text.Length -ge $width) { return $text } + return $text + (' ' * ($width - $text.Length)) + } + + function Right-Text($text, $width) { + $text = "$text " + if ($text.Length -ge $width) { return $text } + return (' ' * ($width - $text.Length)) + $text + } + + # Top border: double corners, light separators + $topBorder = $edgeTL + $middleBorder = $edgeV + $bottomBorder = $edgeBL + + $idx = 0; + foreach ($column in $Columns.Values) { + $topBorder += (Repeat-Char $edgeH $column.Width) + $middleBorder += (Repeat-Char $edgeH $column.Width) + $bottomBorder += (Repeat-Char $edgeH $column.Width) + + if ($idx -lt $Columns.Count -1) { + # Add light separators + $topBorder += $sepTD + $middleBorder += $sepMD + $bottomBorder += $sepBU + } + $idx++ + } + + $topBorder += $edgeTR + $middleBorder += $edgeV + $bottomBorder += $edgeBR + + Write-Host $topBorder -ForegroundColor $TableFormat.BorderColor + Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor -NoNewLine + + $idx = 0; + foreach ($column in $Columns.Values) { + $columnText = switch ($column.Alignment) { + "Left" { Left-Text $column.Name $column.Width } + "Center" { Center-Text $column.Name $column.Width } + "Right" { Right-Text $column.Name $column.Width } + default { $column.Name } + } + + Write-Host $columnText -ForegroundColor $column.Color -NoNewLine + + if ($idx -lt $Columns.Count -1) { + Write-Host $sepV -ForegroundColor $TableFormat.BorderColor -NoNewLine + } + $idx++ + } + + Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor + + Write-Host $middleBorder -ForegroundColor $TableFormat.BorderColor + + foreach ($row in $Rows) { + Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor -NoNewLine + $idx = 0; + foreach ($column in $Columns.Values) { + if ($row -is [hashtable]) { + $value = $row[$column.Key] + } else { + $value = $row.($column.Key) + } + + $columnText = switch ($column.Alignment) { + "Left" { Left-Text $value $column.Width } + "Center" { Center-Text $value $column.Width } + "Right" { Right-Text $value $column.Width } + default { $value } + } + + Write-Host $columnText -ForegroundColor $row.Color -NoNewLine + + if ($idx -lt $Columns.Count -1) { + Write-Host $sepV -ForegroundColor $TableFormat.BorderColor -NoNewLine + } + $idx++ + } + Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor + } + + Write-Host $bottomBorder -ForegroundColor $TableFormat.BorderColor + +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 new file mode 100644 index 0000000..16f617e --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 @@ -0,0 +1,14 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupCachePath.ps1 + . $PSScriptRoot\Get-DevSetupPath.ps1 +} + +Describe "Get-DevSetupCachePath" { + BeforeEach { + Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } + } + It "should return the correct cache path for a valid user" { + $cachePath = Get-DevSetupCachePath + $cachePath | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupCachePath.ps1 b/DevSetup/Private/Utils/Get-DevSetupCachePath.ps1 new file mode 100644 index 0000000..054a8fc --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupCachePath.ps1 @@ -0,0 +1,60 @@ +<# +.SYNOPSIS + Gets the DevSetup cache directory path and ensures it exists. + +.DESCRIPTION + This function retrieves the cache directory path for the DevSetup module. The cache directory + is located at ".cache" within the main DevSetup directory and is used to store temporary files, + downloaded configurations, and other cached data. The function automatically creates the cache + directory if it doesn't exist, ensuring it's always available for use. + +.OUTPUTS + [System.String] + Returns the full path to the DevSetup cache directory. + +.EXAMPLE + Get-DevSetupCachePath + + Returns the path to the DevSetup cache directory, e.g., "C:\Users\Username\.devsetup\.cache" + +.EXAMPLE + $cachePath = Get-DevSetupCachePath + $tempFile = Join-Path $cachePath "temp-config.yaml" + + Gets the cache path and creates a path for a temporary file within it. + +.EXAMPLE + $cacheDir = Get-DevSetupCachePath + Get-ChildItem $cacheDir + + Gets the cache directory and lists its contents. + +.NOTES + - Uses Get-DevSetupPath to determine the base DevSetup directory + - Creates the cache directory (.cache) if it doesn't exist + - Returns the full path as a string for use in other functions + - The cache directory is hidden (starts with a dot) on Unix-like systems + - Suppresses output from New-Item using Out-Null for clean execution + - Ensures the cache directory is always available for DevSetup operations + +.LINK + +.COMPONENT + DevSetup.Utils + +.FUNCTIONALITY + Path Management, Directory Creation, Cache Management +#> + +Function Get-DevSetupCachePath { + $devSetupPath = Get-DevSetupPath + + # Define the cache path + $cachePath = Join-Path -Path $devSetupPath -ChildPath ".cache" + + if (-not (Test-Path -Path $cachePath)) { + New-Item -ItemType Directory -Path $cachePath | Out-Null + } + + return $cachePath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 new file mode 100644 index 0000000..40b722f --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 @@ -0,0 +1,23 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupCommunityEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupPath.ps1 + Mock Get-DevSetupEnvPath { "$TestDrive\Users\Test User\.devsetup\environments" } + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\.devsetup" } + Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } +} + +Describe "Get-DevSetupCommunityEnvPath" { + It "Should call Get-DevSetupEnvPath and Join-Path, and return the correct path" { + $result = Get-DevSetupCommunityEnvPath + $result | Should -Be "$TestDrive\Users\Test User\.devsetup\environments\community" + Assert-MockCalled Get-DevSetupEnvPath -Exactly 1 -Scope It + Assert-MockCalled Join-Path -Exactly 1 -Scope It + } + + It "Should handle different base paths" { + Mock Get-DevSetupEnvPath { "$TestDrive\CustomPath" } + $result = Get-DevSetupCommunityEnvPath + $result | Should -Be "$TestDrive\CustomPath\community" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 b/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 new file mode 100644 index 0000000..e7bee1f --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 @@ -0,0 +1,10 @@ +function Get-DevSetupCommunityEnvPath { + # Get the DevSetup path + $devSetupEnvPath = Get-DevSetupEnvPath + + # Define the environments path + $communityEnvironmentsPath = Join-Path -Path $devSetupEnvPath -ChildPath "community" + + # Return the environments path + return $communityEnvironmentsPath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 new file mode 100644 index 0000000..f6a238d --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 @@ -0,0 +1,14 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupPath.ps1 +} + +Describe "Get-DevSetupEnvPath" { + BeforeEach { + Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } + } + It "should return the correct environment path for a valid user" { + $envPath = Get-DevSetupEnvPath + $envPath | Should -Be "TestDrive:\Users\Test User\.devsetup\environments" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupEnvPath.ps1 b/DevSetup/Private/Utils/Get-DevSetupEnvPath.ps1 new file mode 100644 index 0000000..07308ba --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupEnvPath.ps1 @@ -0,0 +1,10 @@ +function Get-DevSetupEnvPath { + # Get the DevSetup path + $devSetupPath = Get-DevSetupPath + + # Define the environments path + $environmentsPath = Join-Path -Path $devSetupPath -ChildPath "environments" + + # Return the environments path + return $environmentsPath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 new file mode 100644 index 0000000..b45d057 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 @@ -0,0 +1,23 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupLocalEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupPath.ps1 + Mock Get-DevSetupEnvPath { "$TestDrive\Users\Test User\.devsetup\environments" } + Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\.devsetup" } + Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } +} + +Describe "Get-DevSetupLocalEnvPath" { + It "Should call Get-DevSetupEnvPath and Join-Path, and return the correct path" { + $result = Get-DevSetupLocalEnvPath + $result | Should -Be "$TestDrive\Users\Test User\.devsetup\environments\local" + Assert-MockCalled Get-DevSetupEnvPath -Exactly 1 -Scope It + Assert-MockCalled Join-Path -Exactly 1 -Scope It + } + + It "Should handle different base paths" { + Mock Get-DevSetupEnvPath { "$TestDrive\CustomPath" } + $result = Get-DevSetupLocalEnvPath + $result | Should -Be "$TestDrive\CustomPath\local" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 b/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 new file mode 100644 index 0000000..722fadc --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 @@ -0,0 +1,10 @@ +function Get-DevSetupLocalEnvPath { + # Get the DevSetup path + $devSetupEnvPath = Get-DevSetupEnvPath + + # Define the environments path + $localEnvironmentsPath = Join-Path -Path $devSetupEnvPath -ChildPath "local" + + # Return the environments path + return $localEnvironmentsPath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 new file mode 100644 index 0000000..5d32d9c --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 @@ -0,0 +1,22 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupManifest.ps1 +} + +Describe "Get-DevSetupManifest" { + BeforeEach { + Mock Get-Module { + return @{ + ModuleBase = "$PSScriptRoot\..\..\..\DevSetup" + } + } + } + It "should return the manifest file and not null" { + $manifest = Get-DevSetupManifest + $manifest | Should -Not -BeNullOrEmpty + } + + It "should contain the RootModule" { + $manifest = Get-DevSetupManifest + $manifest.RootModule | Should -Not -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupManifest.ps1 b/DevSetup/Private/Utils/Get-DevSetupManifest.ps1 new file mode 100644 index 0000000..67a38dc --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupManifest.ps1 @@ -0,0 +1,26 @@ +Function Get-DevSetupManifest { + try { + $moduleBase = (Get-Module -Name "DevSetup").ModuleBase + if (-not $moduleBase) { + Write-Error "DevSetup module is not installed." + return $null + } + $manifestPath = Join-Path -Path $moduleBase -ChildPath "DevSetup.psd1" + if (-not (Test-Path -Path $manifestPath)) { + Write-Error "DevSetup module manifest not found at $manifestPath." + return $null + } + $manifest = Import-PowerShellDataFile -Path $manifestPath + if (-not $manifest) { + Write-Error "Failed to import DevSetup module manifest." + return $null + } + + # Return the manifest object + return $manifest + } + catch { + Write-Error "Failed to retrieve DevSetup manifest: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 new file mode 100644 index 0000000..913a919 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupPath.Tests.ps1 @@ -0,0 +1,14 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupPath.ps1 + . $PSScriptRoot\Get-EnvironmentVariable.ps1 +} + +Describe "Get-DevSetupPath" { + BeforeEach { + Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } + } + It "should return the correct devsetup for the current user" { + $envPath = Get-DevSetupPath + $envPath | Should -Be "TestDrive:\Users\Test User\devsetup" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupPath.ps1 b/DevSetup/Private/Utils/Get-DevSetupPath.ps1 new file mode 100644 index 0000000..b7d2393 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupPath.ps1 @@ -0,0 +1,9 @@ +Function Get-DevSetupPath { + # Get user's home directory + $homeDirectory = Get-EnvironmentVariable USERPROFILE + + # Define .devsetup folder path + $devSetupPath = Join-Path -Path $homeDirectory -ChildPath "devsetup" + + return $devSetupPath +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 b/DevSetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 new file mode 100644 index 0000000..df90555 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 @@ -0,0 +1,36 @@ +BeforeAll { + . $PSScriptRoot\Get-DevSetupVersion.ps1 + . $PSScriptRoot\Get-DevSetupManifest.ps1 +} + +Describe "Get-DevSetupVersion" { + BeforeEach { + Mock Get-DevSetupManifest { + return @{ + ModuleVersion = '1.0.0' + PrivateData = @{ + PSData = @{ + ProjectUri = 'https://github.com/your/repo' + } + } + } + } + + function Get-GitHubRelease {} + + Mock Get-GitHubRelease { + return @{ + tag_name = '1.0.0' + } + } + } + It "should return the correct version when looking locally" { + $version = Get-DevSetupVersion -Local + $version | Should -Be "1.0.0" + } + + It "should return the correct version when looking remotely" { + $version = Get-DevSetupVersion -Remote + $version | Should -Be "1.0.0" + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-DevSetupVersion.ps1 b/DevSetup/Private/Utils/Get-DevSetupVersion.ps1 new file mode 100644 index 0000000..9c71bf2 --- /dev/null +++ b/DevSetup/Private/Utils/Get-DevSetupVersion.ps1 @@ -0,0 +1,104 @@ +Function Get-DevSetupVersion { + <# + .SYNOPSIS + Retrieves the version of the DevSetup module. + + .DESCRIPTION + Get-DevSetupVersion returns the current version of the DevSetup module either from the locally installed version or from the latest GitHub release. + + .PARAMETER Local + Retrieves the version from the locally installed DevSetup module. This is the default behavior if no parameter is specified. + + .PARAMETER Remote + Retrieves the latest version from the GitHub repository using the latest release tag. + + .OUTPUTS + System.Version. Returns the version object of the DevSetup module. + + .EXAMPLE + Get-DevSetupVersion + Returns the version object of the locally installed DevSetup module. + + .EXAMPLE + Get-DevSetupVersion -Local + Returns the version object of the locally installed DevSetup module. + + .EXAMPLE + Get-DevSetupVersion -Remote + Returns the version object of the latest release from the GitHub repository. + + .EXAMPLE + $version = Get-DevSetupVersion -Local + Write-Host "Major: $($version.Major), Minor: $($version.Minor), Build: $($version.Build)" + Gets the local version and displays individual components. + + .NOTES + This function is used to check the installed version of the DevSetup module and returns a Version object for easy comparison and component access. + The Local and Remote parameters are mutually exclusive. If neither is specified, Local is used by default. + #> + + Param( + [Parameter(Mandatory = $false)] + [switch]$Local, + + [Parameter(Mandatory = $false)] + [switch]$Remote + ) + + # Validate that only one parameter is specified + if ($Local -and $Remote) { + Write-Error "Local and Remote parameters are mutually exclusive. Please specify only one." + return $null + } + + # Default to Local if no parameter is specified + if (-not $Local -and -not $Remote) { + $Local = $true + } + + $manifest = Get-DevSetupManifest + if (-not $manifest) { + Write-Error "Failed to retrieve DevSetup module manifest." + return $null + } + + if ($Local) { + if (-not $manifest.ModuleVersion) { + Write-Error "Version information not found in the DevSetup module manifest." + return $null + } + try { + $versionObject = [Version]::new($manifest.ModuleVersion) + return $versionObject + } + catch { + Write-Error "Failed to parse version '$($manifest.ModuleVersion)' as a valid version object: $_" + return $null + } + } + + if ($Remote) { + try { + $projectUri = $manifest.PrivateData.PSData.ProjectUri + if (-not $projectUri) { + Write-Error "ProjectUri not found in the DevSetup module manifest." + return $null + } + + $release = Get-GitHubRelease -Uri $projectUri + if (-not $release -or -not $release.tag_name) { + Write-Error "Failed to retrieve latest release information from GitHub." + return $null + } + + # Remove 'v' prefix if present in tag name + $versionString = $release.tag_name -replace '^v', '' + $versionObject = [Version]::new($versionString) + return $versionObject + } + catch { + Write-Error "Failed to retrieve or parse remote version: $_" + return $null + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 b/DevSetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 new file mode 100644 index 0000000..cd2c911 --- /dev/null +++ b/DevSetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 @@ -0,0 +1,35 @@ +BeforeAll { + . $PSScriptRoot\Get-EnvironmentVariable.ps1 +} + +Describe "Get-EnvironmentVariable" { + + Context "When the environment variable exists" { + It "Should return the value of the variable" { + $env:TEST_ENV_VAR = "TestValue" + $result = Get-EnvironmentVariable -Name "TEST_ENV_VAR" + $result | Should -Be "TestValue" + Remove-Item Env:\TEST_ENV_VAR + } + } + + Context "When the environment variable does not exist" { + It "Should return null" { + Remove-Item Env:\NOT_EXISTING_VAR -ErrorAction SilentlyContinue + $result = Get-EnvironmentVariable -Name "NOT_EXISTING_VAR" + $result | Should -Be $null + } + } + + Context "When called with pipeline input" { + It "Should return the value for each variable" { + $env:PIPE_VAR1 = "Value1" + $env:PIPE_VAR2 = "Value2" + $results = @("PIPE_VAR1", "PIPE_VAR2") | Get-EnvironmentVariable + $results | Should -Contain "Value1" + $results | Should -Contain "Value2" + Remove-Item Env:\PIPE_VAR1 + Remove-Item Env:\PIPE_VAR2 + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-EnvironmentVariable.ps1 b/DevSetup/Private/Utils/Get-EnvironmentVariable.ps1 new file mode 100644 index 0000000..b0f730c --- /dev/null +++ b/DevSetup/Private/Utils/Get-EnvironmentVariable.ps1 @@ -0,0 +1,10 @@ +Function Get-EnvironmentVariable { + [cmdletbinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string]$Name + ) + process { + Write-Output ([System.Environment]::GetEnvironmentVariable($Name)) + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-PwshVersion.Tests.ps1 b/DevSetup/Private/Utils/Get-PwshVersion.Tests.ps1 new file mode 100644 index 0000000..a034fa5 --- /dev/null +++ b/DevSetup/Private/Utils/Get-PwshVersion.Tests.ps1 @@ -0,0 +1,26 @@ +BeforeAll { + . $PSScriptRoot\Get-PwshVersion.ps1 +} + +Describe "Get-PwshVersion" { + + Context "When called in a typical PowerShell environment" { + It "Should return a hashtable with Major, Minor, and Patch keys" { + $result = Get-PwshVersion + $result | Should -BeOfType 'hashtable' + $result.Keys | Should -Contain 'Major' + $result.Keys | Should -Contain 'Minor' + $result.Keys | Should -Contain 'Patch' + } + + It "Should return correct version numbers from \$PSVersionTable" { + $expectedMajor = $PSVersionTable.PSVersion.Major + $expectedMinor = $PSVersionTable.PSVersion.Minor + $expectedPatch = $PSVersionTable.PSVersion.Build + $result = Get-PwshVersion + $result.Major | Should -Be $expectedMajor + $result.Minor | Should -Be $expectedMinor + $result.Patch | Should -Be $expectedPatch + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Get-PwshVersion.ps1 b/DevSetup/Private/Utils/Get-PwshVersion.ps1 new file mode 100644 index 0000000..4b39bd7 --- /dev/null +++ b/DevSetup/Private/Utils/Get-PwshVersion.ps1 @@ -0,0 +1,10 @@ +Function Get-PwshVersion { + [CmdletBinding()] + Param() + + return @{ + Major = $PSVersionTable.PSVersion.Major + Minor = $PSVersionTable.PSVersion.Minor + Patch = $PSVersionTable.PSVersion.Build + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 new file mode 100644 index 0000000..643628f --- /dev/null +++ b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 @@ -0,0 +1,139 @@ +BeforeAll { + Function Get-GitHubRepository { } + Function Install-GitRepository { } + + . $PSScriptRoot\Initialize-DevSetupEnvs.ps1 + . $PSScriptRoot\Write-StatusMessage.ps1 + . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 + . $PSScriptRoot\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupManifest.ps1 + Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } + Mock Get-DevSetupManifest { + @{ + PrivateData = @{ + PSData = @{ + EnvironmentsProjectUri = "https://github.com/example/envrepo" + } + } + } + } + Mock Get-GitHubRepository { @{ clone_url = "https://github.com/example/envrepo.git" } } + Mock Test-Path { $false } + Mock Install-GitRepository { $true } + Mock Write-StatusMessage { } + Mock Optimize-DevSetupEnvs { } + Mock Write-Error { } + Mock Write-Verbose { } + Mock Get-DevSetupLocalEnvPath { "TestDrive:\DevSetupEnvs\environments\local" } + Mock Get-DevSetupCommunityEnvPath { "TestDrive:\DevSetupEnvs\environments\community" } +} + +Describe "Initialize-DevSetupEnvs" { + + Context "When manifest cannot be retrieved" { + It "Should write error and return null" { + Mock Get-DevSetupManifest { $null } + $result = Initialize-DevSetupEnvs + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to retrieve DevSetup module manifest" } + } + } + + Context "When EnvironmentsProjectUri is missing" { + It "Should write error and return null" { + Mock Get-DevSetupManifest { @{ PrivateData = @{ PSData = @{ } } } } + $result = Initialize-DevSetupEnvs + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "EnvironmentsProjectUri not found" } + } + } + + Context "When EnvironmentsProjectUri is not a .git URL and GitHub API fails" { + It "Should write error and return null" { + Mock Get-GitHubRepository { $null } + $result = Initialize-DevSetupEnvs + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to retrieve repository information or clone_url" } + } + } + + Context "When Get-GitHubRepository throws" { + It "Should write error and return null" { + Mock Get-GitHubRepository { throw "API error" } + $result = Initialize-DevSetupEnvs + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to get repository information from GitHub" } + } + } + + Context "When EnvironmentsProjectUri is a .git URL" { + It "Should use the URI directly and clone the repository" { + Mock Get-DevSetupManifest { + @{ + PrivateData = @{ + PSData = @{ + EnvironmentsProjectUri = "https://github.com/example/envrepo.git" + } + } + } + } + $result = Initialize-DevSetupEnvs + $result | Should -BeOfType [hashtable] + $result.local | Should -Be "TestDrive:\DevSetupEnvs\environments\local" + $result.community | Should -Be "TestDrive:\DevSetupEnvs\environments\community" + Assert-MockCalled Test-Path -Scope It -Exactly 3 + Assert-MockCalled Get-GithubRepository -Scope It -Exactly 0 + Assert-MockCalled Write-Verbose -Scope It -Exactly 0 -ParameterFilter { $Message -match "Environments repository already exists*" } + Assert-MockCalled Write-StatusMessage -Scope It -Exactly 2 + #Assert-MockCalled Install-GitRepository -Scope It -ParameterFilter { $RepositoryUrl -eq "https://github.com/example/envrepo.git" } + } + } + + Context "When repository path already exists" { + It "Should not clone and should write verbose" { + Mock Test-Path { $true } + $result = Initialize-DevSetupEnvs + $result | Should -BeOfType [hashtable] + $result.local | Should -Be "TestDrive:\DevSetupEnvs\environments\local" + $result.community | Should -Be "TestDrive:\DevSetupEnvs\environments\community" + Assert-MockCalled Install-GitRepository -Times 0 -Scope It + Assert-MockCalled Write-Verbose -Scope It -ParameterFilter { $Message -match "already exists" } + } + } + + Context "When Install-GitRepository fails" { + It "Should write failed status message" { + Mock Test-Path { $false } + Mock Install-GitRepository { $null } + $global:LASTEXITCODE = 1 + $result = Initialize-DevSetupEnvs + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } + } + } + + Context "When Install-GitRepository succeeds" { + It "Should write OK status message" { + Mock Test-Path { $false } + Mock Install-GitRepository { $null } + $global:LASTEXITCODE = 0 + $result = Initialize-DevSetupEnvs + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } + } + } + + Context "When Optimize-DevSetupEnvs is called" { + It "Should call Optimize-DevSetupEnvs after cloning" { + $result = Initialize-DevSetupEnvs + Assert-MockCalled Optimize-DevSetupEnvs -Scope It + } + } + + Context "When an unexpected error occurs" { + It "Should write error and return null" { + Mock Get-DevSetupEnvPath { throw "Unexpected error" } + $result = Initialize-DevSetupEnvs + $result | Should -Be $null + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 new file mode 100644 index 0000000..d8e29ee --- /dev/null +++ b/DevSetup/Private/Utils/Initialize-DevSetupEnvs.ps1 @@ -0,0 +1,90 @@ +Function Initialize-DevSetupEnvs { + try { + # Define environments repository path + $environmentsPath = Get-DevSetupEnvPath + $localEnvironmentsPath = Get-DevSetupLocalEnvPath + $communityEnvironmentsPath = Get-DevSetupCommunityEnvPath + + # Get the environments repository URL from the manifest + $manifest = Get-DevSetupManifest + if (-not $manifest) { + Write-Error "Failed to retrieve DevSetup module manifest." + return $null + } + + $environmentsProjectUri = $manifest.PrivateData.PSData.EnvironmentsProjectUri + if (-not $environmentsProjectUri) { + Write-Error "EnvironmentsProjectUri not found in the DevSetup module manifest." + return $null + } + + # Check if the URI ends with .git, if not use Get-GitHubRepository to get clone_url + if ($environmentsProjectUri -notlike "*.git") { + try { + Set-GitHubConfiguration -DisableTelemetry + #Write-Host "GitHub API access is required to retrieve repository information." -ForegroundColor Yellow + #Write-Host "Please create a GitHub Personal Access Token with 'repo' scope at:" -ForegroundColor Yellow + #Write-Host "https://github.com/settings/tokens" -ForegroundColor Cyan + #Write-Host "" + + # Prompt for GitHub access token with masked input + #$secureToken = Read-Host "Enter your GitHub Personal Access Token" -AsSecureString + #if (-not $secureToken -or $secureToken.Length -eq 0) { + # Write-Error "GitHub access token is required to continue." + # return $null + #} + + #$cred = New-Object System.Management.Automation.PSCredential "token", $secureToken + #Set-GitHubAuthentication -Credential $cred + #$secureToken = $null # clear this out now that it's no longer needed + #$cred = $null # clear this out now that it's no longer needed + $repository = Get-GitHubRepository -Uri $environmentsProjectUri 3>$null + if (-not $repository -or -not $repository.clone_url) { + Write-Error "Failed to retrieve repository information or clone_url from GitHub." + return $null + } + $repositoryUrl = $repository.clone_url + } + catch { + Write-Error "Failed to get repository information from GitHub: $_" + return $null + } + } else { + $repositoryUrl = $environmentsProjectUri + } + + if(-not (Test-Path $environmentsPath)) { + New-Item -Path $environmentsPath -Type Directory | Out-Null + } + + if(-not (Test-Path $localEnvironmentsPath)) { + New-Item -Path $localEnvironmentsPath -Type Directory | Out-Null + } + + + # Clone the environments repository if it doesn't exist + if (-not (Test-Path -Path $communityEnvironmentsPath)) { + Write-StatusMessage "- Cloning $repositoryUrl" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline + Install-GitRepository -RepositoryUrl $repositoryUrl -DestinationPath $communityEnvironmentsPath -UpdateExisting:$true *>$null + if($LASTEXITCODE -ne 0) { + Write-StatusMessage "[Failed]" -ForegroundColor Red + } else { + Write-StatusMessage "[OK]" -ForegroundColor Green + } + } else { + Write-Verbose "Environments repository already exists at: $communityEnvironmentsPath" + } + + Optimize-DevSetupEnvs | Out-Null + + # Return the path for use by other functions + return @{ + Local = $localEnvironmentsPath + Community = $communityEnvironmentsPath + } + } + catch { + Write-Error "Failed to initialize DevSetup environment: $_" + return $null + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 new file mode 100644 index 0000000..9818354 --- /dev/null +++ b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 @@ -0,0 +1,127 @@ +BeforeAll { + Function ConvertFrom-Yaml { } + . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 + . $PSScriptRoot\Get-DevSetupEnvPath.ps1 + . $PSScriptRoot\Get-DevSetupPath.ps1 + . $PSScriptRoot\Write-StatusMessage.ps1 + . $PSScriptRoot\Read-ConfigurationFile.ps1 + Mock Get-DevSetupEnvPath { "$TestDrive\DevSetupEnvs" } + Mock Get-DevSetupPath { "$TestDrive\DevSetup" } + Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } + Mock Write-StatusMessage { } + Mock Write-Warning { } + Mock Write-Error { } + Mock Write-Debug { } + Mock Write-Host { } + Mock ConvertTo-Json { param($obj) "json-output" } + Mock Out-File { } + Mock Read-ConfigurationFile { + param($Config) + switch ($Config) { + "$TestDrive\DevSetupEnvs\env1.yaml" { + @{ devsetup = @{ configuration = @{ os = @{ name = "Windows" }; version = "1.0.0" } } } + } + "$TestDrive\DevSetupEnvs\env2.yaml" { + @{ devsetup = @{ configuration = @{ os = @{ name = "Linux" }; version = "2.0.0" } } } + } + default { $null } + } + } +} + +Describe "Optimize-DevSetupEnvs" { + + Context "When environments path is missing or invalid" { + It "Should warn and return false" { + Mock Get-DevSetupEnvPath { $null } + $result = Optimize-DevSetupEnvs + $result | Should -Be $false + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } + } + + It "Should warn and return null if path does not exist" { + Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } + Mock Test-Path { $false } + $result = Optimize-DevSetupEnvs + $result | Should -Be $false + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } + } + } + + Context "When no YAML files are found" { + It "Should write status message and return empty array" { + Mock Test-Path { $true } + Mock Get-ChildItem { @() } + $result = Optimize-DevSetupEnvs + $result | Should -BeOfType 'bool' + $result | Should -Be $true + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Indexing 0 environment files" } + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } + } + } + + Context "When YAML files are found and processed successfully" { + It "Should return environments array and write status messages" { + Mock Test-Path { $true } + Mock Get-ChildItem { + @( + @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" }, + @{ Name = "env2.yaml"; FullName = "$TestDrive\DevSetupEnvs\env2.yaml" } + ) + } + $result = Optimize-DevSetupEnvs + Assert-MockCalled Write-Error -Scope It -Exactly 0 + #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "Indexing 2 environment files" } + #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -eq "[OK]" } + $result | Should -BeOfType 'bool' + $result | Should -Be $false + } + } + + Context "When a YAML file fails to process" { + It "Should warn and continue processing other files" { + Mock Test-Path { $true } + Mock Get-ChildItem { + @( + @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" }, + @{ Name = "bad.yaml"; FullName = "$TestDrive\DevSetupEnvs\bad.yaml" } + ) + } + Mock Read-ConfigurationFile { + param($Config) + if ($Config -eq "$TestDrive\DevSetupEnvs\bad.yaml") { throw "YAML error" } + @{ devsetup = @{ configuration = @{ os = @{ name = "Windows" }; version = "1.0.0" } } } + } + $result = Optimize-DevSetupEnvs + Assert-MockCalled Write-Error -Scope It -Exactly 0 + Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to process bad.yaml" } + $result | Should -BeOfType 'bool' + $result | Should -Be $false + + } + } + + Context "When writing environments.json fails" { + It "Should write failed status message and return null" { + Mock Test-Path { $true } + Mock Get-ChildItem { + @( + @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" } + ) + } + Mock Out-File { throw "File error" } + $result = Optimize-DevSetupEnvs + $result | Should -Be $false + Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } + } + } + + Context "When an unexpected error occurs" { + It "Should write error and return null" { + Mock Get-DevSetupEnvPath { throw "Unexpected error" } + $result = Optimize-DevSetupEnvs + $result | Should -Be $false + Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to optimize DevSetup environments" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 new file mode 100644 index 0000000..d262fea --- /dev/null +++ b/DevSetup/Private/Utils/Optimize-DevSetupEnvs.ps1 @@ -0,0 +1,93 @@ +Function Optimize-DevSetupEnvs { + try { + # Get the DevSetup environments path + $envPath = Get-DevSetupEnvPath + if (-not $envPath -or -not (Test-Path $envPath)) { + Write-Warning "DevSetup environments path not found or doesn't exist: $envPath" + return $false + } + + # Get all YAML files in the environments path + $devsetupEnvFiles = Get-ChildItem -Path $envPath -Filter "*.devsetup" -File -Recurse + if (-not $devsetupEnvFiles) { + #Write-Host "No YAML environment files found in: $envPath" -ForegroundColor Yellow + Write-StatusMessage "- Indexing 0 environment files" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline | Out-Null + Write-StatusMessage "[OK]" -ForegroundColor Green | Out-Null + return $true + } + + #Write-Host "Found $($yamlFiles.Count) environment file(s) to process..." -ForegroundColor Cyan + Write-StatusMessage "- Indexing $($devsetupEnvFiles.Count) environment files" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline | Out-Null + + $environments = @() + + foreach ($devsetupEnvFile in $devsetupEnvFiles) { + try { + Write-Debug "Processing: $($devsetupEnvFile.Name)" + + # Read the YAML configuration + $config = Read-ConfigurationFile -Config $devsetupEnvFile.FullName + + # Extract environment name (filename without extension) + $envName = [System.IO.Path]::GetFileNameWithoutExtension($devsetupEnvFile.Name) + + # Extract platform information + $platform = $null + if ($config -and $config.devsetup -and $config.devsetup.configuration -and $config.devsetup.configuration.os -and $config.devsetup.configuration.os.name) { + $platform = $config.devsetup.configuration.os.name + } + + # Extract version information + $version = "Unknown" + if ($config -and $config.devsetup -and $config.devsetup.configuration -and $config.devsetup.configuration -and $config.devsetup.configuration.version) { + $version = $config.devsetup.configuration.version + } + + $provider = ($devsetupEnvFile.FullName.Split([System.IO.Path]::DirectorySeparatorChar))[-2] + + if($provider -ne 'local') { + $envName = $provider + ":" + $envName + } + + # Create environment entry + $envEntry = @{ + name = $envName + platform = $platform + version = $version + file = $devsetupEnvFile.Name + provider = $provider + } + + $environments += $envEntry + $platformDisplay = if ($platform) { $platform } else { 'Not specified' } + Write-Debug " - Name: $envName, Version: $version, Platform: $platformDisplay" + } + catch { + Write-Warning "Failed to process $($devsetupEnvFile.Name): $_" + continue + } + } + + # Write results to environments.json + $devSetupPath = Get-DevSetupPath + $environmentsJsonPath = Join-Path -Path $devSetupPath -ChildPath "environments.json" + + try { + $jsonOutput = $environments | ConvertTo-Json -Depth 10 + $jsonOutput | Out-File -FilePath $environmentsJsonPath -Encoding UTF8 + Write-Debug "Environment index written to: $environmentsJsonPath" + #Write-Host "Processed $($environments.Count) environment(s) successfully" -ForegroundColor Green + Write-StatusMessage "[OK]" -ForegroundColor Green + } + catch { + Write-StatusMessage "[Failed]" -ForegroundColor Red + return $false + } + + return $true + } + catch { + Write-Error "Failed to optimize DevSetup environments: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 b/DevSetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 new file mode 100644 index 0000000..9ebed88 --- /dev/null +++ b/DevSetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 @@ -0,0 +1,43 @@ +BeforeAll { + function ConvertFrom-Yaml { } + . $PSScriptRoot\Read-ConfigurationFile.ps1 + Mock Get-Content { } + Mock ConvertFrom-Yaml { } +} + +Describe "Read-ConfigurationFile" { + + Context "When configuration file exists and contains valid YAML" { + It "Should return parsed YAML data" { + Mock Get-Content { "key: value" } + Mock ConvertFrom-Yaml { @{ key = "value" } } + $result = Read-ConfigurationFile -Config "config.yaml" + $result | Should -BeOfType System.Collections.Hashtable + $result.key | Should -Be "value" + } + } + + Context "When configuration file does not exist" { + It "Should throw an error" { + Mock Get-Content { throw "File not found" } + { Read-ConfigurationFile -Config "missing.yaml" } | Should -Throw "File not found" + } + } + + Context "When YAML is invalid" { + It "Should throw an error from ConvertFrom-Yaml" { + Mock Get-Content { "invalid: yaml: -" } + Mock ConvertFrom-Yaml { throw "Invalid YAML" } + { Read-ConfigurationFile -Config "bad.yaml" } | Should -Throw "Invalid YAML" + } + } + + Context "When ConvertFrom-Yaml returns $null" { + It "Should return null" { + Mock Get-Content { "key: value" } + Mock ConvertFrom-Yaml { $null } + $result = Read-ConfigurationFile -Config "config.yaml" + $result | Should -Be $null + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Read-ConfigurationFile.ps1 b/DevSetup/Private/Utils/Read-ConfigurationFile.ps1 new file mode 100644 index 0000000..8a39e57 --- /dev/null +++ b/DevSetup/Private/Utils/Read-ConfigurationFile.ps1 @@ -0,0 +1,7 @@ +Function Read-ConfigurationFile { + param ( + [string]$Config + ) + $YamlData = ConvertFrom-Yaml (Get-Content -Path $Config -Raw) + return $YamlData +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 b/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 new file mode 100644 index 0000000..9c3d61b --- /dev/null +++ b/DevSetup/Private/Utils/Test-OperatingSystem.Tests.ps1 @@ -0,0 +1,62 @@ +BeforeAll { + . $PSScriptRoot\Test-OperatingSystem.ps1 + . $PSScriptRoot\Get-PwshVersion.ps1 +} + +Describe "Test-OperatingSystem" { + + if ($PSVersionTable.PSVersion.Major -eq 5) { + BeforeAll { Mock Get-PwshVersion { [PSCustomObject]@{ Major = $PSVersionTable.PSVersion.Major } } } + + Context "When called with -Windows on PowerShell 5.1" { + It "Should return $true" { + $result = Test-OperatingSystem -Windows + $result | Should -Be $true + } + } + + Context "When called with -Linux on PowerShell 5.1" { + It "Should return $false" { + $result = Test-OperatingSystem -Linux + $result | Should -Be $false + } + } + + Context "When called with -MacOS on PowerShell 5.1" { + It "Should return $false" { + $result = Test-OperatingSystem -MacOS + $result | Should -Be $false + } + } + + Context "When called with no parameters on PowerShell 5.1" { + It "Should return $null" { + $result = Test-OperatingSystem + $result | Should -Be $null + } + } + } + + if ($PSVersionTable.PSVersion.Major -ge 6) { + BeforeAll { Mock Get-PwshVersion { [PSCustomObject]@{ Major = $PSVersionTable.PSVersion.Major } } } + + Context "When called with -Windows on PowerShell 7+" { + It "Should return value of `$IsWindows (default: $true)" { + $result = Test-OperatingSystem -Windows + $result | Should -Be $true + } + It "Should return value of `$IsLinux (default: $false)" { + $result = Test-OperatingSystem -Linux + $result | Should -Be $false + } + It "Should return value of `$IsMacOS (default: $false)" { + $result = Test-OperatingSystem -MacOS + $result | Should -Be $false + } + It "Should return $null if no parameter is specified" { + $result = Test-OperatingSystem + $result | Should -Be $null + } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-OperatingSystem.ps1 b/DevSetup/Private/Utils/Test-OperatingSystem.ps1 new file mode 100644 index 0000000..ebb1f27 --- /dev/null +++ b/DevSetup/Private/Utils/Test-OperatingSystem.ps1 @@ -0,0 +1,30 @@ +Function Test-OperatingSystem { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$false)] + [switch]$Windows, + + [Parameter(Mandatory=$false)] + [switch]$Linux, + + [Parameter(Mandatory=$false)] + [switch]$MacOS + ) + + if((Get-PwshVersion).Major -lt 6) { + $IsWindows = $true + $IsLinux = $false + $IsMacOS = $false + } + + if($Windows) { + return $IsWindows + } + if($Linux) { + return $IsLinux + } + if($MacOS) { + return $IsMacOS + } + return $null +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 b/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 new file mode 100644 index 0000000..6865457 --- /dev/null +++ b/DevSetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 @@ -0,0 +1,46 @@ +BeforeAll { + . $PSScriptRoot\Test-RunningAsAdmin.ps1 + . $PSScriptRoot\Test-OperatingSystem.ps1 + Mock Test-OperatingSystem { param($Windows) $false } +} + +Describe "Test-RunningAsAdmin" { + + Context "When not running on Windows" { + It "Should return true (assume sufficient privileges)" { + Mock Test-OperatingSystem { param($Windows) $false } + $result = Test-RunningAsAdmin + $result | Should -Be $true + } + } + + Context "When running on Windows as administrator" { + It "Should return true" { + Mock Test-OperatingSystem { param($Windows) $true } + class MockPrincipal { + [bool] IsInRole([object]$role) { return $true } + } + Mock 'New-Object' -MockWith { + param($type) + return [MockPrincipal]::new() + } + $result = Test-RunningAsAdmin + $result | Should -Be $true + } + } + + Context "When running on Windows but not as administrator" { + It "Should return false" { + Mock Test-OperatingSystem { param($Windows) $true } + class MockPrincipal { + [bool] IsInRole([object]$role) { return $false } + } + Mock 'New-Object' -MockWith { + param($type) + return [MockPrincipal]::new() + } + $result = Test-RunningAsAdmin + $result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Test-RunningAsAdmin.ps1 b/DevSetup/Private/Utils/Test-RunningAsAdmin.ps1 new file mode 100644 index 0000000..85bf9d5 --- /dev/null +++ b/DevSetup/Private/Utils/Test-RunningAsAdmin.ps1 @@ -0,0 +1,13 @@ +Function Test-RunningAsAdmin { + # Check if we're on Windows - Windows security principals are Windows-only + if (-not (Test-OperatingSystem -Windows)) { + # On non-Windows platforms, assume we have sufficient privileges + return $true + } + + $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) + if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + return $false + } + return $true +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Write-NewConfig.ps1 b/DevSetup/Private/Utils/Write-NewConfig.ps1 new file mode 100644 index 0000000..df37b3e --- /dev/null +++ b/DevSetup/Private/Utils/Write-NewConfig.ps1 @@ -0,0 +1,223 @@ +Function Write-NewConfig { + Param( + [Parameter(Mandatory = $true)] + [string]$OutFile + ) + + try { + # Check if running as administrator + if (-not (Test-RunningAsAdmin)) { + throw "This operation requires administrator privileges. Please run as administrator." + } + + # Create base config file + #Write-Host "Creating base configuration file: $OutFile" -ForegroundColor Cyan + + # Get OS information in a PowerShell 5.1 compatible way + $platform = [System.Environment]::OSVersion.Platform.ToString() + $osArchitecture = if ([System.Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } + + # Make platform more user-friendly + $friendlyPlatform = switch ($platform) { + "Win32NT" { "Windows" } + "Unix" { + # Check if it's macOS or Linux in a PS 5.1 compatible way + $uname = "" + try { + $uname = (& uname -s 2>$null) + } catch {} + if ($uname -eq "Darwin") { + "macOS" + } else { + "Linux" + } + } + default { $platform } + } + + # Get friendly OS version + $friendlyOsVersion = switch ($platform) { + "Win32NT" { + try { + $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue + if ($osInfo) { + $osInfo.Caption -replace "Microsoft ", "" + } else { + [System.Environment]::OSVersion.VersionString + } + } + catch { + [System.Environment]::OSVersion.VersionString + } + } + "Unix" { + if ($friendlyPlatform -eq "macOS") { + try { + $macVersion = (& sw_vers -productVersion 2>$null) + if ($macVersion) { + "macOS $macVersion" + } else { + [System.Environment]::OSVersion.VersionString + } + } + catch { + [System.Environment]::OSVersion.VersionString + } + } else { + # Linux + try { + $linuxVersion = "" + if (Test-Path "/etc/os-release") { + $osRelease = Get-Content "/etc/os-release" | Where-Object { $_ -like "PRETTY_NAME=*" } + if ($osRelease) { + $linuxVersion = ($osRelease -split '=')[1] -replace '"', '' + } + } + if ($linuxVersion) { + $linuxVersion + } else { + [System.Environment]::OSVersion.VersionString + } + } + catch { + [System.Environment]::OSVersion.VersionString + } + } + } + default { + [System.Environment]::OSVersion.VersionString + } + } + + # Handle versioning and preserve existing config + $currentVersion = "1.0.0" # Default version for new files + $baseConfig = @{ + devsetup = @{ + dependencies = @{ + chocolatey = @{ + packages = @() + } + powershell = @{ + modules = @() + scope = "CurrentUser" + } + scoop = @{ + packages = @() + buckets = @() + } + } + commands = @() + configuration = @{ + description = "Auto-generated development environment configuration" + version = $currentVersion + createdDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + createdBy = $env:USERNAME + os = @{ + name = $friendlyPlatform + version = $friendlyOsVersion + architecture = $osArchitecture + } + powershell = @{ + version = $PSVersionTable.PSVersion.ToString() + edition = $PSVersionTable.PSEdition + } + } + } + } + + if (Test-Path $OutFile) { + try { + Write-Host "- Using existing configuration..." -ForegroundColor Gray + $existingConfig = Read-ConfigurationFile -Config $OutFile + if ($existingConfig -and $existingConfig.devsetup) { + # Preserve existing dependencies + if ($existingConfig.devsetup.dependencies) { + $baseConfig.devsetup.dependencies = $existingConfig.devsetup.dependencies + } + + # Preserve existing commands + if ($existingConfig.devsetup.commands) { + $baseConfig.devsetup.commands = $existingConfig.devsetup.commands + } + + # Handle version increment + if ($existingConfig.devsetup.configuration -and $existingConfig.devsetup.configuration.version) { + $existingVersionString = $existingConfig.devsetup.configuration.version + + try { + # Parse version using System.Version + $existingVersion = [System.Version]$existingVersionString + $newMinor = $existingVersion.Minor + 1 + $currentVersion = "$($existingVersion.Major).$newMinor.$($existingVersion.Build)" + $baseConfig.devsetup.configuration.version = $currentVersion + Write-Host "- Version: $existingVersionString -> $currentVersion" -ForegroundColor Gray + } + catch { + Write-Warning "- Version: $currentVersion" + } + } else { + Write-Host "- Version: $currentVersion" -ForegroundColor Gray + } + + # Preserve other configuration fields but update system info + if ($existingConfig.devsetup.configuration) { + $baseConfig.devsetup.configuration.description = $existingConfig.devsetup.configuration.description + $baseConfig.devsetup.configuration.createdBy = $existingConfig.devsetup.configuration.createdBy + if ($existingConfig.devsetup.configuration.createdDate) { + # Keep original creation date, but we could add a lastModified field + $baseConfig.devsetup.configuration.createdDate = $existingConfig.devsetup.configuration.createdDate + $baseConfig.devsetup.configuration.lastModified = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + } + } + } + } + catch { + Write-Warning "Failed to read existing configuration for merging: $_" + Write-Host "- Using new configuration with default version: $currentVersion" -ForegroundColor Gray + } + } else { + Write-Host "- Using new configuration file, starting with version: $currentVersion" -ForegroundColor Green + } + + try { + $yamlOutput = $baseConfig | ConvertTo-Yaml + $yamlOutput | Out-File -FilePath $OutFile -Encoding UTF8 + Write-Debug "Base configuration file created successfully!" + } + catch { + Write-Error "Failed to create base configuration file: $_" + return $false + } + + # Convert from installed Chocolatey packages + Write-Host "`nScanning installed Chocolatey packages..." -ForegroundColor Cyan + if (-not (Export-InstalledChocolateyPackages -Config $OutFile)) { + Write-Warning "Failed to convert Chocolatey packages, but continuing..." + } + + # Convert from installed Scoop packages + Write-Host "`nScanning installed Scoop packages..." -ForegroundColor Cyan + if (-not (Export-InstalledScoopPackages -Config $OutFile)) { + Write-Warning "Failed to convert Scoop packages, but continuing..." + } + + # Convert from installed PowerShell modules + Write-Host "`nScanning installed PowerShell modules..." -ForegroundColor Cyan + if (-not (Export-InstalledPowershellModules -Config $OutFile)) { + Write-Warning "Failed to convert PowerShell modules, but continuing..." + } + + ConvertFrom-3rdPartyInstall -Config $OutFile + + Write-Host "`nConfiguration file generation completed!" -ForegroundColor Green + Write-Host "- Configuration saved to: $OutFile" -ForegroundColor Gray + Write-Host "" + + Optimize-DevSetupEnvs + return $true + } + catch { + Write-Error "Error creating new configuration: $_" + return $false + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 b/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 new file mode 100644 index 0000000..c9cc458 --- /dev/null +++ b/DevSetup/Private/Utils/Write-StatusMessage.Tests.ps1 @@ -0,0 +1,81 @@ +BeforeAll { + . $PSScriptRoot\Write-StatusMessage.ps1 + Mock Write-Host { param($Message) } + Mock Write-Verbose { param($Object) } + Mock Write-Debug { param($Object) } + Mock Write-Warning { param($Object) } + Mock Write-Error { param($Object) } +} + +Describe "Write-StatusMessage" { + + Context "When called with default parameters" { + It "Should call Write-Host with the message" { + Write-StatusMessage -Message "Hello" + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "Hello" } + } + } + + Context "When Verbosity is Verbose" { + It "Should call Write-Verbose" { + Write-StatusMessage -Message "Verbose message" -Verbosity "Verbose" + Assert-MockCalled Write-Verbose -Exactly 1 -Scope It + } + } + + Context "When Verbosity is Debug" { + It "Should call Write-Debug" { + Write-StatusMessage -Message "Debug message" -Verbosity "Debug" + Assert-MockCalled Write-Debug -Exactly 1 -Scope It + } + } + + Context "When Verbosity is Warning" { + It "Should call Write-Warning" { + Write-StatusMessage -Message "Warning message" -Verbosity "Warning" + Assert-MockCalled Write-Warning -Exactly 1 -Scope It + } + } + + Context "When Verbosity is Error" { + It "Should call Write-Error" { + Write-StatusMessage -Message "Error message" -Verbosity "Error" + Assert-MockCalled Write-Error -Exactly 1 -Scope It + } + } + + Context "When Indent is specified" { + It "Should indent the message" { + Write-StatusMessage -Message "Indented" -Indent 4 + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq " Indented" } + } + } + + Context "When Width is specified and message is longer" { + It "Should truncate the message with ellipsis" { + Write-StatusMessage -Message "This is a long message" -Width 10 + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "This is..." } + } + } + + Context "When Width is specified and message is shorter" { + It "Should pad the message to the specified width" { + Write-StatusMessage -Message "Short" -Width 10 + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "Short " } + } + } + + Context "When NoNewLine is specified" { + It "Should pass NoNewLine to Write-Host" { + Write-StatusMessage -Message "NoNewLine" -NoNewLine + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $NoNewLine -eq $true } + } + } + + Context "When ForegroundColor is specified" { + It "Should pass ForegroundColor to Write-Host" { + Write-StatusMessage -Message "Color" -ForegroundColor "Green" + Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $ForegroundColor -eq "Green" } + } + } +} \ No newline at end of file diff --git a/DevSetup/Private/Utils/Write-StatusMessage.ps1 b/DevSetup/Private/Utils/Write-StatusMessage.ps1 new file mode 100644 index 0000000..35d6223 --- /dev/null +++ b/DevSetup/Private/Utils/Write-StatusMessage.ps1 @@ -0,0 +1,59 @@ +Function Write-StatusMessage { + [CmdletBinding()] + Param( + [Parameter(Mandatory=$true, Position=0)] + [string]$Message, + [Parameter(Mandatory=$false)] + [string]$ForegroundColor = "Gray", + [Parameter(Mandatory=$false)] + [int]$Indent = 0, + [Parameter(Mandatory=$false)] + [ValidateSet("Default", "Verbose", "Debug", "Warning", "Error")] + [string]$Verbosity = "Default", + [Parameter(Mandatory=$false)] + [int]$Width = 0, + [Parameter(Mandatory=$false)] + [switch]$NoNewLine + ) + + if ($Indent -gt 0) { + $Message = "$(' ' * $Indent)$Message" + } + + if ($Width -gt 0) { + if($Message.Length -gt $Width) { + $Message = $Message.Substring(0, $Width - 3) + "..."; + } else { + $Message = $Message.PadRight($Width, " "); + } + } + + $messageParams = @{ } + + if($Verbosity -eq "Default") { + $messageParams.Object = $Message + $messageParams.ForegroundColor = $ForegroundColor + $messageParams.NoNewLine = $NoNewLine.IsPresent + } else { + $messageParams.Message = $Message + } + #$messageParams.Object = $Message + + switch($Verbosity) { + "Verbose" { + Write-Verbose @messageParams + } + "Debug" { + Write-Debug @messageParams + } + "Warning" { + Write-Warning @messageParams + } + "Error" { + Write-Error @messageParams + } + "Default" { + Write-Host @messageParams + } + } +} \ No newline at end of file diff --git a/DevSetup/Public/Use-DevSetup.ps1 b/DevSetup/Public/Use-DevSetup.ps1 new file mode 100644 index 0000000..71f6379 --- /dev/null +++ b/DevSetup/Public/Use-DevSetup.ps1 @@ -0,0 +1,370 @@ +Function Use-DevSetup { + <# + .SYNOPSIS + Manages development environment configurations using the DevSetup module. + + .DESCRIPTION + Use-DevSetup is the main function for managing development environments. It provides actions to install, update, initialize, export, list, and uninstall development environment configurations. + The function supports multiple installation sources including local configurations by name, remote URLs, and local file paths. + + Run 'Use-DevSetup -Init' first to set up the DevSetup environment and initialize the necessary directory structure and configuration files. + + .PARAMETER Install + Installs a development environment from a configuration file. Can be used with Name, Url, or FilePath parameters. + + .PARAMETER Update + Updates an existing development environment configuration. Requires the Name parameter. + + .PARAMETER Init + Initializes the DevSetup environment and sets up the necessary directory structure and configuration files. This should be run first before using other actions. + + .PARAMETER Export + Exports the current development environment to a configuration file. Requires the Name parameter to specify the name for the exported configuration. + + .PARAMETER List + Lists all available development environment configurations. + + .PARAMETER Uninstall + Uninstalls a development environment configuration. Requires the Name parameter. + + .PARAMETER Name + The name of the environment configuration to use. Required for Install, Update, Export, and Uninstall actions when using local configurations. + + .PARAMETER Url + The URL of a remote configuration file to install. Used with the Install action for remote installations. + + .PARAMETER Path + The local file path to a configuration file to install. Used with the Install action for local file installations. + + .PARAMETER Platform + The platform to filter environments by when using the List action. Use "current" (default) to show environments for the current platform, "all" to show all environments, or specify a platform like "Windows", "Linux", "macOS". + + .OUTPUTS + [System.Boolean] + Returns $true if the action completes successfully, $false otherwise. + + .EXAMPLE + Use-DevSetup -Init + + Initializes the DevSetup environment. Run this first to set up the necessary directory structure and configuration files. + + .EXAMPLE + Use-DevSetup -List + + Lists development environment configurations for the current platform. + + .EXAMPLE + Use-DevSetup -List -Platform "all" + + Lists all available development environment configurations regardless of platform. + + .EXAMPLE + Use-DevSetup -List -Platform "Linux" + + Lists development environment configurations specifically for Linux. + + .EXAMPLE + Use-DevSetup -Install -Name "WebDev" + + Installs the development environment using the "WebDev" configuration from local configurations. + + .EXAMPLE + Use-DevSetup -Install -Url "https://raw.githubusercontent.com/user/configs/main/webdev.devsetup" + + Installs a development environment from a remote configuration file URL. + + .EXAMPLE + Use-DevSetup -Install -Path "C:\Configs\MySetup.devsetup" + + Installs a development environment from a local configuration file path. + + .EXAMPLE + Use-DevSetup -Update + + Updates the devsetup system with any new environments or changes. + + .EXAMPLE + Use-DevSetup -Export -Name "MyCurrentSetup" + + Exports the current system's installed packages and tools to a new configuration file named "MyCurrentSetup". + + .EXAMPLE + Use-DevSetup -Uninstall -Name "WebDev" + + Uninstalls all packages and tools associated with the "WebDev" configuration. + + .NOTES + - Run 'Use-DevSetup -Init' first to initialize the DevSetup environment before using other actions + - Only one action can be specified at a time using parameter sets + - Supports three installation methods: + * By Name: Uses local configuration files from the DevSetup directory + * By URL: Downloads and installs from a remote configuration file + * By Path: Installs from a local file path outside the DevSetup directory + - The function validates input and provides appropriate error messages for invalid combinations + - Displays formatted progress headers with color-coded output for better user experience + - Includes comprehensive try-catch error handling with descriptive error messages + - Update and Uninstall actions are marked as TODO and not yet implemented + + .LINK + + .COMPONENT + DevSetup.Public + + .FUNCTIONALITY + Environment Management, Configuration Installation, System Setup +#> + + Param( + [Parameter(Mandatory = $true, ParameterSetName = "Install")] + [Parameter(Mandatory = $true, ParameterSetName = "InstallUrl")] + [Parameter(Mandatory = $true, ParameterSetName = "InstallPath")] + [switch]$Install, + + [Parameter(Mandatory = $true, ParameterSetName = "Update")] + [switch]$Update, + + [Parameter(Mandatory = $true, ParameterSetName = "Init")] + [switch]$Init, + + [Parameter(Mandatory = $true, ParameterSetName = "Export")] + [switch]$Export, + + [Parameter(Mandatory = $true, ParameterSetName = "List")] + [switch]$List, + + [Parameter(Mandatory = $true, ParameterSetName = "Uninstall")] + [switch]$Uninstall, + + [Parameter(Mandatory = $true, ParameterSetName = "Install")] + [Parameter(Mandatory = $true, ParameterSetName = "Export")] + [Parameter(Mandatory = $true, ParameterSetName = "Uninstall")] + [string]$Name, + + [Parameter(Mandatory = $true, ParameterSetName = "InstallUrl")] + [string]$Url, + + [Parameter(Mandatory = $true, ParameterSetName = "InstallPath")] + [string]$Path, + + [Parameter(Mandatory = $false, ParameterSetName = "List")] + [string]$Platform = "current" + ) + + try { + # Determine which action was selected based on parameter set + $selectedAction = $PSCmdlet.ParameterSetName.ToLower() + + + function Repeat-Char($char, $count) { -join (1..$count | ForEach-Object { $char }) } + + # Display fancy action header + #Write-Host "" + #Write-Host "+===============================================================================+" -ForegroundColor Cyan + #Write-Host "|" -ForegroundColor Cyan -NoNewline + #Write-Host " DEVSETUP " -ForegroundColor Yellow -NoNewline + #Write-Host "|" -ForegroundColor Cyan + #Write-Host "|" -ForegroundColor Cyan -NoNewline + #Write-Host " Development Environment Manager " -ForegroundColor White -NoNewline + #Write-Host "|" -ForegroundColor Cyan + #Write-Host "+===============================================================================+" -ForegroundColor Cyan +# Define box drawing characters using [char] codes + $b = [char]0x2588 # █ (full block) + $tl = [char]0x2554 # ╔ (top-left) + $tr = [char]0x2557 # ╗ (top-right) + $bl = [char]0x255A # ╚ (bottom-left) + $br = [char]0x255D # ╝ (bottom-right) + $h = [char]0x2550 # ═ (horizontal) + $v = [char]0x2551 # ║ (vertical) + $ml = [char]0x2560 # + $mr = [char]0x2563 + + $tb = "$tl" + (Repeat-Char $h 118) + "$tr" + $bm = "$ml" + (Repeat-Char $h 118) + "$mr" + $bb = "$bl" + (Repeat-Char $h 118) + "$br" + $sp = "$v" + (Repeat-Char " " 118) + "$v" + + Write-Host "" + Write-Host "$tb" -ForegroundColor Cyan + Write-Host "$sp" -ForegroundColor Cyan + Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan + + #Write-Host "$v $b$b$tl$h$h$b$b$tr$b$b$tl$h$h$h$h$br$b$b$v $b$b$v$b$b$tl$h$h$h$h$br$b$b$tl$h$h$h$h$br$bl$h$h$b$b$tl$h$h$br$b$b$v $b$b$v$b$b$tl$h$h$b$b$tr $v" -ForegroundColor Cyan + Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$h$h$br" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$h$h$br" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$h$h$br$bl$h$h" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$br" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan + + #Write-Host "$v $b$b$v $b$b$v$b$b$b$b$b$tr $b$b$v $b$b$v$b$b$b$b$b$b$b$tr$b$b$b$b$b$tr $b$b$v $b$b$v $b$b$v$b$b$b$b$b$b$tl$br $v" -ForegroundColor Cyan + Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$br" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan + + #Write-Host "$v $b$b$v $b$b$v$b$b$tl$h$h$br $bl$b$b$tr $b$b$tl$br$bl$h$h$h$h$b$b$v$b$b$tl$h$h$br $b$b$v $b$b$v $b$b$v$b$b$tl$h$h$h$br $v" -ForegroundColor Cyan + Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$br $bl" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$br$bl$h$h$h$h" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$br " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$h$h$h$br" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan + + #Write-Host "$v $b$b$b$b$b$b$tl$br$b$b$b$b$b$b$b$tr $bl$b$b$b$b$tl$br $b$b$b$b$b$b$b$v$b$b$b$b$b$b$b$tr $b$b$v $bl$b$b$b$b$b$b$tl$br$b$b$v $v" -ForegroundColor Cyan + Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr $bl" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$br " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tr " -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v $bl" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine + Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine + Write-Host "$b$b" -ForegroundColor White -NoNewLine + Write-Host "$v" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan + + Write-Host "$v" (Repeat-Char " " 24) "$bl$h$h$h$h$h$br $bl$h$h$h$h$h$h$br $bl$h$h$h$br $bl$h$h$h$h$h$h$br$bl$h$h$h$h$h$h$br $bl$h$br $bl$h$h$h$h$h$br $bl$h$br" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan + + Write-Host "$v" -ForegroundColor Cyan -NoNewline + $version = Get-DevSetupVersion -Local + $versionDisplay = "Development Environment Manager v$version" + $paddedAction = $versionDisplay.PadLeft(($versionDisplay.Length + 118) / 2).PadRight(118) + Write-Host "$paddedAction" -ForegroundColor White -NoNewline + Write-Host "$v" -ForegroundColor Cyan + Write-Host "$sp" -ForegroundColor Cyan + Write-Host "$bm" -ForegroundColor Cyan + + + $actionDisplay = switch ($selectedAction) { + 'install' { ">> INSTALLING Development Environment" } + 'installpath' { ">> INSTALLING Development Environment From Path" } + 'installurl' { ">> INSTALLING Development Environment From Url" } + 'update' { ">> UPDATING DevSetup System" } + 'init' { ">> INITIALIZING DevSetup System" } + 'export' { ">> EXPORTING Current Configuration" } + 'list' { ">> LISTING Available Environments" } + 'uninstall' { ">> UNINSTALLING Development Environment" } + } + + $paddedAction = $actionDisplay.PadLeft(($actionDisplay.Length + 118) / 2).PadRight(118) + Write-Host "$v" -ForegroundColor Cyan -NoNewline + Write-Host "$paddedAction" -ForegroundColor Yellow -NoNewline + Write-Host "$v" -ForegroundColor Cyan + Write-Host "$bb" -ForegroundColor Cyan + Write-Host "" + + switch ($selectedAction) { + {$_ -eq 'install' -or $_ -eq 'installpath' -or $_ -eq 'installurl'} { + Write-Host "Installing development environment..." -ForegroundColor Yellow + $ParameterCopy = [hashtable]$PSBoundParameters + $ParameterCopy.Remove('Install') + Install-DevSetupEnv @ParameterCopy + } + 'update' { + Write-Host "Updating devsetup system..." -ForegroundColor Yellow + Update-DevSetup | Out-Null + } + 'init' { + Write-Host "Initializing DevSetup system..." -ForegroundColor Yellow + Initialize-DevSetup | Out-Null + } + 'export' { + Write-Host "Exporting current development environment..." -ForegroundColor Yellow + Export-DevSetupEnv -Name $Name + } + 'list' { + Show-DevSetupEnvList -Platform $Platform + } + 'uninstall' { + Write-Host "Uninstalling development environment..." -ForegroundColor Yellow + Uninstall-DevSetupEnv -Name $Name + } + } + + #Write-Host "DevSetup action '$selectedAction' completed successfully!" -ForegroundColor Green + } + catch { + Write-Error "Error executing DevSetup action '$selectedAction': $_" + } +} \ No newline at end of file From 5d806261f58f694ecddd623ecdbfd34ebc5c9155 Mon Sep 17 00:00:00 2001 From: Joshua Wilson Date: Fri, 29 Aug 2025 01:09:07 -0500 Subject: [PATCH 2/2] Trying again? --- devsetup/DevSetup.psd1 | 136 ------ devsetup/DevSetup.psm1 | 48 -- .../ConvertFrom-3rdPartyInstall.Tests.ps1 | 50 --- .../3rdParty/ConvertFrom-3rdPartyInstall.ps1 | 17 - .../ConvertFrom-VisualStudioInstall.ps1 | 149 ------- .../VisualStudio/Export-VssConfig.ps1 | 59 --- .../VisualStudio/Import-VssConfig.ps1 | 33 -- .../ConvertFrom-VisualStudioCodeInstall.ps1 | 186 -------- .../VisualStudioCode/Export-VsCodeConfig.ps1 | 62 --- .../VisualStudioCode/Import-VsCodeConfig.ps1 | 123 ------ .../Commands/Export-DevSetupEnv.Tests.ps1 | 39 -- .../Private/Commands/Export-DevSetupEnv.ps1 | 80 ---- .../Commands/Initialize-DevSetup.Tests.ps1 | 68 --- .../Private/Commands/Initialize-DevSetup.ps1 | 114 ----- .../Commands/Install-DevSetupEnv.Tests.ps1 | 85 ---- .../Private/Commands/Install-DevSetupEnv.ps1 | 152 ------- .../Commands/Show-DevSetupEnvList.Tests.ps1 | 124 ------ .../Private/Commands/Show-DevSetupEnvList.ps1 | 171 -------- .../Commands/Uninstall-DevSetupEnv.Tests.ps1 | 75 ---- .../Commands/Uninstall-DevSetupEnv.ps1 | 105 ----- devsetup/Private/Enums/InstalledState.ps1 | 11 - devsetup/Private/Enums/TaskState.ps1 | 9 - ...port-InstalledChocolateyPackages.Tests.ps1 | 102 ----- .../Export-InstalledChocolateyPackages.ps1 | 234 ---------- .../Get-ChocolateyCacheFile.Tests.ps1 | 32 -- .../Chocolatey/Get-ChocolateyCacheFile.ps1 | 65 --- ...et-ChocolateyPackageDependencies.Tests.ps1 | 96 ---- .../Get-ChocolateyPackageDependencies.ps1 | 82 ---- .../Get-ChocolateyVersion.Tests.ps1 | 46 -- .../Chocolatey/Get-ChocolateyVersion.ps1 | 79 ---- .../Chocolatey/Install-Chocolatey.Tests.ps1 | 96 ---- .../Chocolatey/Install-Chocolatey.ps1 | 113 ----- .../Install-ChocolateyPackage.Tests.ps1 | 100 ----- .../Chocolatey/Install-ChocolateyPackage.ps1 | 154 ------- .../Install-ChocolateyPackages.Tests.ps1 | 147 ------- .../Chocolatey/Install-ChocolateyPackages.ps1 | 162 ------- .../Chocolatey/Read-ChocolateyCache.Tests.ps1 | 51 --- .../Chocolatey/Read-ChocolateyCache.ps1 | 77 ---- .../Test-ChocolateyInstalled.Tests.ps1 | 25 -- .../Chocolatey/Test-ChocolateyInstalled.ps1 | 65 --- .../Test-ChocolateyPackageInstalled.Tests.ps1 | 75 ---- .../Test-ChocolateyPackageInstalled.ps1 | 118 ----- .../Uninstall-ChocolateyPackage.Tests.ps1 | 50 --- .../Uninstall-ChocolateyPackage.ps1 | 97 ----- .../Uninstall-ChocolateyPackages.Tests.ps1 | 147 ------- .../Uninstall-ChocolateyPackages.ps1 | 150 ------- .../Write-ChocolateyCache.Tests.ps1 | 56 --- .../Chocolatey/Write-ChocolateyCache.ps1 | 88 ---- .../Core/Install-CoreDependencies.Tests.ps1 | 135 ------ .../Core/Install-CoreDependencies.ps1 | 132 ------ .../Providers/Core/Install-GitRepository.ps1 | 173 -------- .../Providers/Core/Install-Nuget.Tests.ps1 | 114 ----- .../Private/Providers/Core/Install-Nuget.ps1 | 118 ----- ...xport-InstalledPowershellModules.Tests.ps1 | 139 ------ .../Export-InstalledPowershellModules.ps1 | 264 ----------- .../Install-PowershellModule.Tests.ps1 | 154 ------- .../Powershell/Install-PowershellModule.ps1 | 147 ------- .../Install-PowershellModules.Tests.ps1 | 170 -------- .../Powershell/Install-PowershellModules.ps1 | 162 ------- .../Test-PowershellModuleInstalled.Tests.ps1 | 98 ----- .../Test-PowershellModuleInstalled.ps1 | 157 ------- .../Uninstall-PowershellModule.Tests.ps1 | 109 ----- .../Powershell/Uninstall-PowershellModule.ps1 | 96 ---- .../Uninstall-PowershellModules.Tests.ps1 | 170 -------- .../Uninstall-PowershellModules.ps1 | 157 ------- .../Export-InstalledScoopPackages.Tests.ps1 | 107 ----- .../Scoop/Export-InstalledScoopPackages.ps1 | 412 ------------------ .../Providers/Scoop/Find-Scoop.Tests.ps1 | 66 --- .../Private/Providers/Scoop/Find-Scoop.ps1 | 86 ---- .../Scoop/Get-ScoopCacheFile.Tests.ps1 | 16 - .../Providers/Scoop/Get-ScoopCacheFile.ps1 | 61 --- .../Scoop/Get-ScoopVersion.Tests.ps1 | 112 ----- .../Providers/Scoop/Get-ScoopVersion.ps1 | 103 ----- .../Providers/Scoop/Install-Scoop.Tests.ps1 | 56 --- .../Private/Providers/Scoop/Install-Scoop.ps1 | 85 ---- .../Scoop/Install-ScoopBucket.Tests.ps1 | 90 ---- .../Providers/Scoop/Install-ScoopBucket.ps1 | 116 ----- .../Scoop/Install-ScoopComponents.Tests.ps1 | 123 ------ .../Scoop/Install-ScoopComponents.ps1 | 256 ----------- .../Scoop/Install-ScoopPackage.Tests.ps1 | 121 ----- .../Providers/Scoop/Install-ScoopPackage.ps1 | 163 ------- .../Providers/Scoop/Read-ScoopCache.Tests.ps1 | 68 --- .../Providers/Scoop/Read-ScoopCache.ps1 | 74 ---- .../Test-ScoopComponentInstalled.Tests.ps1 | 135 ------ .../Scoop/Test-ScoopComponentInstalled.ps1 | 155 ------- .../Scoop/Test-ScoopInstalled.Tests.ps1 | 50 --- .../Providers/Scoop/Test-ScoopInstalled.ps1 | 82 ---- .../Scoop/Uninstall-ScoopBucket.Tests.ps1 | 84 ---- .../Providers/Scoop/Uninstall-ScoopBucket.ps1 | 106 ----- .../Scoop/Uninstall-ScoopComponents.Tests.ps1 | 137 ------ .../Scoop/Uninstall-ScoopComponents.ps1 | 225 ---------- .../Scoop/Uninstall-ScoopPackage.Tests.ps1 | 97 ----- .../Scoop/Uninstall-ScoopPackage.ps1 | 102 ----- .../Scoop/Write-ScoopCache.Tests.ps1 | 63 --- .../Providers/Scoop/Write-ScoopCache.ps1 | 80 ---- .../Utils/ConvertFrom-Base64.Tests.ps1 | 43 -- devsetup/Private/Utils/ConvertFrom-Base64.ps1 | 29 -- .../Private/Utils/ConvertTo-Base64.Tests.ps1 | 45 -- devsetup/Private/Utils/ConvertTo-Base64.ps1 | 32 -- .../Private/Utils/Find-GitRepositories.ps1 | 121 ----- devsetup/Private/Utils/Format-PrettyTable.ps1 | 130 ------ .../Utils/Get-DevSetupCachePath.Tests.ps1 | 14 - .../Private/Utils/Get-DevSetupCachePath.ps1 | 60 --- .../Get-DevSetupCommunityEnvPath.Tests.ps1 | 23 - .../Utils/Get-DevSetupCommunityEnvPath.ps1 | 10 - .../Utils/Get-DevSetupEnvPath.Tests.ps1 | 14 - .../Private/Utils/Get-DevSetupEnvPath.ps1 | 10 - .../Utils/Get-DevSetupLocalEnvPath.Tests.ps1 | 23 - .../Utils/Get-DevSetupLocalEnvPath.ps1 | 10 - .../Utils/Get-DevSetupManifest.Tests.ps1 | 22 - .../Private/Utils/Get-DevSetupManifest.ps1 | 26 -- .../Private/Utils/Get-DevSetupPath.Tests.ps1 | 14 - devsetup/Private/Utils/Get-DevSetupPath.ps1 | 9 - .../Utils/Get-DevSetupVersion.Tests.ps1 | 36 -- .../Private/Utils/Get-DevSetupVersion.ps1 | 104 ----- .../Utils/Get-EnvironmentVariable.Tests.ps1 | 35 -- .../Private/Utils/Get-EnvironmentVariable.ps1 | 10 - .../Private/Utils/Get-PwshVersion.Tests.ps1 | 26 -- devsetup/Private/Utils/Get-PwshVersion.ps1 | 10 - .../Utils/Initialize-DevSetupEnvs.Tests.ps1 | 139 ------ .../Private/Utils/Initialize-DevSetupEnvs.ps1 | 90 ---- .../Utils/Optimize-DevSetupEnvs.Tests.ps1 | 127 ------ .../Private/Utils/Optimize-DevSetupEnvs.ps1 | 93 ---- .../Utils/Read-ConfigurationFile.Tests.ps1 | 43 -- .../Private/Utils/Read-ConfigurationFile.ps1 | 7 - .../Utils/Test-OperatingSystem.Tests.ps1 | 62 --- .../Private/Utils/Test-OperatingSystem.ps1 | 30 -- .../Utils/Test-RunningAsAdmin.Tests.ps1 | 46 -- .../Private/Utils/Test-RunningAsAdmin.ps1 | 13 - devsetup/Private/Utils/Write-NewConfig.ps1 | 223 ---------- .../Utils/Write-StatusMessage.Tests.ps1 | 81 ---- .../Private/Utils/Write-StatusMessage.ps1 | 59 --- devsetup/Public/Use-DevSetup.ps1 | 370 ---------------- 133 files changed, 12568 deletions(-) delete mode 100644 devsetup/DevSetup.psd1 delete mode 100644 devsetup/DevSetup.psm1 delete mode 100644 devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 delete mode 100644 devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 delete mode 100644 devsetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 delete mode 100644 devsetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 delete mode 100644 devsetup/Private/Commands/Export-DevSetupEnv.ps1 delete mode 100644 devsetup/Private/Commands/Initialize-DevSetup.Tests.ps1 delete mode 100644 devsetup/Private/Commands/Initialize-DevSetup.ps1 delete mode 100644 devsetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 delete mode 100644 devsetup/Private/Commands/Install-DevSetupEnv.ps1 delete mode 100644 devsetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 delete mode 100644 devsetup/Private/Commands/Show-DevSetupEnvList.ps1 delete mode 100644 devsetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 delete mode 100644 devsetup/Private/Commands/Uninstall-DevSetupEnv.ps1 delete mode 100644 devsetup/Private/Enums/InstalledState.ps1 delete mode 100644 devsetup/Private/Enums/TaskState.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 delete mode 100644 devsetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Core/Install-CoreDependencies.ps1 delete mode 100644 devsetup/Private/Providers/Core/Install-GitRepository.ps1 delete mode 100644 devsetup/Private/Providers/Core/Install-Nuget.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Core/Install-Nuget.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Install-PowershellModule.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Install-PowershellModules.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Find-Scoop.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-Scoop.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Read-ScoopCache.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 delete mode 100644 devsetup/Private/Providers/Scoop/Write-ScoopCache.ps1 delete mode 100644 devsetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 delete mode 100644 devsetup/Private/Utils/ConvertFrom-Base64.ps1 delete mode 100644 devsetup/Private/Utils/ConvertTo-Base64.Tests.ps1 delete mode 100644 devsetup/Private/Utils/ConvertTo-Base64.ps1 delete mode 100644 devsetup/Private/Utils/Find-GitRepositories.ps1 delete mode 100644 devsetup/Private/Utils/Format-PrettyTable.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupCachePath.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupEnvPath.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupManifest.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupPath.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupPath.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-DevSetupVersion.ps1 delete mode 100644 devsetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-EnvironmentVariable.ps1 delete mode 100644 devsetup/Private/Utils/Get-PwshVersion.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Get-PwshVersion.ps1 delete mode 100644 devsetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Initialize-DevSetupEnvs.ps1 delete mode 100644 devsetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Optimize-DevSetupEnvs.ps1 delete mode 100644 devsetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Read-ConfigurationFile.ps1 delete mode 100644 devsetup/Private/Utils/Test-OperatingSystem.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Test-OperatingSystem.ps1 delete mode 100644 devsetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Test-RunningAsAdmin.ps1 delete mode 100644 devsetup/Private/Utils/Write-NewConfig.ps1 delete mode 100644 devsetup/Private/Utils/Write-StatusMessage.Tests.ps1 delete mode 100644 devsetup/Private/Utils/Write-StatusMessage.ps1 delete mode 100644 devsetup/Public/Use-DevSetup.ps1 diff --git a/devsetup/DevSetup.psd1 b/devsetup/DevSetup.psd1 deleted file mode 100644 index b68f608..0000000 --- a/devsetup/DevSetup.psd1 +++ /dev/null @@ -1,136 +0,0 @@ -# -# Module manifest for module 'DevSetup' -# -# Generated by: Joshua Wilson -# -# Generated on: 7/31/2025 -# - -@{ - -# Script module or binary module file associated with this manifest. -RootModule = 'DevSetup.psm1' - -# Version number of this module. -ModuleVersion = '0.0.0' - -# Supported PSEditions -# CompatiblePSEditions = @() - -# ID used to uniquely identify this module -GUID = '316f7319-7063-492b-a5d3-dc32c616f157' - -# Author of this module -Author = 'Joshua Wilson' - -# Company or vendor of this module -CompanyName = 'PwshDevs' - -# Copyright statement for this module -Copyright = '(c) 2025 PwshDevs. All rights reserved.' - -# Description of the functionality provided by this module -Description = '' - -# Minimum version of the PowerShell engine required by this module -PowerShellVersion = '5.1' - -# 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 = @('powershell-yaml', 'VSSetup', 'PowerShellForGitHub', 'EZlog') - -# 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 = @() - -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() - -# 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 = @( - 'Use-DevSetup' -) - -# 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 = @() - -# 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 = @('devsetup') - -# 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 = @('BuildTools', 'Toolchain', 'Build', 'Automation', 'Package', 'Management', 'CMake', 'MSBuild', 'NMake', 'B2', 'Chocolatey', 'MySQL', 'Development') - - # A URL to the license for this module. - # LicenseUri = '' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/pwshdevs/devsetup' - - # A URL to the main environments website for this project. - EnvironmentsProjectUri = 'https://github.com/pwshdevs/devsetup.environments' - - # 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 = '' - -} \ No newline at end of file diff --git a/devsetup/DevSetup.psm1 b/devsetup/DevSetup.psm1 deleted file mode 100644 index 8b88c5f..0000000 --- a/devsetup/DevSetup.psm1 +++ /dev/null @@ -1,48 +0,0 @@ -# DevSetup PowerShell Module - -# Get the current module path -$ModulePath = $PSScriptRoot - -# Get all function files from both Private and Public directories, excluding test files -$PrivateFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$PrivateUtilsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Utils") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$PrivateProvidersFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Providers") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$PrivateCommandsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Commands") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$Private3rdpartyFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\3rdparty") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$PrivateEnumsFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Private\Enums") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } -$PublicFunctions = Get-ChildItem -Path (Join-Path $ModulePath "Public") -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Tests.ps1" } - -# Combine all function files -$AllFunctions = @() -if ($PrivateFunctions) { $AllFunctions += $PrivateFunctions } -if ($PrivateUtilsFunctions) { $AllFunctions += $PrivateUtilsFunctions } -if ($PrivateProvidersFunctions) { $AllFunctions += $PrivateProvidersFunctions } -if ($PrivateCommandsFunctions) { $AllFunctions += $PrivateCommandsFunctions } -if ($Private3rdpartyFunctions) { $AllFunctions += $Private3rdpartyFunctions } -if ($PrivateEnumsFunctions) { $AllFunctions += $PrivateEnumsFunctions } -if ($PublicFunctions) { $AllFunctions += $PublicFunctions } - -# Import all functions -foreach ($FunctionFile in $AllFunctions) { - try { - . $FunctionFile.FullName - Write-Verbose "Imported function from: $($FunctionFile.Name)" - } - catch { - Write-Error "Failed to import function from $($FunctionFile.Name): $_" - } -} - -# Initialize global variables -if (-not $global:InstalledPackages) { - $global:InstalledPackages = @() -} - -if (-not $global:InstalledPackagesFile) { - $global:InstalledPackagesFile = $null -} - -New-Alias -Name devsetup -Value Use-DevSetup - -# Export module members (functions will be exported via the manifest) -Write-Verbose "DevSetup module loaded successfully. $($AllFunctions.Count) functions imported ($($PrivateFunctions.Count) private, $($PrivateUtilsFunctions.Count) utils, $($PrivateProvidersFunctions.Count) providers, $($PrivateCommandsFunctions.Count) commands, $($Private3rdpartyFunctions.Count) 3rdparty, $($PublicFunctions.Count) public)." diff --git a/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 b/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 deleted file mode 100644 index e1383d0..0000000 --- a/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.Tests.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -BeforeAll { - . $PSScriptRoot\ConvertFrom-3rdPartyInstall.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudio\ConvertFrom-VisualStudioInstall.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\3rdParty\VisualStudioCode\ConvertFrom-VisualStudioCodeInstall.ps1 - Mock Write-Host { } - Mock Write-Warning { } - Mock ConvertFrom-VisualStudioInstall { $true } - Mock ConvertFrom-VisualStudioCodeInstall { $true } -} - -Describe "ConvertFrom-3rdPartyInstall" { - - Context "When both conversions succeed" { - It "Should not write any warnings" { - $result = ConvertFrom-3rdPartyInstall -Config "test" - Assert-MockCalled Write-Warning -Exactly 0 -Scope It - Assert-MockCalled Write-Host -Exactly 2 -Scope It - } - } - - Context "When Visual Studio conversion fails" { - It "Should write a warning for Visual Studio" { - Mock ConvertFrom-VisualStudioInstall { $false } - Mock ConvertFrom-VisualStudioCodeInstall { $true } - $result = ConvertFrom-3rdPartyInstall -Config "test" - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Visual Studio installations" } - Assert-MockCalled Write-Host -Exactly 2 -Scope It - } - } - - Context "When Visual Studio Code conversion fails" { - It "Should write a warning for Visual Studio Code" { - Mock ConvertFrom-VisualStudioInstall { $true } - Mock ConvertFrom-VisualStudioCodeInstall { $false } - $result = ConvertFrom-3rdPartyInstall -Config "test" - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Visual Studio Code installation" } - Assert-MockCalled Write-Host -Exactly 2 -Scope It - } - } - - Context "When both conversions fail" { - It "Should write warnings for both" { - Mock ConvertFrom-VisualStudioInstall { $false } - Mock ConvertFrom-VisualStudioCodeInstall { $false } - $result = ConvertFrom-3rdPartyInstall -Config "test" - Assert-MockCalled Write-Warning -Exactly 2 -Scope It - Assert-MockCalled Write-Host -Exactly 2 -Scope It - } - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 b/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 deleted file mode 100644 index 540a0f6..0000000 --- a/devsetup/Private/3rdParty/ConvertFrom-3rdPartyInstall.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -Function ConvertFrom-3rdPartyInstall { - Param( - [string]$Config - ) - - # Convert from Visual Studio installations - Write-Host "`nScanning Visual Studio installations..." -ForegroundColor Cyan - if (-not (ConvertFrom-VisualStudioInstall -Config $Config)) { - Write-Warning "Failed to convert Visual Studio installations, but continuing..." - } - - # Convert from Visual Studio Code installations - Write-Host "`nScanning Visual Studio Code installation..." -ForegroundColor Cyan - if (-not (ConvertFrom-VisualStudioCodeInstall -Config $Config)) { - Write-Warning "Failed to convert Visual Studio Code installation, but continuing..." - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 b/devsetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 deleted file mode 100644 index d619c33..0000000 --- a/devsetup/Private/3rdParty/VisualStudio/ConvertFrom-VisualStudioInstall.ps1 +++ /dev/null @@ -1,149 +0,0 @@ -Function ConvertFrom-VisualStudioInstall { - Param( - [Parameter(Mandatory=$true)] - [string]$Config, - [string]$OutFile, - [switch]$DryRun - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "This operation requires administrator privileges. Please run as administrator." - } - - # Get Visual Studio instances - Write-Host "- Detecting Visual Studio installations..." -ForegroundColor Gray - $vsInstances = Get-VSSetupInstance - - if (-not $vsInstances) { - Write-Warning "No Visual Studio instances found." - return $true - } - - # Read existing YAML configuration - $YamlData = Read-ConfigurationFile -Config $Config - - # Ensure chocolateyPackages section exists - if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } - if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } - if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } - - foreach ($instance in $vsInstances) { - Write-Host " - Found: $($instance.DisplayName)" -ForegroundColor Gray - - # Convert display name to Chocolatey package name - # Extract year and type separately to ensure correct ordering - $displayName = $instance.DisplayName - $year = if ($displayName -match '(\d{4})') { $matches[1] } else { '' } - $type = '' - if ($displayName -match 'Community') { $type = 'community' } - elseif ($displayName -match 'Professional') { $type = 'professional' } - elseif ($displayName -match 'Enterprise') { $type = 'enterprise' } - - # Build package name as visualstudio - $packageName = "visualstudio$year$type" - - Write-Host " - Converted to Chocolatey package: $packageName" -ForegroundColor Gray - - # Create temporary file for Visual Studio configuration export - $base64Config = Export-VssConfig -VssInstallPath $instance.InstallationPath - - # Create command string for importing the VS configuration - $Command = "Import-VssConfig -EncodedConfigFile '$base64Config' -VssInstallPath '$($instance.InstallationPath)'" - $commandPackageName = "$packageName.importConfig" - - # Check if command already exists for this package - $existingCommand = $YamlData.devsetup.commands | Where-Object { - ($_ -is [hashtable] -and $_.packageName -eq $commandPackageName) - } - - if ($existingCommand) { - Write-Host " - Updating existing VS configuration command..." -ForegroundColor Gray - - # Find index of existing command - $commandIndex = $YamlData.devsetup.commands.IndexOf($existingCommand) - - # Update with new command - $YamlData.devsetup.commands[$commandIndex] = @{ - packageName = $commandPackageName - command = $Command - } - } else { - Write-Host " - Adding new VS configuration command..." -ForegroundColor Gray - - # Add new command - $YamlData.devsetup.commands += @{ - packageName = $commandPackageName - command = $Command - } - } - - $existingPackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { - ($_ -is [string] -and $_ -eq $packageName) -or - ($_ -is [hashtable] -and $_.name -eq $packageName) - } - - if ($existingPackage) { - Write-Host " - Updating existing Visual Studio packages..." -ForegroundColor Gray - - # Find index of existing package - $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) - - # Update with components - $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ - name = $packageName - version = $null - } - } else { - Write-Host " - Adding new Visual Studio package..." -ForegroundColor Gray - - # Add new package with components - $YamlData.devsetup.dependencies.chocolatey.packages += @{ - name = $packageName - version = $null - } - } - } - - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - } - catch { - Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" - $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 - } - - # Handle output based on parameters - if ($DryRun) { - Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan - Write-Host $yamlOutput -ForegroundColor White - Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow - } else { - # Determine output file - $outputFile = if ($OutFile) { $OutFile } else { $Config } - - try { - Write-Debug "`nSaving configuration to: $outputFile" - $yamlOutput | Out-File -FilePath $outputFile -Encoding UTF8 - Write-Debug "Configuration saved successfully!" - } - catch { - Write-Error "Failed to save configuration to $outputFile`: $_" - return $false - } - } - - # "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" export --installPath "" --config ".vsconfig" - # "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "C:\Program Files\Microsoft Visual Studio\\" --config "C:\Path\To\Your\Config.vsconfig" --passive --allowUnsignedExtensions - - Write-Host "Visual Studio installation conversion completed!" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error in Visual Studio installation conversion: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 b/devsetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 deleted file mode 100644 index 7033535..0000000 --- a/devsetup/Private/3rdParty/VisualStudio/Export-VssConfig.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -Function Export-VssConfig { - param ( - [string]$VssInstallPath - ) - - if (-not (Test-Path -Path $VssInstallPath)) { - Write-Error "Visual Studio installation path not found: $VssInstallPath" - return $false - } - - try { - $tempConfigFile = [System.IO.Path]::GetTempFileName() + ".vsconfig" - - # Execute the command - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" export --installPath $VssInstallPath --config "$tempConfigFile" --passive - - # Since setup.exe is async, wait for the config file to be created and populated - $timeout = 60 # seconds - $elapsed = 0 - $pollInterval = 2 # seconds - - Write-Host " - Waiting for Visual Studio export to complete." -ForegroundColor Gray -NoNewline - - while ($elapsed -lt $timeout) { - if ((Test-Path -Path $tempConfigFile) -and (Get-Item $tempConfigFile).Length -gt 0) { - Write-Host "`n - Export completed successfully." -ForegroundColor Gray - break - } - Start-Sleep -Seconds $pollInterval - $elapsed += $pollInterval - Write-Host "." -NoNewline -ForegroundColor Gray - } - - # Check if we timed out - if ($elapsed -ge $timeout) { - Write-Host " - Export operation timed out after $timeout seconds." -ForegroundColor Gray - Write-Warning "Visual Studio export may still be running in the background. Check the installation manually." - } - - if (-not (Test-Path -Path $tempConfigFile)) { - Write-Error "Failed to export Visual Studio configuration to temporary file." - return $false - } - - $encodedConfig = ConvertTo-Base64 -FilePath $tempConfigFile - if (-not $encodedConfig) { - Write-Error "Failed to convert configuration file to Base64." - return $false - } - - # Clean up temporary files - if (Test-Path $tempConfigFile) { Remove-Item $tempConfigFile -Force } - - return $encodedConfig - } catch { - Write-Error "Failed to export configuration to file: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 b/devsetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 deleted file mode 100644 index 268f5fb..0000000 --- a/devsetup/Private/3rdParty/VisualStudio/Import-VssConfig.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -Function Import-VssConfig { - param ( - [string]$EncodedConfigFile, - [string]$VssInstallPath - ) - - if (-not $EncodedConfigFile) { - Write-Error "Encoded configuration file is empty." - return $false - } - - try { - # Decode the base64 encoded configuration - $decodedConfig = ConvertFrom-Base64 -EncodedString $EncodedConfigFile - - # Create config file in user's home directory - $configFile = Join-Path -Path $env:USERPROFILE -ChildPath ".vssconfig-devsetup" - - # Write the decoded configuration to the config file - $decodedConfig | Out-File -FilePath $configFile -Encoding UTF8 - - Write-Host "Visual Studio configuration saved to: $configFile" -ForegroundColor Green - - # Run the Visual Studio installer with the config file (suppress output) - & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath $VssInstallPath --config "$configFile" --passive --allowUnsignedExtensions > $null 2>&1 - - return $true - } - catch { - Write-Error "Failed to process Visual Studio configuration: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 b/devsetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 deleted file mode 100644 index 9f01805..0000000 --- a/devsetup/Private/3rdParty/VisualStudioCode/ConvertFrom-VisualStudioCodeInstall.ps1 +++ /dev/null @@ -1,186 +0,0 @@ -Function ConvertFrom-VisualStudioCodeInstall { - Param ( - [string]$Config - ) - - try { - Write-Host "- Detecting Visual Studio Code installation..." -ForegroundColor Gray - - # Read existing configuration - $YamlData = Read-ConfigurationFile -Config $Config - - # Ensure chocolateyPackages section exists - if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } - if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } - - # Check if vscode is already in chocolatey packages - $existingVscodePackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { - ($_ -is [string] -and $_ -eq "vscode") -or - ($_ -is [hashtable] -and $_.name -eq "vscode") - } - - if ($existingVscodePackage) { - Write-Host " - Visual Studio Code already configured in chocolatey packages" -ForegroundColor Green - - # Export VS Code configuration - Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray - $encodedConfig = Export-VsCodeConfig - - if ($encodedConfig) { - # Ensure commands section exists - if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } - - # Check if vscode.importConfig command already exists - $existingCommand = $YamlData.devsetup.commands | Where-Object { - ($_ -is [hashtable] -and $_.packageName -eq "vscode.importConfig") - } - - if ($existingCommand) { - # Update existing command with new encoded config - $existingCommand.command = "Import-VsCodeConfig -EncodedConfig $encodedConfig" - Write-Host " - VS Code import command updated in configuration" -ForegroundColor Green - } - else { - # Add new Import-VsCodeConfig command - $YamlData.devsetup.commands += @{ - command = "Import-VsCodeConfig -EncodedConfig '$encodedConfig'" - packageName = "vscode.importConfig" - } - Write-Host " - VS Code import command added to configuration" -ForegroundColor Green - } - - # Save updated configuration - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - $yamlOutput | Out-File -FilePath $Config -Encoding UTF8 - Write-Host " - Configuration updated successfully" -ForegroundColor Green - } - catch { - Write-Error "Failed to save updated configuration: $_" - return $false - } - } - else { - Write-Host " - No VS Code configuration to export" -ForegroundColor Yellow - } - - return $true - } - - # Check for manual installation using multiple methods - $vscodeInstalled = $false - $detectionMethod = "" - - # Method 1: Check if 'code --version' works - try { - $codeVersion = & code --version 2>$null - if ($LASTEXITCODE -eq 0 -and $codeVersion) { - $vscodeInstalled = $true - $detectionMethod = "command line (code --version)" - Write-Host " - Found VS Code via command line: $($codeVersion[0])" -ForegroundColor Gray - } - } - catch { - # Command not found, continue with other methods - } - - # Method 2: Check registry - if (-not $vscodeInstalled) { - try { - $regPath = "HKLM:\SOFTWARE\Classes\Applications\Code.exe\shell\open\command" - $regValue = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue - if ($regValue) { - $vscodeInstalled = $true - $detectionMethod = "registry" - Write-Host " - Found VS Code via registry" -ForegroundColor Gray - } - } - catch { - # Registry check failed, continue - } - } - - # Method 3: Filesystem checks - if (-not $vscodeInstalled) { - $userPath = "$env:LocalAppData\Programs\Microsoft VS Code\bin\code.cmd" - $systemPath = "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd" - - if (Test-Path $userPath) { - $vscodeInstalled = $true - $detectionMethod = "user installation path" - Write-Host " - Found VS Code at: $userPath" -ForegroundColor Gray - } - elseif (Test-Path $systemPath) { - $vscodeInstalled = $true - $detectionMethod = "system installation path" - Write-Host " - Found VS Code at: $systemPath" -ForegroundColor Gray - } - } - - # Method 4: Get-Package check - if (-not $vscodeInstalled) { - try { - $package = Get-Package -Name "*vscode*" -ErrorAction SilentlyContinue - if ($package) { - $vscodeInstalled = $true - $detectionMethod = "package manager" - Write-Host " - Found VS Code via Get-Package: $($package.Name)" -ForegroundColor Gray - } - } - catch { - # Get-Package failed, continue - } - } - - if ($vscodeInstalled) { - Write-Host " - Visual Studio Code detected ($detectionMethod), adding to chocolatey packages" -ForegroundColor Green - - # Add vscode to chocolatey packages - $YamlData.devsetup.dependencies.chocolatey.packages += @{ - name = "vscode" - version = $null - } - - # Export VS Code configuration - Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray - $encodedConfig = Export-VsCodeConfig - - if ($encodedConfig) { - # Ensure commands section exists - if (-not $YamlData.devsetup.commands) { $YamlData.devsetup.commands = @() } - - # Add Import-VsCodeConfig command - $YamlData.devsetup.commands += @{ - command = "Import-VsCodeConfig -EncodedConfig '$encodedConfig'" - packageName = "vscode.importConfig" - } - Write-Host " - VS Code import command added to configuration" -ForegroundColor Green - } - else { - Write-Host " - No VS Code configuration to export" -ForegroundColor Yellow - } - - # Save updated configuration - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - $yamlOutput | Out-File -FilePath $Config -Encoding UTF8 - Write-Host " - Configuration updated successfully" -ForegroundColor Green - } - catch { - Write-Error "Failed to save updated configuration: $_" - return $false - } - } - else { - Write-Host " - Visual Studio Code not detected on this system" -ForegroundColor Yellow - } - - return $true - } - catch { - Write-Error "Error detecting Visual Studio Code installation: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 b/devsetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 deleted file mode 100644 index e67694c..0000000 --- a/devsetup/Private/3rdParty/VisualStudioCode/Export-VsCodeConfig.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -Function Export-VsCodeConfig { - Param( - - ) - - try { - Write-Host " - Exporting VS Code configuration..." -ForegroundColor Gray - - # Check if 'code' command is available - $codeCommand = Get-Command code -ErrorAction SilentlyContinue - if (-not $codeCommand) { - Write-Warning "VS Code 'code' command not found in PATH. Cannot export extensions." - return $null - } - - Write-Host " - VS Code command found, listing extensions..." -ForegroundColor Gray - - # Get list of installed extensions - try { - $command = { - & code --list-extensions 2>$null - } - $extensionsOutput = Invoke-Command -ScriptBlock $command - if ($LASTEXITCODE -ne 0) { - Write-Warning "Failed to get VS Code extensions list" - return $null - } - - # Convert output to array (filter out empty lines) - $extensionsArray = $extensionsOutput | Where-Object { $_ -and $_.Trim() -ne "" } - - if (-not $extensionsArray -or $extensionsArray.Count -eq 0) { - Write-Host " - No VS Code extensions found" -ForegroundColor Yellow - return $null - } - - Write-Host " - Found $($extensionsArray.Count) VS Code extensions" -ForegroundColor Gray - - # Convert array to JSON - $jsonData = $extensionsArray | ConvertTo-Json - - # Convert JSON to Base64 - $base64Config = ConvertTo-Base64 -InputString $jsonData - - if (-not $base64Config) { - Write-Error "Failed to encode VS Code extensions to Base64" - return $null - } - - Write-Host " - VS Code extensions exported and encoded successfully" -ForegroundColor Gray - return $base64Config - } - catch { - Write-Error "Error getting VS Code extensions: $_" - return $null - } - } - catch { - Write-Error "Error exporting VS Code configuration: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 b/devsetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 deleted file mode 100644 index 9208043..0000000 --- a/devsetup/Private/3rdParty/VisualStudioCode/Import-VsCodeConfig.ps1 +++ /dev/null @@ -1,123 +0,0 @@ -Function Import-VsCodeConfig { - Param( - [string]$EncodedConfig - ) - - try { - Write-Host "- Importing VS Code configuration..." -ForegroundColor Gray - - if (-not $EncodedConfig) { - Write-Warning "No encoded configuration provided" - return $false - } - - # Check if 'code' command is available - $codeCommand = Get-Command code -ErrorAction SilentlyContinue - $codePath = $null - - if ($codeCommand) { - $codePath = "code" - Write-Host " - VS Code command found in PATH" -ForegroundColor Gray - } - else { - # Manual path checks when code command is not in PATH - $userPath = "$env:LocalAppData\Programs\Microsoft VS Code\bin\code.cmd" - $systemPath = "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd" - - if (Test-Path $userPath) { - $codePath = $userPath - Write-Host " - VS Code found at user path: $userPath" -ForegroundColor Gray - } - elseif (Test-Path $systemPath) { - $codePath = $systemPath - Write-Host " - VS Code found at system path: $systemPath" -ForegroundColor Gray - } - } - - if (-not $codePath) { - Write-Warning "VS Code executable not found. Cannot install extensions." - return $false - } - - Write-Host " - VS Code command found, decoding configuration..." -ForegroundColor Gray - - # Decode the base64 configuration - $decodedJson = ConvertFrom-Base64 -EncodedString $EncodedConfig - if (-not $decodedJson) { - Write-Error "Failed to decode base64 configuration" - return $false - } - - Write-Host " - Configuration decoded, parsing JSON..." -ForegroundColor Gray - - # Convert from JSON - try { - $extensions = $decodedJson | ConvertFrom-Json - } - catch { - Write-Error "Failed to parse JSON from decoded configuration: $_" - return $false - } - - # Handle both array and single string cases - if ($extensions -is [string]) { - # Single extension - $extensionList = @($extensions) - } - elseif ($extensions -is [array]) { - # Array of extensions - $extensionList = $extensions - } - else { - Write-Error "Unexpected extension data type: $($extensions.GetType())" - return $false - } - - if ($extensionList.Count -eq 0) { - Write-Host " - No extensions to install" -ForegroundColor Yellow - return $true - } - - Write-Host " - Installing $($extensionList.Count) VS Code extensions..." -ForegroundColor Gray - - $successCount = 0 - $failureCount = 0 - - # Install each extension - foreach ($extension in $extensionList) { - if (-not $extension -or $extension.Trim() -eq "") { - continue - } - - Write-Host " - Installing extension: $extension" -ForegroundColor Gray - - try { - $command = { - & $codePath --install-extension $extension --force 2>&1 - } - $result = Invoke-Command -ScriptBlock $command - if ($LASTEXITCODE -eq 0) { - Write-Host " - Successfully installed: $extension" -ForegroundColor Green - $successCount++ - } - else { - Write-Warning " - Failed to install: $extension - $result" - $failureCount++ - } - } - catch { - Write-Warning " - Error installing: $extension - $_" - $failureCount++ - } - } - - # Summary - Write-Host " - Extension installation complete: $successCount successful, $failureCount failed" -ForegroundColor Gray - - return $true - } - catch { - Write-Error "Error importing VS Code configuration: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 b/devsetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 deleted file mode 100644 index d4e4e23..0000000 --- a/devsetup/Private/Commands/Export-DevSetupEnv.Tests.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Export-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Write-NewConfig.ps1 - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } - Mock Get-DevSetupLocalEnvPath { "TestDrive:\DevSetupEnvs\local"} - Mock Write-NewConfig { param($OutFile) $OutFile } - Mock Write-Host { } - Mock Write-Error { } -} - -Describe "Export-DevSetupEnv" { - - Context "When called with a valid name" { - It "Should create the config file and return its path" { - $result = Export-DevSetupEnv -Name "MyEnv" - $result | Should -Be "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" - Assert-MockCalled Write-NewConfig -Exactly 1 -Scope It -ParameterFilter { $OutFile -eq "TestDrive:\DevSetupEnvs\local\MyEnv.devsetup" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "exported to" -and $ForegroundColor -eq "Green" } - } - } - - Context "When called with a name that needs sanitization" { - It "Should sanitize the name and warn" { - $result = Export-DevSetupEnv -Name "Data Science Environment!" - $result | Should -Be "TestDrive:\DevSetupEnvs\local\DataScienceEnvironment.devsetup" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "sanitized" -and $ForegroundColor -eq "Yellow" } - } - } - - Context "When Write-NewConfig fails" { - It "Should write error and return null" { - Mock Write-NewConfig { param($OutFile) $null } - $result = Export-DevSetupEnv -Name "FailEnv" - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to create configuration file" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Export-DevSetupEnv.ps1 b/devsetup/Private/Commands/Export-DevSetupEnv.ps1 deleted file mode 100644 index 8efc008..0000000 --- a/devsetup/Private/Commands/Export-DevSetupEnv.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -<# -.SYNOPSIS - Exports the current system environment to a new DevSetup configuration file. - -.DESCRIPTION - This function creates a new DevSetup environment configuration by scanning the current system - for installed packages and components. It automatically sanitizes the environment name to ensure - file system compatibility and exports the configuration to a YAML file in the DevSetup - environments directory. The function captures the current state of PowerShell modules, - Chocolatey packages, and other installed components for later reproduction. - -.PARAMETER Name - The name for the new environment configuration. - This parameter is mandatory and will be sanitized to contain only alphanumeric characters, hyphens, and periods. - The resulting YAML file will be named "{Name}.yaml" in the DevSetup environments directory. - -.OUTPUTS - [System.String] - Returns the full path to the created configuration file if successful. - Returns $null if the export operation fails. - -.EXAMPLE - Export-DevSetupEnv -Name "MyCurrentSetup" - - Exports the current system state to a configuration file named "MyCurrentSetup.yaml". - -.EXAMPLE - $configPath = Export-DevSetupEnv -Name "WebDev-2024" - if ($configPath) { - Write-Host "Configuration saved to: $configPath" - } else { - Write-Host "Export failed" - } - - Demonstrates capturing the return value to verify export success. - -.EXAMPLE - Export-DevSetupEnv -Name "Data Science Environment!" - - The exclamation mark will be removed, resulting in "DataScienceEnvironment.yaml". - A warning message will indicate the sanitization that occurred. - -.NOTES - - Automatically sanitizes the environment name by removing non-alphanumeric characters except hyphens and periods - - Displays a warning message if sanitization changes the original name - - Uses Get-DevSetupEnvPath to determine the target directory for the configuration file - - Calls Write-NewConfig to perform the actual system scanning and file creation - - Returns the full file path on success for further processing or verification - - Returns $null if Write-NewConfig fails to create the configuration - - The exported configuration can be used with Install-DevSetupEnv to recreate the environment - - Provides color-coded console output: Yellow for warnings, Green for success, Red for errors - -.LINK - -.COMPONENT - DevSetup.Commands - -.FUNCTIONALITY - Environment Export, Configuration Creation, System State Capture -#> - -Function Export-DevSetupEnv { - Param( - [string]$Name - ) - # Sanitize EnvName to only contain alphanumeric characters, hyphens, and periods - $sanitizedEnvName = $Name -replace '[^a-zA-Z0-9\-\.]', '' - if ($sanitizedEnvName -ne $Name) { - Write-Host "EnvName sanitized from '$Name' to '$sanitizedEnvName' (removed non-alphanumeric characters)" -ForegroundColor Yellow - } - $Name = $sanitizedEnvName - $OutFile = Join-Path -Path (Get-DevSetupLocalEnvPath) -ChildPath "$Name.devsetup" - $config = Write-NewConfig -OutFile $OutFile - if (-not $config) { - Write-Error "Failed to create configuration file" - return $null - } - Write-Host "Configuration file exported to: $OutFile" -ForegroundColor Green - return $OutFile -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Initialize-DevSetup.Tests.ps1 b/devsetup/Private/Commands/Initialize-DevSetup.Tests.ps1 deleted file mode 100644 index 1f2d442..0000000 --- a/devsetup/Private/Commands/Initialize-DevSetup.Tests.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Initialize-DevSetup.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Core\Install-CoreDependencies.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Initialize-DevSetupEnvs.ps1 - Mock Write-Host { } - Mock Write-Error { } - Mock Write-Verbose { } - Mock Install-CoreDependencies { $true } - Mock Get-DevSetupPath { "TestDrive:\Users\Test\devsetup" } - Mock Get-DevSetupEnvPath { "TestDrive:\Users\Test\devsetup\envs" } - Mock Get-DevSetupLocalEnvPath { "TestDrive:\Users\Test\devsetup\envs\local" } - Mock Get-DevSetupCommunityEnvPath { "TestDrive:\Users\Test\devsetup\envs\community" } - Mock Test-Path { $false } - Mock New-Item { } - Mock Initialize-DevSetupEnvs { "TestDrive:\Users\Test\devsetup\envs" } -} - -Describe "Initialize-DevSetup" { - - Context "When all steps succeed" { - It "Should install dependencies, create directories, and return true" { - $result = Initialize-DevSetup - $result | Should -Be $true - Assert-MockCalled Install-CoreDependencies -Exactly 1 -Scope It - Assert-MockCalled New-Item -Exactly 1 -Scope It - Assert-MockCalled Initialize-DevSetupEnvs -Exactly 1 -Scope It - #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "initialized at" -and $ForegroundColor -eq "Green" } - } - } - - Context "When core dependencies fail to install" { - It "Should write error and return nothing" { - Mock Install-CoreDependencies { $false } - $result = Initialize-DevSetup - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install core dependencies" } - } - } - - Context "When .devsetup directory already exists" { - It "Should not create the directory and should log verbose" { - Mock Test-Path { $true } - $result = Initialize-DevSetup - $result | Should -Be $true - Assert-MockCalled New-Item -Exactly 0 -Scope It - Assert-MockCalled Write-Verbose -Scope It -ParameterFilter { $Message -match "already exists" } - } - } - - Context "When environment path initialization fails" { - It "Should write error and return false" { - Mock Initialize-DevSetupEnvs { $null } - $result = Initialize-DevSetup - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment path" } - } - } - - Context "When an exception occurs during initialization" { - It "Should write error and return false" { - Mock Install-CoreDependencies { throw "Unexpected error" } - $result = Initialize-DevSetup - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Initialize-DevSetup.ps1 b/devsetup/Private/Commands/Initialize-DevSetup.ps1 deleted file mode 100644 index bd50b5f..0000000 --- a/devsetup/Private/Commands/Initialize-DevSetup.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -<# -.SYNOPSIS - Initializes the DevSetup environment and directory structure. - -.DESCRIPTION - This function sets up the complete DevSetup environment by installing core dependencies and creating - the necessary directory structure. It performs a comprehensive initialization process including - dependency validation, directory creation, and environment path setup. The function ensures all - prerequisites are in place before DevSetup can be used for environment management operations. - -.OUTPUTS - [System.Boolean] - Returns $true if the DevSetup environment is successfully initialized. - Returns $false if initialization fails at any step. - -.EXAMPLE - Initialize-DevSetup - - Initializes the complete DevSetup environment with default settings. - -.EXAMPLE - if (Initialize-DevSetup) { - Write-Host "DevSetup is ready for use" - # Proceed with environment operations - } else { - Write-Host "DevSetup initialization failed" - # Handle initialization failure - } - - Demonstrates conditional logic based on initialization success. - -.EXAMPLE - $setupReady = Initialize-DevSetup - if ($setupReady) { - Use-DevSetup -List - } - - Shows using the function result to proceed with DevSetup operations. - -.NOTES - - This should be the first function called when setting up DevSetup - - Performs initialization in a specific sequence: - 1. Installs core dependencies via Install-CoreDependencies - 2. Creates the main .devsetup directory using Get-DevSetupPath - 3. Initializes the environments directory via Initialize-DevSetupEnvs - - Uses fail-fast approach - stops immediately if core dependencies cannot be installed - - Creates the .devsetup directory in the user's home directory if it doesn't exist - - Uses -Force flag for directory creation to handle any permission issues - - Suppresses directory creation output using Out-Null for clean console experience - - Provides verbose logging when .devsetup directory already exists - - Validates each initialization step and returns appropriate success/failure status - - Includes comprehensive try-catch error handling with descriptive error messages - - Color-coded console output for different phases: Cyan for progress, Green for success - -.LINK - -.COMPONENT - DevSetup.Commands - -.FUNCTIONALITY - Environment Setup, Directory Management, Dependency Installation -#> - -Function Initialize-DevSetup { - try { - # Install core dependencies first - Write-Host "- Installing core dependencies..." -ForegroundColor Cyan - if (-not (Install-CoreDependencies)) { - Write-Error "Failed to install core dependencies" - return - } - Write-Host "- Core dependencies installed successfully" -ForegroundColor Green - - # Define .devsetup folder path - $devSetupPath = Get-DevSetupPath - - # Check if .devsetup folder exists - if (-not (Test-Path -Path $devSetupPath)) { - #Write-Host "Creating .devsetup directory at: $devSetupPath" -ForegroundColor Cyan - New-Item -Path $devSetupPath -ItemType Directory -Force | Out-Null - #Write-Host ".devsetup directory created successfully" -ForegroundColor Green - } else { - Write-Verbose ".devsetup directory already exists at: $devSetupPath" - } - - Write-Host "" - Write-Host "- Installing community environments..." -ForegroundColor Cyan - # Initialize DevSetup environments path - $envSetupPath = Initialize-DevSetupEnvs - if (-not $envSetupPath) { - Write-Error "Failed to initialize DevSetup environment path" - return $false - } else { - Write-Host "- Community environments installed successfully" -ForegroundColor Green - } - - Write-Host "" - Write-Host "Path Information: " -ForegroundColor Yellow - Write-Host "- DevSetup:" -ForegroundColor Cyan - Write-Host " - $devSetupPath" -ForegroundColor Gray - Write-Host "- Local Environments: " -ForegroundColor Cyan - Write-Host " - $($envSetupPath.Local)" -ForegroundColor Gray - Write-Host "- Community Environments: " -ForegroundColor Cyan - Write-Host " - $($envSetupPath.Community)" -ForegroundColor Gray - Write-Host "" - - # Return the path for use by other functions - return $true - } - catch { - Write-Error "Failed to initialize DevSetup environment: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 b/devsetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 deleted file mode 100644 index 05a5b21..0000000 --- a/devsetup/Private/Commands/Install-DevSetupEnv.Tests.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Install-PowershellModules.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackages.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Install-ScoopComponents.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - Mock Get-DevSetupEnvPath { "C:\DevSetupEnvs" } - Mock Test-Path { $true } - Mock Read-ConfigurationFile { } - Mock Install-PowershellModules { } - Mock Install-ChocolateyPackages { } - Mock Install-ScoopComponents { } - Mock Write-Host { } - Mock Write-Error { } - Mock Write-Warning { } - Mock Invoke-Command { } - Mock Invoke-Expression { } -} - -Describe "Install-DevSetupEnv" { - - Context "When environment file does not exist" { - It "Should write error and return" { - Mock Test-Path { $false } - $result = Install-DevSetupEnv -Name "missing-env" - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } - } - } - - Context "When YAML parsing fails" { - It "Should write error and return" { - Mock Test-Path { $true } - Mock Read-ConfigurationFile { $null } - $result = Install-DevSetupEnv -Name "bad-yaml" - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } - } - } - - Context "When all dependencies install and no commands are present" { - It "Should install dependencies and write status" { - Mock Test-Path { $true } - Mock Read-ConfigurationFile { @{ devsetup = @{ } } } - $result = Install-DevSetupEnv -Name "basic-env" - $result | Should -Be $null - Assert-MockCalled Install-PowershellModules -Exactly 1 -Scope It - Assert-MockCalled Install-ChocolateyPackages -Exactly 1 -Scope It - Assert-MockCalled Install-ScoopComponents -Exactly 1 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No commands found" } - } - } - - Context "When commands are present and executed" { - It "Should execute all commands" { - $commands = @( - @{ command = "echo Hello"; packageName = "git" }, - @{ command = "echo World"; packageName = "nodejs" } - ) - Mock Test-Path { $true } - Mock Read-ConfigurationFile { @{ devsetup = @{ commands = $commands } } } - $result = Install-DevSetupEnv -Name "cmd-env" - $result | Should -Be $null - Assert-MockCalled Invoke-Expression -Exactly 2 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: git" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Executing command for: nodejs" } - } - } - - Context "When a command entry is missing the command property" { - It "Should skip and warn" { - $commands = @( - @{ packageName = "git" }, - @{ command = "echo World"; packageName = "nodejs" } - ) - Mock Test-Path { $true } - Mock Read-ConfigurationFile { @{ devsetup = @{ commands = $commands } } } - $result = Install-DevSetupEnv -Name "missing-cmd" - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "missing command property" } - Assert-MockCalled Invoke-Expression -Exactly 1 -Scope It - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Install-DevSetupEnv.ps1 b/devsetup/Private/Commands/Install-DevSetupEnv.ps1 deleted file mode 100644 index 5382ed1..0000000 --- a/devsetup/Private/Commands/Install-DevSetupEnv.ps1 +++ /dev/null @@ -1,152 +0,0 @@ -<# -.SYNOPSIS - Installs a complete development environment from a YAML configuration file. - -.DESCRIPTION - This function orchestrates the installation of a development environment by reading a YAML configuration file - and processing all defined dependencies and commands. It sequentially installs PowerShell modules, Chocolatey - packages, Scoop buckets and packages, then executes any custom commands specified in the configuration. - The function provides comprehensive error handling and progress reporting throughout the installation process. - -.PARAMETER Name - The name of the environment configuration file to install (without the .yaml extension). - The function will look for a file named "{Name}.yaml" in the DevSetup environment path. - This parameter is mandatory and accepts positional input. - -.OUTPUTS - None. This function does not return a value but writes status information to the console. - -.EXAMPLE - Install-DevSetupEnv -Name "development" - - Installs the development environment from the "development.yaml" configuration file. - -.EXAMPLE - Install-DevSetupEnv "web-dev" - - Installs the web development environment using positional parameter syntax. - -.EXAMPLE - Install-DevSetupEnv -Name "my-environment" - - Demonstrates the PSCustomObject structure that would be parsed from the YAML file. - -.NOTES - - Requires the environment YAML file to exist in the DevSetup environment path - - Uses Get-DevSetupEnvPath to determine the configuration file location - - Returns early with error if YAML file is not found or cannot be parsed - - Processes dependencies in a specific order: PowerShell modules, Chocolatey packages, then Scoop components - - Commands are executed after all package installations are complete - - Individual installation failures do not stop the overall process - - Uses Read-ConfigurationFile to parse YAML configuration - - Leverages Install-PowershellModules, Install-ChocolateyPackages, and Install-ScoopComponents functions - - Custom commands are executed using Invoke-CommandFromEnv function - - Provides detailed console output with color-coded status messages - - Skips command entries that are missing the required command property - - Command execution includes package name context for better traceability - -.LINK - -.COMPONENT - DevSetup.Commands - -.FUNCTIONALITY - Environment Installation, Configuration Processing, Development Setup -#> - -Function Install-DevSetupEnv { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, Position=0, ParameterSetName = "Install")] - [string]$Name, - [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallPath")] - [string]$Path, - [Parameter(Mandatory=$true, Position=0, ParameterSetName = "InstallUrl")] - [string]$Url - ) - - $YamlFile = $null - - if($PSBoundParameters.ContainsKey('Name')) { - $Provider = "local" - - if($Name -like "*:*") { - $parts = $Name.Split(":") - $Name = $parts[1]; - $Provider = $parts[0] - } - - $YamlFile = Join-Path -Path (Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider) -ChildPath "$Name.devsetup" - } elseif($PSBoundParameters.ContainsKey('Path')) { - if(-not (Test-Path -Path $Path)) { - Write-Error "Invalid Path provided" - return - } - $YamlFile = $Path - } elseif($PSBoundParameters.ContainsKey('Url')) { - $FileName = Split-Path $Url -Leaf - Write-Host "Downloading DevSetup environment from:" -ForegroundColor Cyan - Write-Host "- $Url" -ForegroundColor Gray - $YamlFile = Join-Path -Path (Get-DevSetupLocalEnvPath) -ChildPath $FileName - Write-Host "Saving Devsetup environment file to:" -ForegroundColor Cyan - Write-Host "- $YamlFile" -ForegroundColor Gray - if((Test-Path -Path $YamlFile)) { - Write-Warning "File $YamlFile already exists" - do { - if(($sAnswer = Read-Host "Overwrite existing file and continue? [Y/N]") -eq '') { $sAnswer = 'N' } - } until ($sAnswer.ToUpper()[0] -match '[yYnN]') - if(-not ($sAnswer.ToUpper()[0] -match '[Y]')) { - return - } - } - try { - Invoke-WebRequest -Uri $Url -OutFile $YamlFile | Out-Null - } catch { - Write-Error "Failed to download devsetup env file" - return - } - } - - if (-not (Test-Path $YamlFile)) { - Write-Error "Environment file not found: $YamlFile" - return - } - - Write-Host "Installing DevSetup environment from:" -ForegroundColor Cyan - Write-Host "- $YamlFile" -ForegroundColor Gray - Write-Host "" - - # Read the configuration from the YAML file - $YamlData = Read-ConfigurationFile -Config $YamlFile - - # Check if YAML data was successfully parsed - if ($null -eq $YamlData) { - Write-Error "Failed to parse YAML configuration from: $YamlFile" - return - } - - # Install PowerShell module dependencies - Install-PowershellModules -YamlData $YamlData | Out-Null - - # Install Chocolatey package dependencies - Install-ChocolateyPackages -YamlData $YamlData | Out-Null - - # Install Scoop package dependencies - Install-ScoopComponents -YamlData $YamlData | Out-Null - - # Execute any commands defined in the configuration - if ($YamlData.devsetup.commands -and $YamlData.devsetup.commands.Count -gt 0) { - Write-Host "Executing configuration commands..." -ForegroundColor Cyan - - foreach ($commandEntry in $YamlData.devsetup.commands) { - if ($commandEntry.command) { - Write-Host " - Executing command for: $($commandEntry.packageName)" -ForegroundColor Gray - Invoke-Expression -Command $commandEntry.command *> $null - } else { - Write-Warning "Skipping command entry with missing command property" - } - } - } else { - Write-Host "No commands found in configuration to execute." -ForegroundColor Gray - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 b/devsetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 deleted file mode 100644 index 422e8cd..0000000 --- a/devsetup/Private/Commands/Show-DevSetupEnvList.Tests.ps1 +++ /dev/null @@ -1,124 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Show-DevSetupEnvList.ps1 - . $PSScriptRoot\Show-DevSetupEnvList.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Optimize-DevSetupEnvs.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Format-PrettyTable.ps1 - Mock Get-DevSetupPath { "C:\DevSetup" } - Mock Optimize-DevSetupEnvs { } - Mock Write-Host { } - Mock Write-Warning { } - Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) $false } - Mock Test-Path { $true } - Mock Get-Content { '[{"name":"EnvWin","version":"1.0","platform":"windows","file":"envwin.yaml"},{"name":"EnvLinux","version":"2.0","platform":"linux","file":"envlinux.yaml"},{"name":"EnvMac","version":"3.0","platform":"macos","file":"envmac.yaml"},{"name":"EnvCross","version":"4.0","platform":"cross-platform","file":"envcross.yaml"},{"name":"EnvUnspec","version":"5.0","file":"envunspec.yaml"}]' } - Mock ConvertFrom-Json { - @( - @{ name = "EnvWin"; version = "1.0"; platform = "windows"; file = "envwin.yaml" }, - @{ name = "EnvLinux"; version = "2.0"; platform = "linux"; file = "envlinux.yaml" }, - @{ name = "EnvMac"; version = "3.0"; platform = "macos"; file = "envmac.yaml" }, - @{ name = "EnvCross"; version = "4.0"; platform = "cross-platform"; file = "envcross.yaml" }, - @{ name = "EnvUnspec"; version = "5.0"; file = "envunspec.yaml" } - ) - } -} - -Describe "Show-DevSetupEnvList" { - Context "When environments.json does not exist" { - It "Should run optimization to create it" { - Mock Test-Path { $false } - Show-DevSetupEnvList -Platform "all" - Assert-MockCalled Optimize-DevSetupEnvs -Exactly 1 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No environments index found" } - } - } - - Context "When environments.json is corrupt" { - It "Should run optimization to recreate it" { - Mock Test-Path { $true } - Mock Get-Content { throw "corrupt file" } - Show-DevSetupEnvList -Platform "all" - Assert-MockCalled Optimize-DevSetupEnvs -Exactly 1 -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to read environments.json" } - } - } - - Context "When filtering for current platform (windows)" { - It "Should detect windows platform and filter environments" { - Mock Format-PrettyTable { } - Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($Windows) { $true } else { $false } } - Show-DevSetupEnvList -Platform "current" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: windows" } - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } - - Context "When filtering for current platform (linux)" { - It "Should detect linux platform and filter environments" { - Mock Format-PrettyTable { } - Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($Linux) { $true } else { $false } } - Show-DevSetupEnvList -Platform "current" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: linux" } - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } - - Context "When filtering for current platform (macos)" { - It "Should detect macos platform and filter environments" { - Mock Format-PrettyTable { } - Mock Test-OperatingSystem { param($Windows, $Linux, $MacOS) if ($MacOS) { $true } else { $false } } - Show-DevSetupEnvList -Platform "current" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for current platform: macos" } - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } - - Context "When filtering for all platforms" { - It "Should show all environments without filtering" { - Mock Format-PrettyTable { } - Show-DevSetupEnvList -Platform "all" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Showing all environments regardless of platform" } - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } - - Context "When filtering for specific platform (windows)" { - It "Should filter and display only windows-compatible environments" { - Mock Format-PrettyTable { } - Show-DevSetupEnvList -Platform "windows" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Filtering for platform: windows" } - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } - - Context "When no environments are found for a platform" { - It "Should display guidance message" { - Mock Format-PrettyTable { } - Mock ConvertFrom-Json { @() } - Show-DevSetupEnvList -Platform "windows" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No development environments found for platform: windows" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Use -Platform 'all' to see all available environments" } - Assert-MockCalled Format-PrettyTable -Exactly 0 -Scope It - } - } - - Context "When no environments exist at all" { - It "Should display no environments found message" { - Mock Format-PrettyTable { } - Mock ConvertFrom-Json { @() } - Show-DevSetupEnvList -Platform "all" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "No development environments found." } - Assert-MockCalled Format-PrettyTable -Exactly 0 -Scope It - } - } - - Context "When environments are found" { - It "Should display the environments table and count" { - Mock Format-PrettyTable { } - Mock Write-Host { } - Show-DevSetupEnvList -Platform "all" - Assert-MockCalled Format-PrettyTable -Exactly 1 -Scope It - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Show-DevSetupEnvList.ps1 b/devsetup/Private/Commands/Show-DevSetupEnvList.ps1 deleted file mode 100644 index 70d60d7..0000000 --- a/devsetup/Private/Commands/Show-DevSetupEnvList.ps1 +++ /dev/null @@ -1,171 +0,0 @@ -<# -.SYNOPSIS - Lists available development environment configurations with platform filtering. - -.DESCRIPTION - This function displays all available development environment configurations in a formatted table. - It supports platform-specific filtering to show only environments compatible with the current - system or a specified platform. The function reads environment metadata from environments.json - and automatically creates this index file if it doesn't exist using Optimize-DevSetupEnvs. - Environments can be filtered by Windows, Linux, macOS, or shown for all platforms. - -.PARAMETER Platform - The platform to filter environments by. - Valid values: "current", "all", "windows", "linux", "macos" - Default value is "current" which shows environments for the detected platform. - Use "all" to display environments regardless of platform compatibility. - -.OUTPUTS - [System.Boolean] - Returns $true when the function completes successfully, regardless of whether environments are found. - -.EXAMPLE - Show-DevSetupEnvList - - Lists development environments compatible with the current platform. - -.EXAMPLE - Show-DevSetupEnvList -Platform "all" - - Displays all available development environments regardless of platform. - -.EXAMPLE - Show-DevSetupEnvList -Platform "linux" - - Shows only environments specifically designed for Linux systems. - -.EXAMPLE - Show-DevSetupEnvList -Platform "windows" - - Lists environments compatible with Windows systems. - -.NOTES - - Automatically detects the current platform using [System.Environment]::OSVersion.Platform - - Maps platform detection: Win32NT → windows, Unix → linux/macos via uname command - - Uses 'uname -s' command on Unix systems to distinguish between Linux (default) and macOS (Darwin) - - Reads environment metadata from environments.json in the DevSetup directory - - Automatically creates environments.json index if missing using Optimize-DevSetupEnvs - - Recreates the index file if environments.json is corrupted or unreadable JSON - - Supports cross-platform environments that work on multiple operating systems - - Includes environments with empty/unspecified platform as compatible with all platforms - - Platform filtering includes exact matches, "cross-platform" tagged environments, and unspecified platforms - - Displays results in a formatted table showing Name, Version, Platform, and File columns - - Shows "Not specified" for missing platform information and "Unknown" for missing version - - Provides helpful guidance when no environments are found for the specified platform - - Platform filtering and matching is case-insensitive for user convenience - - Displays environment count summary after the table - - Uses color-coded console output for better user experience - -.LINK - -.COMPONENT - DevSetup.Commands - -.FUNCTIONALITY - Environment Discovery, Platform Detection, Configuration Listing -#> - -Function Show-DevSetupEnvList { - Param ( - [Parameter(Mandatory=$false, Position=0)] - [ValidateSet("current", "all", "windows", "linux", "macos")] - [string]$Platform = "current" # Default to current platform - ) - - - Write-Host "Listing available development environments..." -ForegroundColor Yellow - - # Determine the platform filter - $platformFilter = $Platform.ToLower() - if ($platformFilter -eq "current") { - # Get current system platform - if((Test-OperatingSystem -Windows)) { - $platformFilter = "windows" - } elseif((Test-OperatingSystem -Linux)) { - $platformFilter = "linux" - } elseif((Test-OperatingSystem -MacOS)) { - $platformFilter = "macos" - } else { - $platformFilter = "windows" - } - Write-Host "Filtering for current platform: $platformFilter" -ForegroundColor Gray - } elseif ($platformFilter -eq "all") { - Write-Host "Showing all environments regardless of platform" -ForegroundColor Gray - } else { - Write-Host "Filtering for platform: $platformFilter" -ForegroundColor Gray - } - - # Get the environments.json file path - $devSetupPath = Get-DevSetupPath - $environmentsJsonPath = Join-Path -Path $devSetupPath -ChildPath "environments.json" - - if (-not (Test-Path $environmentsJsonPath)) { - Write-Host "No environments index found. Running optimization to create it..." -ForegroundColor Cyan - Optimize-DevSetupEnvs | Out-Null - } else { - try { - # Read the environments.json file - $jsonContent = Get-Content -Path $environmentsJsonPath -Raw - $environments = $jsonContent | ConvertFrom-Json - } - catch { - Write-Warning "Failed to read environments.json. Running optimization to recreate it..." - Optimize-DevSetupEnvs | Out-Null - } - } - - # Filter environments by platform - if ($platformFilter -ne "all") { - $filteredEnvironments = @() - foreach ($env in $environments) { - $envPlatform = if ($env.platform) { $env.platform.ToLower() } else { "" } - # Match exact platform or cross-platform environments - if ($envPlatform -eq $platformFilter) { - $filteredEnvironments += $env - } - } - $environments = $filteredEnvironments - } - - if ($environments.Count -eq 0) { - if ($platformFilter -eq "all") { - Write-Host "No development environments found." -ForegroundColor Yellow - } else { - Write-Host "No development environments found for platform: $platformFilter" -ForegroundColor Yellow - Write-Host "Use -Platform 'all' to see all available environments." -ForegroundColor Gray - } - return $true - } - - # Create a formatted table - $tableData = @() - foreach ($env in $environments) { - $platformDisplay = if ($env.platform) { $env.platform } else { "Not specified" } - $versionDisplay = if ($env.version) { $env.version } else { "Unknown" } - $tableData += @{ - Name = $env.name - Version = $versionDisplay - Platform = $platformDisplay - File = $env.file - Provider = $env.provider - Color = "DarkGray" - } - } - - $columnDefinitions = [ordered]@{ - Name = @{ Name = "Name"; Width = 32; Alignment = "Left"; Color = "White"; Key = "Name" } - Version = @{ Name = "Version"; Width = 10; Alignment = "Center"; Color = "White"; Key = "Version" } - Platform = @{ Name = "Platform"; Width = 15; Alignment = "Center"; Color = "White"; Key = "Platform" } - Provider = @{ Name = "Provider"; Width = 15; Alignment = "Center"; Color = "White"; Key = "Provider" } - File = @{ Name = "File"; Width = 42; Alignment = "Left"; Color = "White"; Key = "File" } - } - - $tableFormat = @{ - BorderColor = "DarkGray" - } - - Format-PrettyTable -Columns $columnDefinitions -Rows $tableData -TableFormat $tableFormat - - Write-Host "Found $($environments.Count) environment(s)" -ForegroundColor Cyan - Write-Host "" -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 b/devsetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 deleted file mode 100644 index 24bb3ed..0000000 --- a/devsetup/Private/Commands/Uninstall-DevSetupEnv.Tests.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-DevSetupEnv.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Utils\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Scoop\Uninstall-ScoopComponents.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\Chocolatey\Uninstall-ChocolateyPackages.ps1 - . $PSScriptRoot\..\..\..\DevSetup\Private\Providers\PowerShell\Uninstall-PowershellModules.ps1 - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } - Mock Test-Path { $true } - Mock Read-ConfigurationFile { } - Mock Uninstall-PowershellModules { $true } - Mock Uninstall-ChocolateyPackages { $true } - Mock Uninstall-ScoopComponents { $true } - Mock Write-Host { } - Mock Write-Error { } -} - -Describe "Uninstall-DevSetupEnv" { - - Context "When environment file does not exist" { - It "Should write error and return" { - Mock Test-Path { $false } - $result = Uninstall-DevSetupEnv -Name "missing-env" - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Environment file not found" } - } - } - - Context "When YAML parsing fails" { - It "Should write error and return" { - Mock Test-Path { $true } - Mock Read-ConfigurationFile { $null } - $result = Uninstall-DevSetupEnv -Name "bad-yaml" - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to parse YAML" } - } - } - - Context "When all uninstallers succeed" { - It "Should call all uninstallers and write status" { - Mock Test-Path { $true } - Mock Read-ConfigurationFile { @{ devsetup = @{ } } } - $result = Uninstall-DevSetupEnv -Name "basic-env" - $result | Should -Be $null - Assert-MockCalled Uninstall-PowershellModules -Exactly 1 -Scope It - Assert-MockCalled Uninstall-ChocolateyPackages -Exactly 1 -Scope It - Assert-MockCalled Uninstall-ScoopComponents -Exactly 1 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Uninstalling DevSetup environment from:" } - } - } - - Context "When a component uninstaller fails" { - It "Should continue calling other uninstallers" { - $script:callCount = 0 - Mock Uninstall-PowershellModules { $script:callCount++; $false } - Mock Uninstall-ChocolateyPackages { $script:callCount++; $true } - Mock Uninstall-ScoopComponents { $script:callCount++; $true } - Mock Test-Path { $true } - Mock Read-ConfigurationFile { @{ devsetup = @{ } } } - $result = Uninstall-DevSetupEnv -Name "partial-fail" - $result | Should -Be $null - $script:callCount | Should -Be 3 - } - } - - Context "When an exception occurs during uninstall" { - It "Should write error and return" { - Mock Test-Path { $true } - Mock Read-ConfigurationFile { throw "Unexpected error" } - $result = Uninstall-DevSetupEnv -Name "exception-env" - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Commands/Uninstall-DevSetupEnv.ps1 b/devsetup/Private/Commands/Uninstall-DevSetupEnv.ps1 deleted file mode 100644 index a2b4190..0000000 --- a/devsetup/Private/Commands/Uninstall-DevSetupEnv.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -<# -.SYNOPSIS - Uninstalls a development environment configuration and removes all associated packages. - -.DESCRIPTION - This function removes a complete development environment by uninstalling all packages and components - defined in a YAML configuration file. It processes PowerShell modules, Chocolatey packages, and - Scoop packages in sequence, effectively reversing the installation performed by Install-DevSetupEnv. - The function validates the configuration file exists and can be parsed before proceeding with - the uninstallation process. - -.PARAMETER Name - The name of the environment configuration to uninstall. - This parameter is mandatory and must match an existing YAML configuration file in the DevSetup environments directory. - The file should be named "{Name}.yaml" and contain valid DevSetup configuration structure. - -.OUTPUTS - None - This function does not return a value but provides console output indicating the progress of uninstallation operations. - -.EXAMPLE - Uninstall-DevSetupEnv -Name "WebDev" - - Uninstalls all packages and components from the "WebDev" environment configuration. - -.EXAMPLE - Uninstall-DevSetupEnv "DataScience" - - Removes the complete "DataScience" development environment using positional parameter. - -.EXAMPLE - $envName = "GameDev" - Uninstall-DevSetupEnv -Name $envName - - Demonstrates using a variable to specify the environment name for uninstallation. - -.NOTES - - Requires the specified environment configuration file to exist in the DevSetup environments directory - - Uses Get-DevSetupEnvPath to locate the environments directory - - Validates YAML file existence before attempting to parse configuration - - Processes uninstallation in specific order: - 1. PowerShell modules via Uninstall-PowershellModules - 2. Chocolatey packages via Uninstall-ChocolateyPackages - 3. Scoop packages via Uninstall-ScoopComponents - - Each uninstaller function handles its own error reporting and validation - - Does not remove the YAML configuration file itself after uninstallation - - Provides descriptive error messages for missing or invalid configuration files - - Status variables are assigned but not currently used for flow control - -.LINK - -.COMPONENT - DevSetup.Commands - -.FUNCTIONALITY - Environment Management, Package Removal, Configuration Processing -#> - -Function Uninstall-DevSetupEnv { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, Position=0)] - [string]$Name - ) - - try { - $Provider = "local" - - if($Name -like "*:*") { - $parts = $Name.Split(":") - $Name = $parts[1]; - $Provider = $parts[0] - } - - $YamlFile = Join-Path -Path (Join-Path -Path (Get-DevSetupEnvPath) -ChildPath $Provider) -ChildPath "$Name.devsetup" - #$YamlFile = Join-Path -Path (Get-DevSetupEnvPath) -ChildPath "$Name.yaml" - if (-not (Test-Path $YamlFile)) { - Write-Error "Environment file not found: $YamlFile" - return - } - - Write-Host "Uninstalling DevSetup environment from: $YamlFile" -ForegroundColor Cyan - - # Read the configuration from the YAML file - $YamlData = Read-ConfigurationFile -Config $YamlFile - - # Check if YAML data was successfully parsed - if ($null -eq $YamlData) { - Write-Error "Failed to parse YAML configuration from: $YamlFile" - return - } - - # Install PowerShell module dependencies - $status = Uninstall-PowershellModules -YamlData $YamlData - - # Install Chocolatey package dependencies - $status = Uninstall-ChocolateyPackages -YamlData $YamlData - - # Install Scoop package dependencies - $status = Uninstall-ScoopComponents -YamlData $YamlData - } catch { - Write-Error "An error occurred during uninstallation: $_" - return - } -} \ No newline at end of file diff --git a/devsetup/Private/Enums/InstalledState.ps1 b/devsetup/Private/Enums/InstalledState.ps1 deleted file mode 100644 index 9a4d05d..0000000 --- a/devsetup/Private/Enums/InstalledState.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -Add-Type -Language CSharp -TypeDefinition @" - [System.FlagsAttribute] - public enum InstalledState { - NotInstalled = 0, - Installed = 1 << 0, - MinimumVersionMet = 1 << 1, - RequiredVersionMet = 1 << 2, - GlobalVersionMet = 1 << 3, - Pass = Installed | MinimumVersionMet | RequiredVersionMet | GlobalVersionMet - } -"@ \ No newline at end of file diff --git a/devsetup/Private/Enums/TaskState.ps1 b/devsetup/Private/Enums/TaskState.ps1 deleted file mode 100644 index 3f07168..0000000 --- a/devsetup/Private/Enums/TaskState.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -Add-Type -Language CSharp -TypeDefinition @" - [System.FlagsAttribute] - public enum TaskState { - Unknown = 0, - Pass = 1 << 0, - Warn = 1 << 1, - Fail = 1 << 2, - } -"@ \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 deleted file mode 100644 index 0c0cf58..0000000 --- a/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.Tests.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -BeforeAll { - function ConvertTo-Yaml { } - . $PSScriptRoot\Export-InstalledChocolateyPackages.ps1 - . $PSScriptRoot\Get-ChocolateyPackageDependencies.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Get-ChocolateyPackageDependencies { @('chocolatey-core.extension') } - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } - Mock ConvertTo-Yaml { param($obj) "yaml-output" } - Mock ConvertTo-Json { param($obj) "json-output" } - Mock Out-File { $true } - Mock Write-Host { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Write-Debug { } - Mock Write-Verbose { } -} - -Describe "Export-InstalledChocolateyPackages" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It - } - } - - Context "When no Chocolatey packages are found" { - It "Should warn and return true" { - Mock Test-RunningAsAdmin { $true } - Mock Invoke-Expression { @() } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No Chocolatey packages found" } - } - } - - Context "When Chocolatey packages are found and DryRun is used" { - It "Should display the YAML output and not write to file" { - Mock Invoke-Expression { @("git|2.40.0", "nodejs|18.16.0") } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Times 0 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } - } - } - - Context "When Chocolatey packages are found and OutFile is specified" { - It "Should write the YAML output to the specified file" { - Mock Invoke-Expression { @("git|2.40.0", "nodejs|18.16.0") } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" -OutFile "out.yaml" - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } - } - } - - Context "When YAML conversion fails" { - It "Should fallback to JSON output" { - Mock Invoke-Expression { @("git|2.40.0") } - Mock ConvertTo-Yaml { throw "YAML error" } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Json -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } - } - } - - Context "When Out-File fails" { - It "Should write error and return false" { - Mock Invoke-Expression { @("git|2.40.0") } - Mock Out-File { throw "File error" } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } - } - } - - Context "When package version changes" { - It "Should update the package version in the config" { - Mock Invoke-Expression { @("git|2.41.0") } - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @(@{ name = "git"; version = "2.40.0" }) } } } } } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating package: git" } - } - } - - Context "When package is new" { - It "Should add the package to the config" { - Mock Invoke-Expression { @("newpkg|1.0.0") } - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @() } } } } } - $result = Export-InstalledChocolateyPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding package: newpkg" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 b/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 deleted file mode 100644 index c259a60..0000000 --- a/devsetup/Private/Providers/Chocolatey/Export-InstalledChocolateyPackages.ps1 +++ /dev/null @@ -1,234 +0,0 @@ -<# -.SYNOPSIS - Exports installed Chocolatey packages to a YAML configuration file. - -.DESCRIPTION - This function scans the system for installed Chocolatey packages and exports them to a YAML - configuration file in DevSetup format. It uses 'choco list --local-only --limit-output' to retrieve - comprehensive package information including versions. The function intelligently filters out - system packages and can update existing configuration files by merging new packages with existing ones. - -.PARAMETER Config - The path to the YAML configuration file to read from and write to. - This parameter is mandatory and specifies both the input and output file unless OutFile is specified. - -.PARAMETER OutFile - The path to save the updated YAML configuration. - Optional parameter that allows saving to a different file than the input Config file. - -.PARAMETER DryRun - Switch parameter that prevents writing to files and displays the resulting configuration to the console. - Useful for previewing changes before committing them to a file. - -.OUTPUTS - [System.Boolean] - Returns $true if the export completes successfully or if no packages are found. - Returns $false if there are errors during the export process. - -.EXAMPLE - Export-InstalledChocolateyPackages -Config "environment.yaml" - - Exports installed Chocolatey packages to the existing environment.yaml configuration file. - -.EXAMPLE - Export-InstalledChocolateyPackages -Config "current.yaml" -OutFile "backup.yaml" - - Reads from current.yaml and saves the updated configuration with installed packages to backup.yaml. - -.EXAMPLE - Export-InstalledChocolateyPackages -Config "dev-env.yaml" -DryRun - - Shows what the configuration would look like without actually saving to file. - -.NOTES - - Requires administrator privileges to access all installed packages - - Uses 'choco list --local-only --limit-output' for machine-readable package information - - Automatically filters out system packages: - * Packages ending with '.install' (installer packages) - * Packages starting with 'chocolatey' (Chocolatey system packages) - - Merges with existing YAML configuration, preserving other sections and structure - - Supports both simple string format and complex object format for packages - - Updates existing packages when versions have changed - - Converts string entries to hashtable format when version information is added - - Creates the devsetup.dependencies.chocolatey structure if it doesn't exist - - Provides detailed console output with color-coded status messages for operations - - Handles YAML conversion errors gracefully by falling back to JSON format - - Tracks package changes: new additions, version updates, and no-change skips - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Configuration Export, Package Discovery, YAML Generation -#> - -Function Export-InstalledChocolateyPackages { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Config, - [Parameter(Mandatory = $false)] - [ValidateNotNullOrEmpty()] - [string]$OutFile, - [Parameter(Mandatory = $false)] - [switch]$DryRun - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "This operation requires administrator privileges. Please run as administrator." - } - - # Get list of installed Chocolatey packages - Write-Host "- Getting list of installed Chocolatey packages..." -ForegroundColor Gray - $chocoList = Invoke-Expression "& choco list --local-only --limit-output" - - if (-not $chocoList) { - Write-Warning "No Chocolatey packages found or Chocolatey is not installed." - return $true - } - - $chocolateyPackages = @() - - $packagesToIgnore = Get-ChocolateyPackageDependencies | Select-Object -Unique - - foreach ($line in $chocoList) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - - # Parse package info (format: packagename|version) - $parts = $line.Split('|') - if ($parts.Count -ge 2) { - $packageName = $parts[0].Trim() - $version = $parts[1].Trim() - - # Skip packages starting with chocolatey - if ($packageName -like "chocolatey*") { - Write-Verbose "Skipping chocolatey package: $packageName" - continue - } - - if($packagesToIgnore -contains $packageName) { - Write-Verbose "Skipping ignored package: $packageName" - continue - } - - Write-Debug "Found package: $packageName (version: $version)" - $chocolateyPackages += @{ - name = $packageName - version = $version - } - } - } - - Write-Debug "Found $($chocolateyPackages.Count) Chocolatey packages (excluding .install and chocolatey* packages)" - - # Read existing YAML configuration - $YamlData = Read-ConfigurationFile -Config $Config - - # Ensure chocolateyPackages section exists - if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } - if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey) { $YamlData.devsetup.dependencies.chocolatey = @{} } - if (-not $YamlData.devsetup.dependencies.chocolatey.packages) { $YamlData.devsetup.dependencies.chocolatey.packages = @() } - - # Add packages to YAML data - foreach ($package in $chocolateyPackages) { - # Check if package already exists - $existingPackage = $YamlData.devsetup.dependencies.chocolatey.packages | Where-Object { - ($_ -is [string] -and $_ -eq $package.name) -or - ($_ -is [hashtable] -and $_.name -eq $package.name) - } - - if (-not $existingPackage) { - Write-Host " - Adding package: $($package.name) ($($package.version))" -ForegroundColor Gray - $YamlData.devsetup.dependencies.chocolatey.packages += @{ - name = $package.name - version = $package.version - } - } else { - # Package exists, check if version has changed - $existingVersion = $null - if ($existingPackage -is [hashtable] -and $existingPackage.version) { - $existingVersion = $existingPackage.version - } - - if ($existingVersion -and $existingVersion -ne $package.version) { - Write-Host " - Updating package: $($package.name) ($existingVersion -> $($package.version))" -ForegroundColor Cyan - - # Find index and update - $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) - - # Preserve existing package structure but update version - if ($existingPackage -is [string]) { - # Convert string to hashtable with version - $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ - name = $package.name - version = $package.version - } - } else { - # Update existing hashtable - $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version - } - } elseif (-not $existingVersion) { - Write-Host " - Updating package: $($package.name)" -ForegroundColor Yellow - - # Find index and add version - $index = $YamlData.devsetup.dependencies.chocolatey.packages.IndexOf($existingPackage) - - if ($existingPackage -is [string]) { - # Convert string to hashtable with version - $YamlData.devsetup.dependencies.chocolatey.packages[$index] = @{ - name = $package.name - version = $package.version - } - } else { - # Add version to existing hashtable - $YamlData.devsetup.dependencies.chocolatey.packages[$index].version = $package.version - } - } else { - Write-Host " - Skipping package (No Change): $($package.name) ($($package.version))" -ForegroundColor Gray - } - } - } - - # Convert to YAML - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - } - catch { - Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" - $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 - } - - # Handle output based on parameters - if ($DryRun) { - Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan - Write-Host $yamlOutput -ForegroundColor White - Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow - } else { - # Determine output file - $outputFile = if ($OutFile) { $OutFile } else { $Config } - - try { - Write-Debug "`nSaving configuration to: $outputFile" - $yamlOutput | Out-File -FilePath $outputFile - Write-Debug "Configuration saved successfully!" - } - catch { - Write-Error "Failed to save configuration to $outputFile`: $_" - return $false - } - } - - Write-Host "Chocolatey packages conversion completed!" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error converting Chocolatey packages: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 deleted file mode 100644 index bf6cb94..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.Tests.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1 - Mock Write-Error { } -} - -Describe "Get-ChocolateyCacheFile" { - - Context "When Get-DevSetupCachePath returns a valid path" { - It "Should return the correct cache file path" { - Mock Get-DevSetupCachePath { return "C:\Users\Test\.devsetup\.cache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "C:\Users\Test\.devsetup\.cache\chocolatey.cache" - } - } - - Context "When Get-DevSetupCachePath returns a different path" { - It "Should append chocolatey.cache to the returned path" { - Mock Get-DevSetupCachePath { return "D:\DevSetupCache" } - $result = Get-ChocolateyCacheFile - $result | Should -Be "D:\DevSetupCache\chocolatey.cache" - } - } - - Context "When Get-DevSetupCachePath returns an empty string" { - It "Should write error and return null" { - Mock Get-DevSetupCachePath { return "" } - $result = Get-ChocolateyCacheFile - $result | Should -Be $null - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 deleted file mode 100644 index 78472fb..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyCacheFile.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -<# -.SYNOPSIS - Gets the file path for the Chocolatey package cache file. - -.DESCRIPTION - This function constructs and returns the full path to the Chocolatey package cache file within the DevSetup - cache directory. The cache file is used to store information about installed Chocolatey packages and their - versions for performance optimization and offline reference. The function uses Get-DevSetupCachePath - to ensure the cache directory exists before returning the file path. - -.OUTPUTS - [System.String] - Returns the full path to the Chocolatey cache file (chocolatey.cache) within the DevSetup cache directory. - -.EXAMPLE - Get-ChocolateyCacheFile - - Returns the path to the Chocolatey cache file, e.g., "C:\Users\Username\.devsetup\.cache\chocolatey.cache" - -.EXAMPLE - $chocoCacheFile = Get-ChocolateyCacheFile - if (Test-Path $chocoCacheFile) { - $cachedData = Get-Content $chocoCacheFile - } - - Gets the cache file path and checks if it exists before reading cached data. - -.EXAMPLE - $cacheFile = Get-ChocolateyCacheFile - Export-Clixml -Path $cacheFile -InputObject $chocolateyPackages - - Uses the cache file path to save Chocolatey package information. - -.NOTES - - Uses Get-DevSetupCachePath to ensure the cache directory exists - - Returns a consistent file path (chocolatey.cache) within the DevSetup cache structure - - The cache file is used for storing Chocolatey package metadata and version information - - Does not create the cache file itself - only returns the path where it should be located - - Used by other Chocolatey-related functions for performance optimization and data persistence - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Path Management, Cache Management, File System Operations -#> - -Function Get-ChocolateyCacheFile { - [CmdletBinding()] - Param() - - # Get the DevSetup cache path - $cachePath = Get-DevSetupCachePath - if([string]::IsNullOrEmpty($cachePath)) { - Write-Error "Failed to retrieve DevSetup cache path." - return $null - } - - # Construct the full path to the cache file - $cacheFilePath = Join-Path -Path $cachePath -ChildPath "chocolatey.cache" - - return $cacheFilePath -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 deleted file mode 100644 index 100e8d1..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.Tests.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-ChocolateyPackageDependencies.ps1 - Mock Write-Debug { } - $isPS5 = $PSVersionTable.PSVersion.Major -eq 5 -} - -Describe "Get-ChocolateyPackageDependencies" { - - Context "When Chocolatey install path does not exist" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $false } - $result = Get-ChocolateyPackageDependencies - $result | Should -Be $null - } - } - - Context "When no nuspec files are found" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $true } - Mock Get-ChildItem { @() } - $result = Get-ChocolateyPackageDependencies - $result | Should -Be $null - } - } - - Context "When nuspec files have no dependencies" { - It "Should return $null in PS5, empty array in PS6+" { - Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) - } - Mock Get-Content { - '' - } - $result = Get-ChocolateyPackageDependencies - $result | Should -Be $null - } - } - - Context "When nuspec files have dependencies including chocolatey system packages" { - It "Should return only non-chocolatey dependencies" { - Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" } - ) - } - Mock Get-Content { - ' - - - - ' - } - $result = Get-ChocolateyPackageDependencies - $result | Should -Not -Be $null - $result | Should -Contain "git" - $result | Should -Contain "nodejs" - $result | Should -Not -Contain "chocolatey-core.extension" - } - } - - Context "When multiple nuspec files have overlapping dependencies" { - It "Should return all dependencies including duplicates" { - Mock Test-Path { return $true } - Mock Get-ChildItem { - @( - [PSCustomObject]@{ FullName = "C:\choco\lib\foo\foo.nuspec" }, - [PSCustomObject]@{ FullName = "C:\choco\lib\bar\bar.nuspec" } - ) - } - $nuspecs = @( - ' - - - ', - ' - - - ' - ) - $script:callCount = 0 - Mock Get-Content -MockWith { - $nuspecs[$script:callCount++] - } - $result = Get-ChocolateyPackageDependencies - $result | Should -Not -Be $null - $result | Should -Contain "git" - $result | Should -Contain "nodejs" - $result | Should -Contain "python" - ($result | Where-Object { $_ -eq "nodejs" }).Count | Should -Be 2 - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 deleted file mode 100644 index 0c4e424..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyPackageDependencies.ps1 +++ /dev/null @@ -1,82 +0,0 @@ -<# -.SYNOPSIS - Retrieves all package dependencies from installed Chocolatey packages. - -.DESCRIPTION - This function scans all installed Chocolatey packages and extracts their dependency information - by parsing the .nuspec files in the Chocolatey lib directory. It reads the XML metadata from - each package's nuspec file and collects all non-Chocolatey dependencies into a consolidated - list. The function automatically filters out Chocolatey-specific dependencies to focus on - actual package dependencies. - -.OUTPUTS - [System.Array] - Returns an array of package dependency names (strings) found across all installed packages. - Returns an empty array if no dependencies are found or Chocolatey is not installed. - -.EXAMPLE - Get-ChocolateyPackageDependencies - - Returns all package dependencies from installed Chocolatey packages. - -.EXAMPLE - $dependencies = Get-ChocolateyPackageDependencies - if ($dependencies.Count -gt 0) { - Write-Host "Found $($dependencies.Count) dependencies" - $dependencies | ForEach-Object { Write-Host "- $_" } - } - - Demonstrates retrieving and displaying all package dependencies. - -.EXAMPLE - $allDeps = Get-ChocolateyPackageDependencies - $uniqueDeps = $allDeps | Select-Object -Unique | Sort-Object - - Gets all dependencies and creates a sorted list of unique dependency names. - -.NOTES - - Requires Chocolatey to be installed with packages in the standard lib directory - - Uses $Env:ChocolateyInstall environment variable to locate the Chocolatey installation - - Scans all .nuspec files recursively in the Chocolatey lib directory - - Parses XML metadata from nuspec files to extract dependency information - - Automatically filters out dependencies with IDs starting with "chocolatey" (Chocolatey system packages) - - Returns all dependencies in a flat array, including duplicates from multiple packages - - Provides debug logging for troubleshooting package discovery issues - - Returns empty array gracefully if Chocolatey installation path is not found - - Uses ForEach-Object (%) for efficient processing of large package collections - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Dependency Analysis, Package Management, Metadata Extraction -#> - -Function Get-ChocolateyPackageDependencies { - [CmdletBinding()] - Param() - - write-Debug "Retrieving Chocolatey package dependencies..." - $packageDependencies = @() - - $chocolateyInstallPath = Join-Path $Env:ChocolateyInstall lib - if (-not (Test-Path $chocolateyInstallPath)) { - Write-Debug "Chocolatey installation path not found: $chocolateyInstallPath" - return $packageDependencies - } - - Get-ChildItem $chocolateyInstallPath -Recurse "*.nuspec" | % { - $dependencies = ([xml](Get-Content $_.FullName)).package.metadata.dependencies.dependency | Foreach-Object { - if (-not ($_.id -like "chocolatey*")) { - $_.id - } - } - - if ($dependencies) { - $packageDependencies = $packageDependencies + $dependencies; - } - } - return [array]$packageDependencies -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 deleted file mode 100644 index 7494a1f..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.Tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-ChocolateyVersion.ps1 - . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - Mock Write-Warning { } -} - -Describe "Get-ChocolateyVersion" { - - Context "When Chocolatey is not installed" { - It "Should return null and write a warning" { - Mock Test-ChocolateyInstalled { return $false } - $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } - } - } - - Context "When Chocolatey is installed and version is returned" { - It "Should return the trimmed version string" { - Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { " 1.4.0 " } - $result = Get-ChocolateyVersion - $result | Should -Be "1.4.0" - } - } - - Context "When Chocolatey is installed but version is not returned" { - It "Should return null and write a warning" { - Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { $null } - $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to retrieve" } - } - } - - Context "When an error occurs during version retrieval" { - It "Should return null and write a warning" { - Mock Test-ChocolateyInstalled { return $true } - Mock Invoke-Expression { throw "choco error" } - $result = Get-ChocolateyVersion - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "An error occurred" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 b/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 deleted file mode 100644 index 0f14d5d..0000000 --- a/devsetup/Private/Providers/Chocolatey/Get-ChocolateyVersion.ps1 +++ /dev/null @@ -1,79 +0,0 @@ -<# -.SYNOPSIS - Retrieves the version of the installed Chocolatey package manager. - -.DESCRIPTION - This function gets the version information from Chocolatey by executing the 'choco --version' command. - It includes validation to ensure Chocolatey is installed before attempting to retrieve version information - and provides comprehensive error handling with appropriate warning messages for various failure scenarios. - -.OUTPUTS - [System.String] - Returns the Chocolatey version string (trimmed of whitespace) if successful. - Returns $null if Chocolatey is not installed, version retrieval fails, or an error occurs. - -.EXAMPLE - Get-ChocolateyVersion - - Returns the installed Chocolatey version, e.g., "1.4.0" - -.EXAMPLE - $chocoVersion = Get-ChocolateyVersion - if ($chocoVersion) { - Write-Host "Chocolatey version: $chocoVersion" - } else { - Write-Host "Could not determine Chocolatey version" - } - - Demonstrates capturing and validating the version result. - -.EXAMPLE - $version = Get-ChocolateyVersion - if ($version -and [version]$version -lt [version]"1.0.0") { - Write-Warning "Chocolatey version is outdated. Consider upgrading." - } - - Shows version comparison for compatibility checking. - -.NOTES - - Requires Chocolatey to be installed on the system - - Uses Test-ChocolateyInstalled to verify Chocolatey availability before proceeding - - Returns $null immediately if Chocolatey is not installed - - Suppresses stderr output using '2>$null' to avoid console clutter - - Trims whitespace from the version string for clean output - - Includes comprehensive try-catch error handling - - Provides descriptive warning messages for different failure scenarios - - Does not require administrator privileges - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Version Detection, System Information, Package Manager Utilities -#> - -Function Get-ChocolateyVersion { - [CmdletBinding()] - Param( - ) - - if (-not (Test-ChocolateyInstalled)) { - Write-Warning "Chocolatey is not installed. Cannot retrieve version." - return $null - } - - try { - $version = Invoke-Expression "& choco --version" 2>$null - if ($version) { - return $version.Trim() - } else { - Write-Warning "Failed to retrieve Chocolatey version." - return $null - } - } catch { - Write-Warning "An error occurred while trying to get Chocolatey version: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 deleted file mode 100644 index e213399..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.Tests.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-Chocolatey.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } - Mock Write-Host { } - Mock Write-Error { } - Mock Test-RunningAsAdmin { return $true } - Mock Get-Command { $null } - Mock Invoke-Expression { } - Mock Set-ExecutionPolicy { } -} - -Describe "Install-Chocolatey" { - - Context "When not running on Windows" { - It "Should skip installation and return true" { - Mock Test-OperatingSystem { param($Windows) $false } - $result = Install-Chocolatey - $result | Should -Be $true - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -match "not available on this platform" } - } - } - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-OperatingSystem { param($Windows) $true } - Mock Test-RunningAsAdmin { return $false } - $result = Install-Chocolatey - $result | Should -Be $false - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "administrator privileges" } - } - } - - Context "When Chocolatey is already installed" { - It "Should return true and show version" { - Mock Test-OperatingSystem { param($Windows) $true } - Mock Test-RunningAsAdmin { return $true } - Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } - Mock Invoke-Expression { "1.4.0" } - $result = Install-Chocolatey - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } - } - } - - Context "When Chocolatey is not installed and installation succeeds" { - It "Should install and return true" { - Mock Test-OperatingSystem { param($Windows) $true } - $script:installCalled = $false - $script:commandCallCount = 0 - Mock Test-RunningAsAdmin { return $true } - Mock Get-Command -MockWith { - $script:commandCallCount++ - if ($script:commandCallCount -eq 1) { return $null } - else { return [PSCustomObject]@{ Name = "choco" } } - } - Mock Invoke-Expression -MockWith { - param($expr) - if ($expr -like "*--version*") { return "1.4.0" } - $script:installCalled = $true - } - $result = Install-Chocolatey - $result | Should -Be $true - $script:installCalled | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "[OK]" } - } - } - - Context "When Chocolatey is not installed and installation fails" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) $true } - $script:commandCallCount = 0 - Mock Test-RunningAsAdmin { return $true } - Mock Get-Command -MockWith { - $script:commandCallCount++ - return $null - } - Mock Invoke-Expression { } - $result = Install-Chocolatey - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } - } - } - - Context "When an unexpected error occurs" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) $true } - Mock Test-RunningAsAdmin { throw "Unexpected error" } - $result = Install-Chocolatey - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing Chocolatey" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 b/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 deleted file mode 100644 index 66a6744..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-Chocolatey.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -<# -.SYNOPSIS - Installs Chocolatey package manager on Windows systems. - -.DESCRIPTION - This function installs the Chocolatey package manager by downloading and executing the official - installation script from the Chocolatey website. It includes comprehensive validation for platform - compatibility, administrator privileges, and existing installations. The function handles security - protocol configuration and execution policy adjustments required for the installation process. - -.OUTPUTS - [System.Boolean] - Returns $true if Chocolatey is successfully installed or already exists. - Returns $false if the installation fails or system requirements are not met. - -.EXAMPLE - Install-Chocolatey - - Installs Chocolatey package manager on the current system. - -.EXAMPLE - if (Install-Chocolatey) { - Write-Host "Chocolatey is ready for use" - # Proceed with package installations - } else { - Write-Host "Failed to install Chocolatey" - # Handle installation failure - } - - Demonstrates conditional logic based on installation success. - -.EXAMPLE - $chocoReady = Install-Chocolatey - if ($chocoReady) { - choco install git -y - } - - Shows using the function result to proceed with package operations. - -.NOTES - - Requires administrator privileges on Windows systems - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Automatically skips installation on non-Windows platforms (returns $true) - - Checks for existing Chocolatey installation before attempting download - - Sets execution policy to Bypass for the current process scope during installation - - Configures TLS 1.2 security protocol for secure download - - Downloads installation script from https://community.chocolatey.org/install.ps1 - - Verifies successful installation by checking for 'choco' command availability - - Displays version information after successful installation - - Uses comprehensive try-catch error handling with descriptive error messages - - Suppresses command output using Out-Null to avoid console clutter - - Returns $true even if Chocolatey is already installed (idempotent behavior) - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Package Manager Installation, System Setup, Prerequisites Management -#> - -Function Install-Chocolatey { - [CmdletBinding()] - Param() - - try { - # Check if we're on Windows - Chocolatey is Windows-only - if (-not (Test-OperatingSystem -Windows)) { - Write-Host "Chocolatey is not available on this platform. Skipping installation." -ForegroundColor Yellow - return $true - } - - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey installation requires administrator privileges. Please run as administrator." - } - - Write-StatusMessage "- Installing Chocolatey package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - # Check if chocolatey is installed by testing the command - $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue - - if ($chocoInstalled) { - $chocoVersion = Invoke-Expression "& choco --version" 2>$null - #Write-Host "Chocolatey is already installed (version: $chocoVersion)" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - #Write-Host "Chocolatey not found. Installing Chocolatey..." -ForegroundColor Cyan - - # Set security protocols and execution policy - Set-ExecutionPolicy Bypass -Scope Process -Force | Out-Null - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 - - # Download and install Chocolatey - (Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) *> $null) *> $null - - # Verify installation - $chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue - if ($chocoInstalled) { - $chocoVersion = Invoke-Expression "& choco --version" 2>$null - #Write-Host "Chocolatey successfully installed (version: $chocoVersion)!" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - throw "Failed to install Chocolatey" - } - } - return $true - } - catch { - Write-Error "Error checking/installing Chocolatey: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 deleted file mode 100644 index 2299ecf..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.Tests.ps1 +++ /dev/null @@ -1,100 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-ChocolateyPackage.ps1 - . $PSScriptRoot\Test-ChocolateyPackageInstalled.ps1 - . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Test-ChocolateyPackageInstalled { } - Mock Uninstall-ChocolateyPackage { $true } - Mock Get-Command { "choco" } - Mock Invoke-Command { } - Mock Write-ChocolateyCache { $true } - Mock Write-Debug { } - Mock Write-Warning { } - Mock Write-Error { } -} - -Describe "Install-ChocolateyPackage" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } - } - } - - Context "When package is already installed and version matches" { - It "Should return true immediately" { - Mock Test-ChocolateyPackageInstalled { - [InstalledState]::Pass -bor [InstalledState]::Installed - } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $true - } - } - - Context "When package is installed but version does not match" { - It "Should uninstall and reinstall the package" { - Mock Test-ChocolateyPackageInstalled { [InstalledState]::Installed } - $script:uninstallCalled = $false - Mock Uninstall-ChocolateyPackage -MockWith { - param($PackageName) - $script:uninstallCalled = $true - $true - } - $LASTEXITCODE = 0 - Mock Invoke-Command { } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $true - $script:uninstallCalled | Should -Be $true - } - } - - Context "When installing with version and params" { - It "Should build the correct choco command" { - $LASTEXITCODE = 0 - $script:paramsPassed = $null - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } - Mock Invoke-Command -MockWith { - param($ScriptBlock) - $script:paramsPassed = $ScriptBlock.ToString() - } - $result = Install-ChocolateyPackage -PackageName "git" -Version "2.42.0" -Param "/silent" - $result | Should -Be $true - # You can add more checks for $paramsPassed if needed - } - } - - Context "When installation fails (non-zero exit code)" { - It "Should write error and return false" { - $LASTEXITCODE = 1 - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to install" } - } - } - - Context "When Write-ChocolateyCache fails after install" { - It "Should write warning and return false" { - $LASTEXITCODE = 0 - Mock Test-ChocolateyPackageInstalled { [InstalledState]::NotInstalled } - Mock Write-ChocolateyCache { $false } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } - } - } - - Context "When an exception occurs during install" { - It "Should write error and return false" { - Mock Test-ChocolateyPackageInstalled { throw "Unexpected error" } - $result = Install-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error checking/installing package" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 b/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 deleted file mode 100644 index 6342b5f..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackage.ps1 +++ /dev/null @@ -1,154 +0,0 @@ -<# -.SYNOPSIS - Installs a Chocolatey package with optional version and parameter specification. - -.DESCRIPTION - This function installs a Chocolatey package using the 'choco install' command with comprehensive - validation and conflict resolution. It checks for existing installations, handles version conflicts - by reinstalling when necessary, and validates administrator privileges before proceeding. The function - supports custom installation parameters and provides detailed error handling throughout the process. - -.PARAMETER PackageName - The name of the Chocolatey package to install. - This parameter is mandatory and must be a valid, non-empty string representing a Chocolatey package name. - -.PARAMETER Version - The specific version of the package to install. - Optional parameter that specifies the exact version required. If not provided, the latest version is installed. - -.PARAMETER Param - Custom installation parameters to pass to the Chocolatey package. - Optional parameter that allows passing package-specific installation arguments using the --params flag. - -.OUTPUTS - [System.Boolean] - Returns $true if the package was successfully installed or already meets requirements. - Returns $false if the installation failed or insufficient privileges. - -.EXAMPLE - Install-ChocolateyPackage -PackageName "git" - - Installs the latest version of git package. - -.EXAMPLE - Install-ChocolateyPackage -PackageName "nodejs" -Version "18.17.0" - - Installs a specific version of nodejs package. - -.EXAMPLE - Install-ChocolateyPackage -PackageName "googlechrome" -Param "/nogoogle" - - Installs Google Chrome with custom installation parameters. - -.EXAMPLE - $result = Install-ChocolateyPackage -PackageName "vscode" -Version "1.75.0" -Param "/silent" - if ($result) { - Write-Host "Visual Studio Code installed successfully" - } else { - Write-Host "Failed to install Visual Studio Code" - } - - Demonstrates capturing the return value and using custom parameters. - -.NOTES - - Requires administrator privileges to install packages - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Uses Test-ChocolateyPackageInstalled to check existing installations - - Automatically uninstalls existing packages when version conflicts exist - - Uses comprehensive logic to determine installation necessity: - * Returns immediately if package with correct version exists - * Uninstalls and reinstalls if package exists but version differs - * Installs directly if package doesn't exist - - Uses $LASTEXITCODE to verify command execution success - - Includes comprehensive try-catch error handling with descriptive error messages - - Provides detailed debug logging for troubleshooting installation issues - - Suppresses command output using Out-Null to avoid console clutter - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Package Management, Software Installation, Version Control -#> - -Function Install-ChocolateyPackage { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $PackageName, - - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [String] $Version, - - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [String] $Param - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey package installation requires administrator privileges. Please run as administrator." - } - - $testParams = @{ - PackageName = $PackageName - } - - if($PSBoundParameters.ContainsKey('Version')) { - $testParams.Version = $Version - } - - $testResult = Test-ChocolateyPackageInstalled @testParams - - if($testResult.HasFlag([InstalledState]::Pass)) { - return $true - } - - if($testResult.HasFlag([InstalledState]::Installed)) { - Uninstall-ChocolateyPackage -PackageName $PackageName | Out-Null - } - - $installParams = @( - 'install', - '-y', - $PackageName - ) - - if($PSBoundParameters.ContainsKey('Version')) { - $installParams = $installParams + @('--version', $Version) - } - - if($PSBoundParameters.ContainsKey('Param')) { - $installParams = $installParams + @('--params', $Param) - } - - $chocoCommand = Get-Command choco -ErrorAction SilentlyContinue - - $command = { - & $chocoCommand @installParams - } - - Invoke-Command -ScriptBlock $command | Out-Null - - if ($LASTEXITCODE -eq 0) { - Write-Debug "INSTALL:Successfully installed: $PackageName" - if (-not (Write-ChocolateyCache)) { - Write-Warning "Failed to write Chocolatey cache." - return $false - } - return $true - } else { - Write-Error "Failed to install: $PackageName" - return $false - } - } - catch { - Write-Error "Error checking/installing package $PackageName`: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 deleted file mode 100644 index 75d45a7..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.Tests.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-ChocolateyPackages.ps1 - . $PSScriptRoot\Install-ChocolateyPackage.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\Read-ChocolateyCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Write-ChocolateyCache { $true } - Mock Write-Warning { } - Mock Write-StatusMessage { } -Verifiable - Mock Install-ChocolateyPackage { $true } - Mock Write-Error {} - Mock Write-Host {} -} - -Describe "Install-ChocolateyPackages" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Install-ChocolateyPackages -YamlData @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result | Should -Be $false - } - } - - Context "When Chocolatey packages config is missing" { - It "Should write warning and return" { - $yamlData = @{ devsetup = @{ dependencies = @{ } } } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not found" } - } - } - - Context "When Write-ChocolateyCache fails" { - It "Should write warning and return false" { - Mock Write-ChocolateyCache { $false } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } - } - } - - Context "When all packages install successfully (string format)" { - It "Should process all packages and return true" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git", "nodejs") - } - } - } - } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "installation completed" } - } - } - - Context "When all packages install successfully (object format)" { - It "Should process all packages and return true" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @( - @{ name = "git"; version = "2.42.0" }, - @{ name = "nodejs"; params = "/silent" } - ) - } - } - } - } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It - } - } - - Context "When some packages fail to install" { - It "Should continue processing and return true" { - $callCount = 0 - Mock Install-ChocolateyPackage -MockWith { - param($PackageName, $Version, $Param) - $callCount++ - if ($callCount -eq 1) { $true } else { $false } - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git", "nodejs") - } - } - } - } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 2 -Scope It - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -eq "[FAILED]" } - } - } - - Context "When package entry is empty or missing name" { - It "Should skip invalid entries and continue" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @( - $null, - @{ version = "1.0.0" }, - "git" - ) - } - } - } - } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Install-ChocolateyPackage -Exactly 1 -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "no name specified" } - } - } - - Context "When an exception occurs during installation" { - It "Should write error and return false" { - Mock Install-ChocolateyPackage { throw "Unexpected error" } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git") - } - } - } - } - $result = Install-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error installing Chocolatey packages" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 b/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 deleted file mode 100644 index 3583f32..0000000 --- a/devsetup/Private/Providers/Chocolatey/Install-ChocolateyPackages.ps1 +++ /dev/null @@ -1,162 +0,0 @@ -<# -.SYNOPSIS - Installs Chocolatey packages from YAML configuration data. - -.DESCRIPTION - This function processes YAML configuration data to install Chocolatey packages using Install-ChocolateyPackage. - It supports both simple string formats and complex object formats for packages, allowing for detailed - configuration including versions and custom installation parameters. The function validates administrator - privileges before proceeding and provides comprehensive error handling and progress reporting throughout - the installation process. - -.PARAMETER YamlData - The YAML configuration data containing Chocolatey package definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.chocolatey.packages - -.OUTPUTS - [System.Boolean] - Returns $true if installation completes successfully (even if individual packages fail). - Returns $false if configuration is invalid or critical errors occur. - -.EXAMPLE - $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml - Install-ChocolateyPackages -YamlData $yamlData - - Installs Chocolatey packages from a YAML configuration file. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @( - "git", - @{ - name = "nodejs" - version = "18.17.0" - }, - @{ - name = "googlechrome" - params = "/nogoogle" - }, - @{ - name = "vscode" - version = "1.75.0" - params = "/silent" - } - ) - } - } - } - } - Install-ChocolateyPackages -YamlData $yamlData - - Demonstrates the PSCustomObject structure and installs the configured packages. - -.NOTES - - Requires administrator privileges to install Chocolatey packages - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Throws an exception if not running as administrator - - Returns early with warning if Chocolatey packages configuration is missing - - Supports both string and object formats for package definitions: - * String format: Simple package name for latest version - * Object format: Supports name (required), version (optional), params (optional) - - Skips empty or invalid entries in the configuration without stopping execution - - Uses Install-ChocolateyPackage function for actual installation - - Provides detailed progress reporting with color-coded status messages - - Individual installation failures do not stop the overall process - - Tracks and reports installation counts for all processed packages - - Uses parameter splatting for reliable package installation - - Displays installation status ([OK]/[FAILED]) for each package - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Bulk Installation, Configuration Processing, Package Management -#> - -Function Install-ChocolateyPackages { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [PSCustomObject]$YamlData - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey package installation requires administrator privileges. Please run as administrator." - } - - # Check if chocolatey dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { - Write-Warning "Chocolatey packages not found in YAML configuration. Skipping installation." - return - } - - if (-not (Write-ChocolateyCache)) { - Write-Warning "Failed to write Chocolatey cache." - return $false - } - - $chocolateyPackages = $YamlData.devsetup.dependencies.chocolatey.packages - Write-StatusMessage "- Installing Chocolatey packages from configuration:" -ForegroundColor Cyan - - $packageCount = 0 - - foreach ($package in $chocolateyPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Normalize package to object format - if ($package -is [string]) { - $packageObj = @{ name = $package } - } else { - $packageObj = $package - } - - # Validate package name - if ([string]::IsNullOrEmpty($packageObj.name)) { - Write-Warning "Package entry #$packageCount has no name specified, skipping" - continue - } - - # Build install parameters - $installParams = @{ - PackageName = $packageObj.name - } - if ($packageObj.version) { - $installParams.Version = $packageObj.version - Write-StatusMessage "- Installing Chocolatey package: $($packageObj.name) (version: $($packageObj.version))" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline - } else { - Write-StatusMessage "- Installing Chocolatey package: $($packageObj.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewline - } - - if($packageObj.params) { - $installParams.Param = $packageObj.params - } - - #$installParams.Debug = $true - - if((Install-ChocolateyPackage @installParams)) { - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } - } - - Write-StatusMessage "- Chocolatey packages installation completed! Processed $packageCount packages." -ForegroundColor Green - write-host "" - return $true - } - catch { - Write-Error "Error installing Chocolatey packages: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 deleted file mode 100644 index d8ba920..0000000 --- a/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.Tests.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Read-ChocolateyCache.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 - Mock Get-ChocolateyCacheFile { "C:\fakepath\choco.cache" } - Mock Write-Debug { } - Mock Write-Error { } - Mock Write-ChocolateyCache { return $true } -} - -Describe "Read-ChocolateyCache" { - - Context "When cache file exists and can be read" { - It "Should return the cache data as an array of strings" { - Mock Test-Path { param($Path) $true } - Mock Get-Content { @("git|2.42.0", "nodejs|20.10.0") } - $result = Read-ChocolateyCache - $result | Should -Contain "git|2.42.0" - $result | Should -Contain "nodejs|20.10.0" - } - } - - Context "When cache file does not exist and Write-ChocolateyCache succeeds" { - It "Should create the cache file and return its contents" { - Mock Test-Path { param($Path) $false } - Mock Write-ChocolateyCache { return $true } - Mock Get-Content { @("git|2.42.0") } - $result = Read-ChocolateyCache - $result | Should -Contain "git|2.42.0" - Assert-MockCalled Write-ChocolateyCache -Exactly 1 -Scope It - } - } - - Context "When cache file does not exist and Write-ChocolateyCache fails" { - It "Should throw an exception" { - Mock Test-Path { param($Path) return $false } - Mock Write-ChocolateyCache { return $false } - { Read-ChocolateyCache } | Should -Throw "Failed to create Chocolatey cache file: C:\fakepath\choco.cache" - } - } - - Context "When reading cache file fails" { - It "Should write error and return null" { - Mock Test-Path { param($Path) $true } - Mock Get-Content { throw "Read error" } - $result = Read-ChocolateyCache - $result | Should -Be $null - Assert-MockCalled Write-Error -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to read Chocolatey cache file" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 b/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 deleted file mode 100644 index 92296ba..0000000 --- a/devsetup/Private/Providers/Chocolatey/Read-ChocolateyCache.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -<# -.SYNOPSIS - Reads cached Chocolatey package information from the DevSetup cache file. - -.DESCRIPTION - This function reads cached Chocolatey package data from the DevSetup cache system. - It automatically handles cache file creation if the file doesn't exist by calling Write-ChocolateyCache, - and provides comprehensive error handling for file operations. The function returns the cached data - as an array of strings for use by other Chocolatey-related functions. - -.OUTPUTS - [System.Array] - Returns the cached data as an array of strings if successful. - Returns $null if the cache file cannot be read or parsed. - -.EXAMPLE - Read-ChocolateyCache - - Reads the Chocolatey cache data and returns it as an array of strings. - -.EXAMPLE - $chocoCache = Read-ChocolateyCache - if ($chocoCache) { - Write-Host "Found $($chocoCache.Count) cached entries" - } else { - Write-Host "No cache data available" - } - - Demonstrates reading cache data and checking for successful retrieval. - -.EXAMPLE - $cachedPackages = Read-ChocolateyCache - $gitPackage = $cachedPackages | Where-Object { $_ -like "*git*" } - - Shows reading cache data and filtering for specific package information. - -.NOTES - - Uses Get-ChocolateyCacheFile to determine the cache file location - - Automatically creates cache file if it doesn't exist using Write-ChocolateyCache - - Throws an exception if cache file creation fails - - Uses Get-Content to read the cached data as an array of strings - - Provides comprehensive error handling for file operations - - Returns $null on any error to allow calling functions to handle gracefully - - Used by other Chocolatey functions to avoid repeated system queries for performance - - Provides debug logging when cache file is not found - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Cache Management, Data Retrieval, Performance Optimization -#> - -Function Read-ChocolateyCache { - [CmdletBinding()] - Param() - - $cacheFile = Get-ChocolateyCacheFile - - if (-Not (Test-Path $cacheFile)) { - Write-Debug "Chocolatey cache file not found: $cacheFile" - if(-not (Write-ChocolateyCache)) { - throw "Failed to create Chocolatey cache file: $cacheFile" - } - } - - try { - $cacheData = Get-Content $cacheFile - return $cacheData - } - catch { - Write-Error "Failed to read Chocolatey cache file: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 deleted file mode 100644 index c6de976..0000000 --- a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.Tests.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - Mock Write-Warning { } -} - -Describe "Test-ChocolateyInstalled" { - - Context "When Chocolatey is installed" { - It "Should return true" { - Mock Get-Command { [PSCustomObject]@{ Name = "choco" } } - $result = Test-ChocolateyInstalled - $result | Should -Be $true - Assert-MockCalled Write-Warning -Exactly 0 -Scope It - } - } - - Context "When Chocolatey is not installed" { - It "Should return false and write a warning" { - Mock Get-Command { $null } - $result = Test-ChocolateyInstalled - $result | Should -Be $false - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 b/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 deleted file mode 100644 index f599417..0000000 --- a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyInstalled.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -<# -.SYNOPSIS - Tests whether Chocolatey package manager is installed on the system. - -.DESCRIPTION - This function checks if Chocolatey is installed and available by attempting to locate the 'choco' command. - It uses Get-Command to verify that the Chocolatey executable is accessible in the system PATH. - The function provides a warning message when Chocolatey is not found and returns a boolean result - indicating the installation status. - -.OUTPUTS - [System.Boolean] - Returns $true if Chocolatey is installed and the 'choco' command is available. - Returns $false if Chocolatey is not installed or the 'choco' command cannot be found. - -.EXAMPLE - Test-ChocolateyInstalled - - Checks if Chocolatey is installed on the system. - -.EXAMPLE - if (Test-ChocolateyInstalled) { - Write-Host "Chocolatey is available" - # Proceed with Chocolatey operations - } else { - Write-Host "Chocolatey is not installed" - # Handle missing Chocolatey - } - - Demonstrates conditional logic based on Chocolatey availability. - -.EXAMPLE - $hasChocolatey = Test-ChocolateyInstalled - if (-not $hasChocolatey) { - Install-Chocolatey - } - - Shows using the function result to trigger Chocolatey installation if needed. - -.NOTES - - Uses Get-Command with -ErrorAction SilentlyContinue to suppress errors when 'choco' is not found - - Provides a descriptive warning message when Chocolatey is not installed - - Does not require administrator privileges to check installation status - - Checks for command availability rather than file system presence for more reliable detection - - Used as a prerequisite check by other Chocolatey-related functions in the DevSetup module - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Installation Verification, Prerequisites Check, System Detection -#> - -Function Test-ChocolateyInstalled { - [CmdletBinding()] - Param() - - if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { - Write-Warning "Chocolatey is not installed. Cannot check for Chocolatey packages." - return $false - } - return $true -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 deleted file mode 100644 index 37daa35..0000000 --- a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.Tests.ps1 +++ /dev/null @@ -1,75 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-ChocolateyPackageInstalled.ps1 - . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - . $PSScriptRoot\Read-ChocolateyCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { } - Mock Write-Warning { } -} - -Describe "Test-ChocolateyPackageInstalled" { - - Context "When Chocolatey is not installed" { - It "Should return NotInstalled and write a warning" { - Mock Test-ChocolateyInstalled { $false } - $result = Test-ChocolateyPackageInstalled -PackageName "git" - $result | Should -Be ([InstalledState]::NotInstalled) - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not installed" } - } - } - - Context "When package is not in cache" { - It "Should return NotInstalled" { - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { @("nodejs|20.10.0") } - $result = Test-ChocolateyPackageInstalled -PackageName "git" - $result | Should -Be ([InstalledState]::NotInstalled) - } - } - - Context "When package is in cache (any version)" { - It "Should return Installed, GlobalVersionMet, MinimumVersionMet, RequiredVersionMet" { - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { @("git|2.42.0") } - $result = Test-ChocolateyPackageInstalled -PackageName "git" - $result.HasFlag([InstalledState]::Installed) | Should -Be $true - $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true - $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $true - $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $true - } - } - - Context "When package is in cache but version does not match" { - It "Should not set MinimumVersionMet or RequiredVersionMet" { - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { @("git|2.42.0") } - $result = Test-ChocolateyPackageInstalled -PackageName "git" -Version "2.41.0" - $result.HasFlag([InstalledState]::Installed) | Should -Be $true - $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true - $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $false - $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $false - } - } - - Context "When package is in cache and version matches" { - It "Should set all flags" { - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { @("git|2.42.0") } - $result = Test-ChocolateyPackageInstalled -PackageName "git" -Version "2.42.0" - $result.HasFlag([InstalledState]::Installed) | Should -Be $true - $result.HasFlag([InstalledState]::GlobalVersionMet) | Should -Be $true - $result.HasFlag([InstalledState]::MinimumVersionMet) | Should -Be $true - $result.HasFlag([InstalledState]::RequiredVersionMet) | Should -Be $true - } - } - - Context "When Read-ChocolateyCache throws an error" { - It "Should return NotInstalled" { - Mock Test-ChocolateyInstalled { $true } - Mock Read-ChocolateyCache { throw "cache error" } - $result = Test-ChocolateyPackageInstalled -PackageName "git" - $result | Should -Be ([InstalledState]::NotInstalled) - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 b/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 deleted file mode 100644 index b61a5f0..0000000 --- a/devsetup/Private/Providers/Chocolatey/Test-ChocolateyPackageInstalled.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -<# -.SYNOPSIS - Tests whether a Chocolatey package is installed with optional version validation. - -.DESCRIPTION - This function checks if a Chocolatey package is installed on the system and optionally validates - a specific version requirement. It uses 'choco list' with exact matching to find installed packages - and examines the returned package information to determine installation status and version details. - The function supports multiple parameter sets to check different combinations of package existence - and version matching. - -.PARAMETER PackageName - The name of the Chocolatey package to check. - This parameter is mandatory for all parameter sets and must be a valid, non-empty string representing a Chocolatey package name. - -.PARAMETER Version - The specific version of the package to validate. - Mandatory parameter for PackageVersionCheck parameter set. - When specified, the function checks if the installed package matches this exact version. - -.OUTPUTS - [System.Boolean] - Returns $true if the package meets all specified criteria (exists, version matches if specified). - Returns $false if the package is not installed, doesn't meet the specified criteria, or an error occurs. - -.EXAMPLE - Test-ChocolateyPackageInstalled -PackageName "git" - - Checks if the git package is installed (any version). - -.EXAMPLE - Test-ChocolateyPackageInstalled -PackageName "nodejs" -Version "18.17.0" - - Checks if nodejs package version 18.17.0 is installed. - -.EXAMPLE - $isInstalled = Test-ChocolateyPackageInstalled -PackageName "vscode" - if ($isInstalled) { - Write-Host "Visual Studio Code is installed" - } else { - Write-Host "Visual Studio Code is not installed" - } - - Demonstrates capturing the return value to check installation status. - -.NOTES - - Requires Chocolatey to be installed on the system - - Uses Test-ChocolateyInstalled to verify Chocolatey availability before proceeding - - Returns $false immediately if Chocolatey is not installed - - Uses 'choco list' with -exact and -r flags for precise package matching and machine-readable output - - Parses package information in "packagename|version" format returned by Chocolatey - - Suppresses command output using '*>$null' to avoid console clutter - - Parameter sets determine validation criteria: - * PackageCheck: Only checks if package exists (PackageName parameter only) - * PackageVersionCheck: Checks existence and exact version match (PackageName and Version parameters) - - Includes comprehensive try-catch error handling with descriptive error messages - - Provides detailed debug logging for troubleshooting installation issues - - Uses ValidateNotNullOrEmpty attribute to ensure parameters contain valid values - - Returns early if package is not found to avoid unnecessary processing - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Package Detection, Installation Verification, Version Validation -#> - -Function Test-ChocolateyPackageInstalled { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] - [ValidateNotNullOrEmpty()] - [string]$PackageName, - - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] - [ValidateNotNullOrEmpty()] - [string]$Version - ) - - if (-not (Test-ChocolateyInstalled)) { - Write-Warning "Chocolatey is not installed. Cannot check for packages." - return [InstalledState]::NotInstalled - } - - [InstalledState]$installedState = [InstalledState]::NotInstalled - - try { - $package = Read-ChocolateyCache - if ($package) { - # choco list can return multiple lines, so find the exact match - $exactMatch = $package | Where-Object { ($_ -split '\|')[0] -eq $PackageName } - if ($exactMatch) { - $installedState += [InstalledState]::Installed - $installedState += [InstalledState]::GlobalVersionMet - - $parts = $exactMatch -split '\|' - $installedVersion = if ($parts.Length -gt 1) { $parts[1] } else { $null } - - # Now compare with the requested version - if ($PSBoundParameters.ContainsKey('Version')) { - if([Version]$installedVersion -eq [Version]$Version) { - $installedState += [InstalledState]::MinimumVersionMet - $installedState += [InstalledState]::RequiredVersionMet - } - } else { - $installedState += [InstalledState]::MinimumVersionMet - $installedState += [InstalledState]::RequiredVersionMet - } - } - } - return $installedState - } catch { - return [InstalledState]::NotInstalled - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 deleted file mode 100644 index d4df488..0000000 --- a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.Tests.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Write-Debug { } - Mock Write-Error { } - Mock Invoke-Expression { } -} - -Describe "Uninstall-ChocolateyPackage" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Uninstall-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "administrator privileges" } - } - } - - Context "When uninstallation succeeds" { - It "Should return true and write debug" { - Mock Test-RunningAsAdmin { $true } - $global:LASTEXITCODE = 0 - $result = Uninstall-ChocolateyPackage -PackageName "git" - $result | Should -Be $true - Assert-MockCalled Write-Debug -Scope It -ParameterFilter { $Message -match "uninstalled successfully" } - } - } - - Context "When uninstallation fails (non-zero exit code)" { - It "Should write error and return false" { - Mock Test-RunningAsAdmin { $true } - $global:LASTEXITCODE = 1 - $result = Uninstall-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to uninstall" } - } - } - - Context "When an exception occurs during uninstall" { - It "Should write error and return false" { - Mock Test-RunningAsAdmin { $true } - Mock Invoke-Expression { throw "Unexpected error" } - $result = Uninstall-ChocolateyPackage -PackageName "git" - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error uninstalling Chocolatey package" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 b/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 deleted file mode 100644 index a33ef47..0000000 --- a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackage.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -<# -.SYNOPSIS - Uninstalls a Chocolatey package and its dependencies from the system. - -.DESCRIPTION - This function removes a Chocolatey package from the system using the 'choco uninstall' command. - It validates administrator privileges before proceeding, handles package dependencies by uninstalling - them first, and removes all versions of the specified package including metapackages. The function - provides comprehensive error handling and uses exit codes to verify successful uninstallation. - -.PARAMETER PackageName - The name of the Chocolatey package to uninstall. - This parameter is mandatory and must be a valid, non-empty string representing an installed Chocolatey package name. - -.OUTPUTS - [System.Boolean] - Returns $true if the package and all dependencies were successfully uninstalled. - Returns $false if the uninstallation failed or insufficient privileges. - -.EXAMPLE - Uninstall-ChocolateyPackage -PackageName "git" - - Uninstalls the git package and any dependent packages from the system. - -.EXAMPLE - $result = Uninstall-ChocolateyPackage -PackageName "nodejs" - if ($result) { - Write-Host "Node.js and dependencies removed successfully" - } else { - Write-Host "Failed to remove Node.js" - } - - Demonstrates capturing the return value to check uninstallation success. - -.EXAMPLE - @("git", "nodejs", "vscode") | ForEach-Object { - Uninstall-ChocolateyPackage -PackageName $_ - } - - Shows bulk uninstallation of multiple packages with dependency handling. - -.NOTES - - Requires administrator privileges to uninstall packages - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Throws an exception if not running as administrator - - Handles package dependencies by uninstalling them first using Get-ChocolateyPackageDependencies - - Uses recursive calls to uninstall dependency packages before the main package - - Automatically handles metapackages (packages ending with .install) - - Uses 'choco uninstall' with -y flag for automatic confirmation - - Uses --all-versions flag to remove all installed versions of the package - - Uses $LASTEXITCODE to verify command execution success - - Suppresses command output using Out-Null to avoid console clutter - - Includes comprehensive try-catch error handling with descriptive error messages - - Provides detailed debug logging for troubleshooting uninstallation issues - - Checks for and removes associated .install metapackages after main package removal - -.LINK - -.COMPONENT - DevSetup,Providers.Chocolatey - -.FUNCTIONALITY - Package Management, Software Removal, System Cleanup, Dependency Management -#> - -Function Uninstall-ChocolateyPackage { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $PackageName - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey package uninstallation requires administrator privileges. Please run as administrator." - } - - Write-Debug "Uninstalling Chocolatey package: $PackageName" - - # Uninstall the package - Invoke-Expression "& choco uninstall -y $PackageName --remove-dependencies --all-versions --ignore-package-exit-codes" | Out-Null - - if ($LASTEXITCODE -eq 0) { - Write-Debug "Chocolatey package '$PackageName' uninstalled successfully." - return $true - } else { - Write-Error "Failed to uninstall Chocolatey package '$PackageName'." - return $false - } - } - catch { - Write-Error "Error uninstalling Chocolatey package: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 deleted file mode 100644 index accff48..0000000 --- a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.Tests.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-ChocolateyPackages.ps1 - . $PSScriptRoot\Uninstall-ChocolateyPackage.ps1 - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\Read-ChocolateyCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Write-ChocolateyCache { $true } - Mock Write-Warning { } - Mock Write-StatusMessage { } - Mock Uninstall-ChocolateyPackage { $true } - Mock Write-Error { } - Mock Write-Host { } -} - -Describe "Uninstall-ChocolateyPackages" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Uninstall-ChocolateyPackages -YamlData @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result | Should -Be $false - } - } - - Context "When Chocolatey packages config is missing" { - It "Should write warning and return" { - $yamlData = @{ devsetup = @{ dependencies = @{ } } } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $null - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "not found" } - } - } - - Context "When Write-ChocolateyCache fails" { - It "Should write warning and return false" { - Mock Write-ChocolateyCache { $false } - $yamlData = @{ devsetup = @{ dependencies = @{ chocolatey = @{ packages = @("git") } } } } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-Warning -Exactly 1 -Scope It -ParameterFilter { $Message -match "Failed to write Chocolatey cache" } - } - } - - Context "When all packages uninstall successfully (string format)" { - It "Should process all packages and return true" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git", "nodejs") - } - } - } - } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "uninstallation completed" } - } - } - - Context "When all packages uninstall successfully (object format)" { - It "Should process all packages and return true" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @( - @{ name = "git"; version = "2.42.0" }, - @{ name = "nodejs"; params = "/silent" } - ) - } - } - } - } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It - } - } - - Context "When some packages fail to uninstall" { - It "Should continue processing and return true" { - $callCount = 0 - Mock Uninstall-ChocolateyPackage -MockWith { - param($PackageName) - $callCount++ - if ($callCount -eq 1) { $true } else { $false } - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git", "nodejs") - } - } - } - } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 2 -Scope It - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -eq "[FAILED]" } - } - } - - Context "When package entry is empty or missing name" { - It "Should skip invalid entries and continue" { - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @( - $null, - @{ version = "1.0.0" }, - "git" - ) - } - } - } - } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $true - Assert-MockCalled Uninstall-ChocolateyPackage -Exactly 1 -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "no name specified" } - } - } - - Context "When an exception occurs during uninstallation" { - It "Should write error and return false" { - Mock Uninstall-ChocolateyPackage { throw "Unexpected error" } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git") - } - } - } - } - $result = Uninstall-ChocolateyPackages -YamlData $yamlData - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error uninstalling Chocolatey packages" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 b/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 deleted file mode 100644 index 35943c1..0000000 --- a/devsetup/Private/Providers/Chocolatey/Uninstall-ChocolateyPackages.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -<# -.SYNOPSIS - Uninstalls multiple Chocolatey packages from the system based on YAML configuration. - -.DESCRIPTION - This function removes multiple Chocolatey packages specified in a DevSetup YAML configuration. - It validates administrator privileges, parses the configuration for Chocolatey package definitions, - and systematically uninstalls each package. The function supports both simple string format and - complex object format for package specifications, handles version constraints, and provides - comprehensive progress reporting during the uninstallation process. - -.PARAMETER YamlData - The parsed YAML configuration data containing Chocolatey package definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.chocolatey.packages containing an array of package specifications. - -.OUTPUTS - [System.Boolean] - Returns $true if all packages are successfully processed (even if some individual uninstalls fail). - Returns $false if the operation encounters critical errors or cannot proceed. - -.EXAMPLE - $config = Read-ConfigurationFile -Path "environment.yaml" - Uninstall-ChocolateyPackages -YamlData $config - - Uninstalls all Chocolatey packages defined in the environment.yaml configuration. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @("git", "nodejs", "vscode") - } - } - } - } - Uninstall-ChocolateyPackages -YamlData $yamlData - - Demonstrates uninstalling packages using a programmatically created configuration. - -.EXAMPLE - if (Uninstall-ChocolateyPackages -YamlData $config) { - Write-Host "All Chocolatey packages processed successfully" - } else { - Write-Host "Chocolatey uninstallation encountered errors" - } - - Shows checking the return value to verify uninstallation completion. - -.NOTES - - Requires administrator privileges to uninstall Chocolatey packages - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Throws an exception if not running as administrator - - Updates Chocolatey cache using Write-ChocolateyCache before uninstallation - - Skips uninstallation gracefully if no Chocolatey packages are found in configuration - - Supports two package specification formats: - * Simple string: "packagename" - * Complex object: @{ name = "packagename"; version = "1.0.0" } - - Validates package names and skips entries with missing names - - Uses Uninstall-ChocolateyPackage for individual package removal - - Provides detailed progress reporting with package counts and status indicators - - Uses color-coded console output: Cyan for progress, Gray for package status, Green/Red for results - - Continues processing remaining packages even if individual uninstalls fail - - Returns $true for overall success even with individual package failures - - Includes comprehensive try-catch error handling with descriptive error messages - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Package Management, Batch Uninstallation, Configuration Processing, System Cleanup -#> - -Function Uninstall-ChocolateyPackages { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [PSCustomObject]$YamlData - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "Chocolatey package uninstallation requires administrator privileges. Please run as administrator." - } - - # Check if chocolatey dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.chocolatey -or -not $YamlData.devsetup.dependencies.chocolatey.packages) { - Write-Warning "Chocolatey packages not found in YAML configuration. Skipping uninstallation." - return - } - - if (-not (Write-ChocolateyCache)) { - Write-Warning "Failed to write Chocolatey cache." - return $false - } - - $chocolateyPackages = $YamlData.devsetup.dependencies.chocolatey.packages - Write-StatusMessage "- Uninstalling Chocolatey packages from configuration:" -ForegroundColor Cyan - - $packageCount = 0 - - foreach ($package in $chocolateyPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Normalize package to object format - if ($package -is [string]) { - $packageObj = @{ name = $package } - } else { - $packageObj = $package - } - - # Validate package name - if ([string]::IsNullOrEmpty($packageObj.name)) { - Write-Warning "Package entry #$packageCount has no name specified, skipping" - continue - } - - # Build install parameters - $installParams = @{ - PackageName = $packageObj.name - } - if ($packageObj.version) { - Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: $($packageObj.version))" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline - } else { - Write-StatusMessage "- Uninstalling Chocolatey package: $($packageObj.name) (version: latest)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewline - } - - if((Uninstall-ChocolateyPackage @installParams)) { - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } - } - - Write-StatusMessage "- Chocolatey packages uninstallation completed! Processed $packageCount packages." -ForegroundColor Green - write-host "" - return $true - } - catch { - Write-Error "Error uninstalling Chocolatey packages: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 b/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 deleted file mode 100644 index 7406561..0000000 --- a/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.Tests.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Write-ChocolateyCache.ps1 - . $PSScriptRoot\Test-ChocolateyInstalled.ps1 - . $PSScriptRoot\Get-ChocolateyCacheFile.ps1 - Mock Write-Error { } - Mock Write-Debug { } -} - -Describe "Write-ChocolateyCache" { - - Context "When Chocolatey is not installed" { - It "Should return false and write error" { - Mock Test-ChocolateyInstalled { return $false } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - $result = Write-ChocolateyCache - $result | Should -Be $false - } - } - - Context "When cache file is written successfully" { - It "Should return true and write debug" { - Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } - $script:setContentCalled = $false - Mock Set-Content -MockWith { - param($Path, $Value, $Force) - $script:setContentCalled = $true - } - $result = Write-ChocolateyCache - $result | Should -Be $true - $script:setContentCalled | Should -Be $true - } - } - - Context "When writing cache file fails" { - It "Should return false and write error" { - Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { "git|2.42.0`nnodejs|20.10.0" } - Mock Set-Content { throw "Failed to write file" } - $result = Write-ChocolateyCache - $result | Should -Be $false - } - } - - Context "When choco command throws an exception" { - It "Should return false and write error" { - Mock Test-ChocolateyInstalled { return $true } - Mock Get-ChocolateyCacheFile { return "C:\fakepath\choco.cache" } - Mock Invoke-Expression { throw "choco failed" } - $result = Write-ChocolateyCache - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 b/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 deleted file mode 100644 index 2058c2b..0000000 --- a/devsetup/Private/Providers/Chocolatey/Write-ChocolateyCache.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -<# -.SYNOPSIS - Writes current Chocolatey package information to the DevSetup cache file. - -.DESCRIPTION - This function exports the current Chocolatey package installation data and writes it to the DevSetup - cache file for performance optimization and offline reference. It validates Chocolatey installation, - executes 'choco list -r' to generate machine-readable package data, and saves the output to the - cache file. The function provides comprehensive error handling and validation throughout the process. - -.OUTPUTS - [System.Boolean] - Returns $true if the cache file is successfully written. - Returns $false if Chocolatey is not installed or the write operation fails. - -.EXAMPLE - Write-ChocolateyCache - - Exports current Chocolatey packages and writes them to the cache file. - -.EXAMPLE - if (Write-ChocolateyCache) { - Write-Host "Chocolatey cache updated successfully" - } else { - Write-Host "Failed to update Chocolatey cache" - } - - Demonstrates checking the return value to verify cache update success. - -.EXAMPLE - $cacheUpdated = Write-ChocolateyCache - if ($cacheUpdated) { - $cachedData = Read-ChocolateyCache - } - - Shows writing cache data and then reading it back for use. - -.NOTES - - Requires Chocolatey to be installed on the system - - Uses Test-ChocolateyInstalled to validate Chocolatey availability - - Returns $false immediately if Chocolatey is not available - - Executes 'choco list -r' to generate machine-readable package data with pipe-delimited format - - Uses Get-ChocolateyCacheFile to determine the cache file location - - Overwrites existing cache file using -Force flag - - Provides debug logging for successful cache operations - - Includes comprehensive try-catch error handling for command execution and file operations - - Uses Set-Content for reliable file writing with proper encoding - -.LINK - -.COMPONENT - DevSetup.Providers.Chocolatey - -.FUNCTIONALITY - Cache Management, Data Serialization, Performance Optimization -#> - -Function Write-ChocolateyCache { - [CmdletBinding()] - Param() - - $cacheFile = Get-ChocolateyCacheFile - - if(-not (Test-ChocolateyInstalled)) { - Write-Error "Chocolatey is not installed. Cannot write cache file." - return $false - } - - try { - #$chocolatelyPackages = @{} - #choco list -r | foreach-object { - # $package = $_ -split '\|' - # if($package.Count -eq 2) { - # $chocolatelyPackages[$package[0]] = @{ - # Name = $package[0] - # Version = $package[1] - # } - # } - #} - Invoke-Expression "& choco list -r" | Set-Content $cacheFile -Force - Write-Debug "Chocolatey cache written successfully to: $cacheFile" - return $true - } - catch { - Write-Error "Failed to write Chocolatey cache file: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 b/devsetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 deleted file mode 100644 index f5b7300..0000000 --- a/devsetup/Private/Providers/Core/Install-CoreDependencies.Tests.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-CoreDependencies.ps1 - . $PSScriptRoot\Install-Nuget.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Powershell\Install-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-Chocolatey.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Chocolatey\Install-ChocolateyPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Providers\Scoop\Install-Scoop.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } - Mock Write-Host { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Test-RunningAsAdmin { return $true } -} - -Describe "Install-CoreDependencies" { - - Context "When NuGet installation fails" { - It "Should return false and write error" { - Mock Install-NuGet { return $false } - Mock Test-OperatingSystem { param($os) return $false } - $result = Install-CoreDependencies - $result | Should -Be $false - } - } - - Context "When manifest is missing or has no required modules" { - It "Should return true and write warning" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return $null } - Mock Test-OperatingSystem { param($os) return $false } - $result = Install-CoreDependencies - $result | Should -Be $true - - Mock Get-DevSetupManifest { return @{ RequiredModules = $null } } - $result = Install-CoreDependencies - $result | Should -Be $true - } - } - - Context "When required module installation fails" { - It "Should return false and write error" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } - Mock Test-OperatingSystem { param($os) return $false } - $script:callCount = 0 - Mock Install-PowerShellModule -MockWith { - param($ModuleName, $Force, $AllowClobber, $Scope) - $script:callCount++ - if ($script:callCount -eq 1) { return $true } - else { return $false } - } - $result = Install-CoreDependencies - $result | Should -Be $false - } - } - - Context "When required modules include empty names" { - It "Should skip empty module names and return true" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", $null, "PSReadLine") } } - Mock Install-PowerShellModule { return $true } - Mock Test-OperatingSystem { param($os) return $false } - $result = Install-CoreDependencies - $result | Should -Be $true - } - } - - Context "When all core dependencies install successfully on Windows" { - It "Should install everything and return true" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git", "PSReadLine") } } - Mock Install-PowerShellModule { return $true } - Mock Install-Chocolatey { return $true } - Mock Install-ChocolateyPackage { return $true } - Mock Install-Scoop { return $true } - Mock Test-OperatingSystem { param($os) if ($os -eq 'Windows') { return $true } else { return $false } } - $result = Install-CoreDependencies - $result | Should -Be $true - } - } - - Context "When Chocolatey installation fails on Windows" { - It "Should return false and write error" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } - Mock Install-Chocolatey { return $false } - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - $result = Install-CoreDependencies - $result | Should -Be $false - } - } - - Context "When Git installation fails on Windows" { - It "Should return false and write error" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } - Mock Install-Chocolatey { return $true } - Mock Install-ChocolateyPackage { return $false } - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - $result = Install-CoreDependencies - $result | Should -Be $false - } - } - - Context "When Scoop installation fails on Windows" { - It "Should return false and write error" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } - Mock Install-Chocolatey { return $true } - Mock Install-ChocolateyPackage { return $true } - Mock Install-Scoop { return $false } - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - $result = Install-CoreDependencies - $result | Should -Be $false - } - } - - Context "When all core dependencies install successfully on non-Windows" { - It "Should skip Windows-only installs and return true" { - Mock Install-NuGet { return $true } - Mock Get-DevSetupManifest { return @{ RequiredModules = @("posh-git") } } - Mock Install-PowerShellModule { return $true } - Mock Test-OperatingSystem { param($os) return $false } - $result = Install-CoreDependencies - $result | Should -Be $true - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Core/Install-CoreDependencies.ps1 b/devsetup/Private/Providers/Core/Install-CoreDependencies.ps1 deleted file mode 100644 index e03f285..0000000 --- a/devsetup/Private/Providers/Core/Install-CoreDependencies.ps1 +++ /dev/null @@ -1,132 +0,0 @@ -<# -.SYNOPSIS - Installs core dependencies required for the DevSetup module to function properly. - -.DESCRIPTION - This function installs essential system dependencies and package managers required for DevSetup operations. - It sequentially installs NuGet PackageProvider, required PowerShell modules from the DevSetup manifest, - and platform-specific tools. On Windows, it also installs Chocolatey, Git, and Scoop. The function - validates each installation step and fails fast if any critical component cannot be installed. It also - refreshes the PATH environment variable to ensure newly installed tools are immediately available. - -.OUTPUTS - [System.Boolean] - Returns $true if all core dependencies are successfully installed. - Returns $false if any critical installation fails. - -.EXAMPLE - Install-CoreDependencies - - Installs all core dependencies required for DevSetup functionality. - -.EXAMPLE - if (Install-CoreDependencies) { - Write-Host "DevSetup is ready for use" - # Proceed with environment setup - } else { - Write-Host "Failed to install core dependencies" - # Handle installation failure - } - - Demonstrates conditional logic based on installation success. - -.EXAMPLE - $coreReady = Install-CoreDependencies - if ($coreReady) { - # Continue with package installations - Install-ChocolateyPackages -YamlData $config - } - - Shows using the function result to proceed with subsequent operations. - -.NOTES - - Cross-platform support with platform detection using $IsWindows, $IsLinux, $IsMacOS - - Sets up platform variables if not defined ($IsWindows = $true, others = $false by default) - - Installs dependencies in a specific order to ensure proper functionality: - 1. NuGet PackageProvider (all platforms) - 2. Required PowerShell modules from DevSetup manifest (all platforms) - 3. Windows-only components: - - Chocolatey package manager - - Git version control system via Chocolatey - - Scoop package manager - - Uses fail-fast approach - stops immediately if any critical component fails - - Installs PowerShell modules with -Force, -AllowClobber, and CurrentUser scope - - Refreshes PATH environment variable after Git installation for immediate availability - - Gets required modules list from Get-DevSetupManifest - - Provides color-coded console output for installation progress - - Skips empty module names in the manifest gracefully - - Returns $true even if no required modules are found (considered success) - - Windows-specific installations are conditionally executed based on platform detection - -.LINK - -.COMPONENT - DevSetup.Providers.Core - -.FUNCTIONALITY - System Setup, Dependency Management, Package Manager Installation -#> - -Function Install-CoreDependencies { - [CmdletBinding()] - Param() - - # Install NuGet PackageProvider - if (-not (Install-NuGet)) { - Write-Error "Failed to install NuGet PackageProvider" - return $false - } - - # Get required modules from DevSetup manifest - $manifest = Get-DevSetupManifest - if (-not $manifest -or -not $manifest.RequiredModules) { - Write-Warning "No required modules found in DevSetup manifest" - return $true - } - - # Install each required PowerShell module - foreach ($moduleName in $manifest.RequiredModules) { - if (-not $moduleName -or [string]::IsNullOrEmpty($moduleName)) { - Write-Warning "Skipping empty module name" - continue - } - - Write-StatusMessage "- Installing powershell module: $moduleName" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - if (-not (Install-PowerShellModule -ModuleName $moduleName -Force -AllowClobber -Scope 'CurrentUser')) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - Write-Error "Failed to install required PowerShell module: $moduleName" - return $false - } - Write-StatusMessage "[OK]" -ForegroundColor Green - } - - if ((Test-OperatingSystem -Windows)) { - # Install Chocolatey first - if (-not (Install-Chocolatey)) { - Write-Error "Cannot proceed without Chocolatey" - return $false - } - - # Install Git using Chocolatey - Write-StatusMessage "- Installing Git package via Chocolatey" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - if (-not (Install-ChocolateyPackage -PackageName "git" -Version 2.50.1)) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - Write-Error "Failed to install Git package" - return $false - } else { - Write-StatusMessage "[OK]" -ForegroundColor Green - } - - $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", "User") + ";" + [System.Environment]::GetEnvironmentVariable("PATH", "Machine") - - # Install Scoop PackageProvider - if (-not (Install-Scoop)) { - Write-Error "Failed to install Scoop PackageProvider" - return $false - } - } else { - Write-Warning "Skipping Windows-only installations on non-Windows platform" - } - - return $true -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Core/Install-GitRepository.ps1 b/devsetup/Private/Providers/Core/Install-GitRepository.ps1 deleted file mode 100644 index 19e68e1..0000000 --- a/devsetup/Private/Providers/Core/Install-GitRepository.ps1 +++ /dev/null @@ -1,173 +0,0 @@ -<# -.SYNOPSIS - Clones or updates a Git repository to a specified local destination. - -.DESCRIPTION - This function clones a Git repository from a remote URL to a local destination path. It includes - intelligent Git detection, handles existing repositories with update or replace options, supports - branch specification, and provides comprehensive error handling. The function automatically detects - Git installation in both PATH and common installation locations. - -.PARAMETER RepositoryUrl - The URL of the Git repository to clone. - This parameter is mandatory and must be a valid, non-empty string representing a Git repository URL. - -.PARAMETER DestinationPath - The local path where the repository should be cloned. - This parameter is mandatory and must be a valid, non-empty string representing a local directory path. - -.PARAMETER Branch - The specific branch to clone from the repository. - Optional parameter that specifies which branch to clone. If not provided, the default branch is used. - -.PARAMETER UpdateExisting - Switch parameter that controls behavior when the destination path already exists. - When specified, performs a git pull to update the existing repository instead of removing and re-cloning. - -.OUTPUTS - [System.Boolean] - Returns $true if the repository was successfully cloned or updated. - Returns $false if the operation failed or Git is not available. - -.EXAMPLE - Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" - - Clones a repository to the specified path using the default branch. - -.EXAMPLE - Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" -Branch "develop" - - Clones a specific branch of the repository. - -.EXAMPLE - Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" -UpdateExisting - - Updates an existing repository instead of removing and re-cloning. - -.EXAMPLE - $success = Install-GitRepository -RepositoryUrl "https://github.com/user/repo.git" -DestinationPath "C:\Code\repo" - if ($success) { - Write-Host "Repository ready for use" - } else { - Write-Host "Failed to clone repository" - } - - Demonstrates capturing the return value to check operation success. - -.NOTES - - Requires Git to be installed on the system - - Automatically detects Git in PATH using Get-Command - - Falls back to common Git installation path: "C:\Program Files\Git\cmd\git.exe" - - Uses $LASTEXITCODE to verify Git command execution success - - Handles existing destinations in two ways: - * UpdateExisting: Performs git pull to update existing repository - * Default: Removes existing directory and performs fresh clone - - Uses Push-Location/Pop-Location for safe directory operations during updates - - Provides color-coded console output for different operation types - - Includes comprehensive try-catch error handling - - Uses parameter splatting for reliable Git command execution - -.LINK - -.COMPONENT - DevSetup.Providers.Core - -.FUNCTIONALITY - Version Control, Repository Management, Git Operations -#> - -Function Install-GitRepository { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$RepositoryUrl, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$DestinationPath, - - [Parameter(Mandatory = $false)] - [ValidateNotNullOrEmpty()] - [string]$Branch, - - [Parameter(Mandatory = $false)] - [switch]$UpdateExisting = $false - ) - - # Check if Git is installed - $gitCommand = Get-Command git -ErrorAction SilentlyContinue - if (-not $gitCommand) { - # Check common Git installation path - $gitPath = "C:\Program Files\Git\cmd\git.exe" - if (Test-Path $gitPath) { - Write-Host "Using Git from: $gitPath" -ForegroundColor Gray - # Use the full path for git commands - $gitExecutable = $gitPath - } else { - Write-Error "Git is not installed or not found in PATH. Please install Git and try again." - return $false - } - } else { - $gitExecutable = "git" - Write-Host "Git found in PATH" -ForegroundColor Gray - } - - try { - # Check if destination already exists - if (Test-Path -Path $DestinationPath) { - if ($UpdateExisting) { - Write-Host "Updating existing repository at $DestinationPath" -ForegroundColor Yellow - - # Change to the repository directory and pull updates - Push-Location $DestinationPath - try { - & $gitExecutable pull - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to update repository at $DestinationPath" - return $false - } - Write-Host "Repository updated successfully" -ForegroundColor Green - return $true - } - finally { - Pop-Location - } - } else { - Write-Host "Removing existing directory to perform fresh clone: $DestinationPath" -ForegroundColor Yellow - Remove-Item -Path $DestinationPath -Recurse -Force - } - } - - # Build git clone command - $gitArgs = @("clone") - - # Add branch parameter only if specified - if (-not [string]::IsNullOrWhiteSpace($Branch)) { - $gitArgs += "--branch" - $gitArgs += $Branch - Write-Host "Cloning repository from $RepositoryUrl (branch: $Branch) to $DestinationPath" -ForegroundColor Cyan - } else { - Write-Host "Cloning repository from $RepositoryUrl (default branch) to $DestinationPath" -ForegroundColor Cyan - } - - # Add repository URL and destination path - $gitArgs += $RepositoryUrl - $gitArgs += $DestinationPath - - # Execute git clone command - & $gitExecutable @gitArgs - - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to clone repository from $RepositoryUrl to $DestinationPath" - return $false - } - - Write-Host "Repository cloned successfully to $DestinationPath" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error cloning repository: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Core/Install-Nuget.Tests.ps1 b/devsetup/Private/Providers/Core/Install-Nuget.Tests.ps1 deleted file mode 100644 index 597915d..0000000 --- a/devsetup/Private/Providers/Core/Install-Nuget.Tests.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-Nuget.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-OperatingSystem.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-Host { } - Mock Write-Error { } - Mock Write-StatusMessage { } -} - -Describe "Install-Nuget" { - - Context "When not running on Windows" { - It "Should skip installation and return true" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $false } else { return $true } } - $result = Install-Nuget - $result | Should -Be $true - } - } - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $false } - $result = Install-Nuget - $result | Should -Be $false - } - } - - Context "When NuGet PackageProvider is already installed" { - It "Should return true and not install again" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $true } - Mock Get-PackageProvider { - [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } - } - $result = Install-Nuget - $result | Should -Be $true - } - } - - Context "When NuGet PackageProvider is not installed and installation succeeds" { - It "Should install and return true" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $true } - $script:installCalled = $false - $script:providerCallCount = 0 - Mock Get-PackageProvider -MockWith { - param($Name) - $script:providerCallCount++ - if ($script:providerCallCount -eq 1) { return $null } - else { return [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } - } - Mock Install-PackageProvider -MockWith { - param($Name, $MinimumVersion, $Force, $Scope) - $script:installCalled = $true - } - $result = Install-Nuget - $result | Should -Be $true - $script:installCalled | Should -Be $true - } - } - - Context "When NuGet PackageProvider installation fails" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $true } - $script:providerCallCount = 0 - Mock Get-PackageProvider -MockWith { - param($Name) - $script:providerCallCount++ - return $null - } - Mock Install-PackageProvider { } - $result = Install-Nuget - $result | Should -Be $false - } - } - - Context "When NuGet CLI is available and version is detected" { - It "Should check CLI version and return true" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $true } - Mock Get-PackageProvider { [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } - Mock Get-Command { [PSCustomObject]@{ Name = "nuget" } } - Mock Invoke-Expression { "NuGet Version: 6.0.0" } - Mock Select-String { [PSCustomObject]@{ Line = "NuGet Version: 6.0.0" } } - Mock ForEach-Object { "6.0.0" } - $result = Install-Nuget - $result | Should -Be $true - } - } - - Context "When NuGet CLI is available but version detection fails" { - It "Should still return true" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { return $true } - Mock Get-PackageProvider { [PSCustomObject]@{ Name = "NuGet"; Version = "2.8.5.201" } } - Mock Get-Command { [PSCustomObject]@{ Name = "nuget" } } - Mock Invoke-Expression { throw "CLI error" } - $result = Install-Nuget - $result | Should -Be $true - } - } - - Context "When an unexpected error occurs" { - It "Should return false and write error" { - Mock Test-OperatingSystem { param($Windows) if ($Windows) { return $true } else { return $false } } - Mock Test-RunningAsAdmin { throw "Unexpected error" } - $result = Install-Nuget - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Core/Install-Nuget.ps1 b/devsetup/Private/Providers/Core/Install-Nuget.ps1 deleted file mode 100644 index e2e52af..0000000 --- a/devsetup/Private/Providers/Core/Install-Nuget.ps1 +++ /dev/null @@ -1,118 +0,0 @@ -<# -.SYNOPSIS - Installs the NuGet PackageProvider for PowerShell package management. - -.DESCRIPTION - This function installs the NuGet PackageProvider which is required for PowerShell package management - operations. It validates platform compatibility (Windows-only), administrator privileges, and existing - installations before proceeding. The function also detects and reports on the availability of the - NuGet CLI tool if present on the system. - -.OUTPUTS - [System.Boolean] - Returns $true if NuGet PackageProvider is successfully installed or already exists. - Returns $false if the installation fails or system requirements are not met. - -.EXAMPLE - Install-Nuget - - Installs the NuGet PackageProvider on the current system. - -.EXAMPLE - if (Install-Nuget) { - Write-Host "NuGet PackageProvider is ready for use" - # Proceed with PowerShell module installations - } else { - Write-Host "Failed to install NuGet PackageProvider" - # Handle installation failure - } - - Demonstrates conditional logic based on installation success. - -.EXAMPLE - $nugetReady = Install-Nuget - if ($nugetReady) { - Install-Module -Name SomeModule -Force - } - - Shows using the function result to proceed with module operations. - -.NOTES - - Requires administrator privileges on Windows systems - - Uses Test-RunningAsAdmin to validate privileges before proceeding - - Throws an exception if not running as administrator - - Windows-only functionality - automatically skips installation on non-Windows platforms - - Installs minimum version 2.8.5.201 of the NuGet PackageProvider - - Uses CurrentUser scope for installation to minimize system impact - - Verifies successful installation by re-querying the PackageProvider - - Detects and reports NuGet CLI availability if present - - Uses -Force flag to bypass confirmation prompts - - Includes comprehensive try-catch error handling with descriptive error messages - - Returns $true for successful installation or if already installed (idempotent behavior) - -.LINK - -.COMPONENT - DevSetup.Providers.Core - -.FUNCTIONALITY - Package Management Setup, NuGet Installation, Prerequisites Management -#> - -Function Install-Nuget { - [CmdletBinding()] - Param() - - try { - # Check if we're on Windows - NuGet PackageProvider is Windows-only - if (-not (Test-OperatingSystem -Windows)) { - Write-Host "NuGet PackageProvider is not available on this platform. Skipping installation." -ForegroundColor Yellow - return $true - } - - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "NuGet installation requires administrator privileges. Please run as administrator." - } - - # Check if NuGet PackageProvider is installed - $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue - Write-StatusMessage "- Installing NuGet PackageProvider" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - if ($nugetProvider) { - #Write-Host "NuGet PackageProvider is already installed (version: $($nugetProvider.Version))" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - #Write-Host "Installing NuGet PackageProvider..." -ForegroundColor Cyan - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser - - # Verify installation - $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue - if ($nugetProvider) { - #Write-Host "NuGet PackageProvider successfully installed (version: $($nugetProvider.Version))" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - throw "Failed to install NuGet PackageProvider" - } - } - - # Check if nuget.exe CLI is also available - $nugetExe = Get-Command nuget -ErrorAction SilentlyContinue - if ($nugetExe) { - try { - $nugetVersion = (Invoke-Expression "& nuget help" 2>$null | Select-String "NuGet Version" | ForEach-Object { $_.Line.Split(':')[1].Trim() }) - if ($nugetVersion) { - #Write-Host "NuGet CLI is also available: $nugetVersion" -ForegroundColor Yellow - } - } - catch { - # Silently ignore CLI version check errors - } - } - - return $true - } - catch { - Write-Error "Error checking/installing NuGet: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 b/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 deleted file mode 100644 index 439119f..0000000 --- a/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.Tests.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -BeforeAll { - function ConvertTo-Yaml { } - . $PSScriptRoot\Export-InstalledPowershellModules.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupManifest.ps1 - Mock Test-RunningAsAdmin { $true } - Mock Get-InstalledModule { @( - @{ Name = "ModuleA"; Version = [version]"1.0.0" }, - @{ Name = "ModuleB"; Version = [version]"2.0.0" } - ) } - Mock Get-DevSetupManifest { @{ RequiredModules = @("ModuleA") } } - Mock Get-Module { param($Name) @{ Name = $Name; ModuleBase = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\$Name"; Version = [version]"1.0.0" } } - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @() } } } } } - Mock ConvertTo-Yaml { param($obj) "yaml-output" } - Mock ConvertTo-Json { param($obj) "json-output" } - Mock Out-File { } - Mock Write-Host { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Write-Debug { } - Mock Write-Verbose { } -} - -Describe "Export-InstalledPowershellModules" { - - Context "When not running as administrator" { - It "Should throw and return false" { - Mock Test-RunningAsAdmin { $false } - $result = Export-InstalledPowershellModules -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "requires administrator privileges" } - } - } - - Context "When no modules are found" { - It "Should warn and return true" { - Mock Get-InstalledModule { @() } - $result = Export-InstalledPowershellModules -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No PowerShell modules found" } - } - } - - Context "When core dependency modules are present" { - It "Should skip core dependency modules" { - Mock Get-InstalledModule { @( - @{ Name = "ModuleA"; Version = [version]"1.0.0" }, - @{ Name = "ModuleB"; Version = [version]"2.0.0" } - ) } - Mock Get-DevSetupManifest { @{ RequiredModules = @("ModuleA") } } - $result = Export-InstalledPowershellModules -Config "test.yaml" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding module: ModuleB" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -notmatch "Adding module: ModuleA" } - } - } - - Context "When modules are found and added to config" { - It "Should add new modules to YAML data" { - $result = Export-InstalledPowershellModules -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding module: ModuleB" } - } - } - - Context "When module version changes" { - It "Should update the module version in the config" { - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; minimumVersion = "1.0.0"; scope = "CurrentUser" }) } } } } } - Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } - $result = Export-InstalledPowershellModules -Config "test.yaml" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating module: ModuleB" } - } - } - - Context "When module exists but has no version" { - It "Should add minimumVersion to the module" { - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; scope = "CurrentUser" }) } } } } } - Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } - $result = Export-InstalledPowershellModules -Config "test.yaml" - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Updating module version: ModuleB" } - } - } - - Context "When module is unchanged" { - It "Should skip updating the module" { - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ powershell = @{ modules = @(@{ name = "ModuleB"; minimumVersion = "2.0.0"; scope = "CurrentUser" }) } } } } } - Mock Get-InstalledModule { @(@{ Name = "ModuleB"; Version = [version]"2.0.0" }) } - $result = Export-InstalledPowershellModules -Config "test.yaml" - #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Skipping module (No Change): ModuleB" } - } - } - - Context "When DryRun is used" { - It "Should display YAML output and not write to file" { - $result = Export-InstalledPowershellModules -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Times 0 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } - } - } - - Context "When OutFile is specified" { - It "Should write YAML output to the specified file" { - $result = Export-InstalledPowershellModules -Config "test.yaml" -OutFile "out.yaml" - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } - } - } - - Context "When YAML conversion fails" { - It "Should fallback to JSON output" { - Mock ConvertTo-Yaml { throw "YAML error" } - $result = Export-InstalledPowershellModules -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Json -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } - } - } - - Context "When Out-File fails" { - It "Should write error and return false" { - Mock Out-File { throw "File error" } - $result = Export-InstalledPowershellModules -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } - } - } - - Context "When an unexpected error occurs" { - It "Should write error and return false" { - Mock Get-InstalledModule { throw "Unexpected error" } - $result = Export-InstalledPowershellModules -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Error converting PowerShell modules" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 b/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 deleted file mode 100644 index 65b1f91..0000000 --- a/devsetup/Private/Providers/Powershell/Export-InstalledPowershellModules.ps1 +++ /dev/null @@ -1,264 +0,0 @@ -<# -.SYNOPSIS - Exports installed PowerShell modules to a YAML configuration file. - -.DESCRIPTION - This function scans the system for installed PowerShell modules and exports them to a YAML - configuration file in DevSetup format. It uses Get-InstalledModule to retrieve comprehensive - module information including versions and installation scope. The function intelligently skips - core dependency modules defined in the DevSetup manifest and can update existing configuration - files by merging new modules with existing ones. - -.PARAMETER Config - The path to the YAML configuration file to read from and write to. - This parameter is mandatory and specifies both the input and output file unless OutFile is specified. - -.PARAMETER OutFile - The path to save the updated YAML configuration. - Optional parameter that allows saving to a different file than the input Config file. - -.PARAMETER DryRun - Switch parameter that prevents writing to files and displays the resulting configuration to the console. - Useful for previewing changes before committing them to a file. - -.OUTPUTS - [System.Boolean] - Returns $true if the export completes successfully or if no modules are found. - Returns $false if there are errors during the export process. - -.EXAMPLE - Export-InstalledPowershellModules -Config "environment.yaml" - - Exports installed PowerShell modules to the existing environment.yaml configuration file. - -.EXAMPLE - Export-InstalledPowershellModules -Config "current.yaml" -OutFile "backup.yaml" - - Reads from current.yaml and saves the updated configuration with installed modules to backup.yaml. - -.EXAMPLE - Export-InstalledPowershellModules -Config "dev-env.yaml" -DryRun - - Shows what the configuration would look like without actually saving to file. - -.NOTES - - Requires administrator privileges to access all installed modules - - Uses Get-InstalledModule to retrieve module information from PowerShell Gallery - - Automatically skips core dependency modules listed in the DevSetup manifest - - Handles both CurrentUser and AllUsers scope modules using path analysis - - Merges with existing YAML configuration, preserving other sections - - Supports both simple string format and complex object format for modules - - Updates existing modules when versions have changed - - Converts string entries to hashtable format when additional properties are needed - - Tracks installation scope (CurrentUser/AllUsers) for each module - - Creates the devsetup.dependencies.powershell structure if it doesn't exist - - Provides detailed console output with color-coded status messages - - Includes comprehensive error handling for module scanning and file operations - - Preserves existing module properties while updating changed values - -.LINK - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Configuration Export, Module Discovery, YAML Generation -#> - -Function Export-InstalledPowershellModules { - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Config, - [Parameter(Mandatory = $false)] - [ValidateNotNullOrEmpty()] - [string]$OutFile, - [switch]$DryRun - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "This operation requires administrator privileges. Please run as administrator." - } - - # Get installed PowerShell modules - Write-Host "- Getting list of installed PowerShell modules..." -ForegroundColor Gray - $installedModules = Get-InstalledModule -ErrorAction SilentlyContinue - - if (-not $installedModules) { - Write-Warning "No PowerShell modules found or PowerShellGet is not available." - return $true - } - - $powershellModules = @() - - # Get core dependency modules to skip from DevSetup manifest - $manifest = Get-DevSetupManifest - $coreModulesToSkip = @() - if ($manifest -and $manifest.RequiredModules) { - $coreModulesToSkip = $manifest.RequiredModules | ForEach-Object { - if ($_ -is [string]) { - $_ - } elseif ($_ -is [hashtable] -and $_.ModuleName) { - $_.ModuleName - } elseif ($_ -is [hashtable] -and $_.name) { - $_.name - } - } - } - - foreach ($module in $installedModules) { - # Skip core dependency modules - if ($module.Name -in $coreModulesToSkip) { - Write-Verbose "Skipping core dependency module: $($module.Name)" - continue - } - - # Get module scope information - $moduleInfo = Get-Module -Name $module.Name -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 - - # Check if module is in CurrentUser or AllUsers scope - $modulePath = $moduleInfo.ModuleBase - $scope = "Unknown" - - if ($modulePath -like "*\WindowsPowerShell\Modules\*" -or $modulePath -like "*\PowerShell\Modules\*") { - if ($modulePath -like "*$env:USERPROFILE*") { - $scope = "CurrentUser" - } else { - $scope = "AllUsers" - } - } - - if ($scope -eq "CurrentUser" -or $scope -eq "AllUsers") { - Write-Debug "Found module: $($module.Name) (version: $($module.Version), scope: $scope)" - $powershellModules += @{ - name = $module.Name - version = $module.Version.ToString() - scope = $scope - } - } else { - Write-Verbose "Skipping module with unknown scope: $($module.Name)" - } - } - - Write-Debug " - Found $($powershellModules.Count) PowerShell modules in CurrentUser or AllUsers scope (excluding core dependencies)" - - # Read existing YAML configuration - $YamlData = Read-ConfigurationFile -Config $Config - - # Ensure powershellModules section exists - if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } - if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } - if (-not $YamlData.devsetup.dependencies.powershell) { $YamlData.devsetup.dependencies.powershell = @{} } - if (-not $YamlData.devsetup.dependencies.powershell.modules) { $YamlData.devsetup.dependencies.powershell.modules = @() } - - # Add modules to YAML data - foreach ($module in $powershellModules) { - # Check if module already exists - $existingModule = $YamlData.devsetup.dependencies.powershell.modules | Where-Object { - ($_ -is [string] -and $_ -eq $module.name) -or - ($_ -is [hashtable] -and $_.name -eq $module.name) - } - - if (-not $existingModule) { - Write-Host " - Adding module: $($module.name) ($($module.version), $($module.scope))" -ForegroundColor Gray - $YamlData.devsetup.dependencies.powershell.modules += @{ - name = $module.name - minimumVersion = $module.version - scope = $module.scope - } - } else { - # Module exists, check if version has changed - $existingVersion = $null - if ($existingModule -is [hashtable] -and $existingModule.minimumVersion) { - $existingVersion = $existingModule.minimumVersion - } elseif ($existingModule -is [hashtable] -and $existingModule.version) { - $existingVersion = $existingModule.version - } - - if ($existingVersion -and $existingVersion -ne $module.version) { - Write-Host " - Updating module: $($module.name) ($existingVersion -> $($module.version))" -ForegroundColor Gray - - # Find index and update - $index = $YamlData.devsetup.dependencies.powershell.modules.IndexOf($existingModule) - - # Preserve existing module structure but update version - if ($existingModule -is [string]) { - # Convert string to hashtable with version - $YamlData.devsetup.dependencies.powershell.modules[$index] = @{ - name = $module.name - minimumVersion = $module.version - scope = $module.scope - } - } else { - # Update existing hashtable - $YamlData.devsetup.dependencies.powershell.modules[$index].minimumVersion = $module.version - if (-not $existingModule.scope) { - $YamlData.devsetup.dependencies.powershell.modules[$index].scope = $module.scope - } - } - } elseif (-not $existingVersion) { - Write-Host " - Updating module version: $($module.name)" -ForegroundColor Gray - - # Find index and add version - $index = $YamlData.devsetup.dependencies.powershell.modules.IndexOf($existingModule) - - if ($existingModule -is [string]) { - # Convert string to hashtable with version - $YamlData.devsetup.dependencies.powershell.modules[$index] = @{ - name = $module.name - minimumVersion = $module.version - scope = $module.scope - } - } else { - # Add version to existing hashtable - $YamlData.devsetup.dependencies.powershell.modules[$index].minimumVersion = $module.version - if (-not $existingModule.scope) { - $YamlData.devsetup.dependencies.powershell.modules[$index].scope = $module.scope - } - } - } else { - Write-Host " - Skipping module (No Change): $($module.name) ($($module.version))" -ForegroundColor Gray - } - } - } - - # Convert to YAML - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - } - catch { - Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" - $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 - } - - # Handle output based on parameters - if ($DryRun) { - Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan - Write-Host $yamlOutput -ForegroundColor White - Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow - } else { - # Determine output file - $outputFile = if ($OutFile) { $OutFile } else { $Config } - - try { - Write-Debug "`nSaving configuration to: $outputFile" - $yamlOutput | Out-File -FilePath $outputFile - Write-Debug "Configuration saved successfully!" - } - catch { - Write-Error "Failed to save configuration to $outputFile`: $_" - return $false - } - } - - Write-Host "PowerShell modules conversion completed!" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error converting PowerShell modules: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 b/devsetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 deleted file mode 100644 index 6880c74..0000000 --- a/devsetup/Private/Providers/Powershell/Install-PowershellModule.Tests.ps1 +++ /dev/null @@ -1,154 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-PowershellModule.ps1 - . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 - . $PSScriptRoot\Uninstall-PowershellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - Mock Write-Error {} - Mock Write-Warning {} -} - -Describe "Install-PowershellModule" { - - Context "When installing for AllUsers without admin privileges" { - It "Should return false" { - Mock Test-RunningAsAdmin { return $false } - $result = Install-PowershellModule -ModuleName "Az" -Scope "AllUsers" - $result | Should -Be $false - } - } - - Context "When module is already installed with correct version and scope" { - It "Should return true and not call Uninstall-PowershellModule or Install-Module" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::Pass - } - Mock Uninstall-PowershellModule { throw "Should not be called" } - Mock Install-Module { throw "Should not be called" } - $result = Install-PowershellModule -ModuleName "Az" - $result | Should -Be $true - } - } - - Context "When module is installed but needs to be uninstalled and reinstalled" { - It "Should uninstall and install the module, returning true" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::Installed - } - $script:uninstallCalled = $false - Mock Uninstall-PowershellModule -MockWith { - param( - [string]$ModuleName, - [string]$Scope - ) - $script:uninstallCalled = $true - } - $script:installCalled = $false - Mock Install-Module -MockWith { - param( - [string]$Name, - [switch]$Force, - [string]$Scope, - [switch]$AllowClobber, - [string]$RequiredVersion - ) - $script:installCalled = $true - } - $result = Install-PowershellModule -ModuleName "Az" - $result | Should -Be $true - $uninstallCalled | Should -Be $true - $installCalled | Should -Be $true - } - } - - Context "When module is not installed" { - It "Should install the module and return true" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::NotInstalled - } - $script:installCalled = $false - Mock Install-Module -MockWith { - param( - [string]$Name, - [switch]$Force, - [string]$Scope, - [switch]$AllowClobber, - [string]$RequiredVersion - ) - $script:installCalled = $true - } - $result = Install-PowershellModule -ModuleName "Az" - $result | Should -Be $true - $installCalled | Should -Be $true - } - } - - Context "When Install-Module throws an exception" { - It "Should return false" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::NotInstalled - } - Mock Install-Module { throw "Install failed" } - $result = Install-PowershellModule -ModuleName "Az" - $result | Should -Be $false - } - } - - Context "When Uninstall-PowershellModule throws an exception" { - It "Should return true" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::Installed - } - Mock Uninstall-PowershellModule { throw "Uninstall failed" } - Mock Install-Module -MockWith { - param( - [string]$Name - ) - $script:installParams = @{ - ModuleName = $Name - } - } - $result = Install-PowershellModule -ModuleName "Az" - $result | Should -Be $true - } - } - - Context "When installing with Version, Force, and AllowClobber" { - It "Should pass correct parameters and return true" { - Mock Test-RunningAsAdmin { return $true } - Mock Test-PowershellModuleInstalled { - return [InstalledState]::NotInstalled - } - Mock Install-Module -MockWith { - param( - [string]$Name, - [switch]$Force, - [string]$Scope, - [switch]$AllowClobber, - [string]$RequiredVersion - ) - $script:installParams = @{ - ModuleName = $Name - Force = $Force - Scope = $Scope - AllowClobber = $AllowClobber - RequiredVersion = $RequiredVersion - } - } - $result = Install-PowershellModule -ModuleName "Az" -Version "9.0.1" -Force -AllowClobber -Scope "CurrentUser" - $result | Should -Be $true - - $installParams | Should -Not -Be $null - $installParams.ModuleName | Should -Be "Az" - $installParams.Force | Should -Be $true - $installParams.Scope | Should -Be "CurrentUser" - $installParams.AllowClobber | Should -Be $true - $installParams.RequiredVersion | Should -Be "9.0.1" - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Install-PowershellModule.ps1 b/devsetup/Private/Providers/Powershell/Install-PowershellModule.ps1 deleted file mode 100644 index bd4b9ce..0000000 --- a/devsetup/Private/Providers/Powershell/Install-PowershellModule.ps1 +++ /dev/null @@ -1,147 +0,0 @@ -<# -.SYNOPSIS - Installs a PowerShell module with specified parameters and scope validation. - -.DESCRIPTION - Installs a PowerShell module using `Install-Module` with comprehensive validation and scope management. - Checks for existing installations and handles version/scope conflicts by intelligently uninstalling and reinstalling as needed. - Supports both `CurrentUser` and `AllUsers` scopes, with privilege validation for `AllUsers`. - -.PARAMETER ModuleName - The name of the PowerShell module to install. - Mandatory and must be a valid, non-empty string. - -.PARAMETER Version - The specific version of the module to install. - Optional; installs the latest version if not provided. - -.PARAMETER Force - Switch to force installation even if the module already exists. - Optional; passes the `-Force` flag to `Install-Module`. - -.PARAMETER AllowClobber - Switch to allow installation of modules that contain cmdlets with the same names as existing cmdlets. - Optional; passes the `-AllowClobber` flag to `Install-Module`. - -.PARAMETER Scope - The installation scope for the module. - Optional; valid values are `'CurrentUser'` or `'AllUsers'`. Defaults to `'CurrentUser'`. - `AllUsers` scope requires administrator privileges. - -.OUTPUTS - `[System.Boolean]` - Returns `$true` if the module was successfully installed or already meets requirements. - Returns `$false` if the installation failed. - -.EXAMPLE - Install-PowershellModule -ModuleName "posh-git" - # Installs the latest version of posh-git module for the current user. - -.EXAMPLE - Install-PowershellModule -ModuleName "PSReadLine" -Version "2.2.6" - # Installs a specific version of PSReadLine module for the current user. - -.EXAMPLE - Install-PowershellModule -ModuleName "PowerShellGet" -Scope "AllUsers" -Force - # Installs PowerShellGet module for all users with force flag (requires administrator privileges). - -.EXAMPLE - Install-PowershellModule -ModuleName "Az" -AllowClobber -Scope "CurrentUser" - # Installs the Az module allowing cmdlet name conflicts for the current user. - -.NOTES - **Scope Requirements:** - - Administrator privileges required for `AllUsers` scope. - - Uses `Test-PowershellModuleInstalled` to check existing installations. - - **Installation Logic:** - - Returns immediately if module with correct version and scope exists. - - Uninstalls and reinstalls if version matches but scope differs. - - Reinstalls in-place if scope matches but version differs. - - Uninstalls and reinstalls if both version and scope differ. - - **Error Handling:** - - Uses try/catch for robust error handling. - - Returns `$false` on any failure. - - **Parameter Splatting:** - - Uses parameter splatting for reliable `Install-Module` execution. - -.LINK - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Module Management, Package Installation, Scope Validation -#> - -Function Install-PowershellModule { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [String] $ModuleName, - - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [String] $Version, - - [Parameter(Mandatory=$false)] - [Switch] $Force = $false, - - [Parameter(Mandatory=$false)] - [Switch] $AllowClobber = $false, - - [Parameter(Mandatory=$false)] - [ValidateSet('CurrentUser', 'AllUsers')] - [String] $Scope = 'CurrentUser' - ) - - try { - # Check if running as administrator only when installing for all users - if ($Scope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { - throw "PowerShell module installation to AllUsers scope requires administrator privileges. Please run as administrator or use CurrentUser scope." - } - - $installParams = @{ - Name = $ModuleName - Force = $Force - Scope = $Scope - AllowClobber = $AllowClobber - SkipPublisherCheck = $true - } - - $testParams = @{ - ModuleName = $ModuleName - Scope = $Scope - } - - if($PSBoundParameters.ContainsKey('Version')) { - $testParams.Version = $Version - $installParams.RequiredVersion = $Version - } - - $testResult = Test-PowershellModuleInstalled @testParams - - if($testResult.HasFlag([InstalledState]::Pass)) { - return $true - } - - if($testResult.HasFlag([InstalledState]::Installed)) { - try { - Uninstall-PowershellModule -ModuleName $ModuleName - } catch { - # Uninstall might have failed, we keep going anyways - Write-Debug "Failed to uninstall existing module '$ModuleName': $_" - } - } - - # Install the PowerShell module - Install-Module @installParams - return $true - } - catch { - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 b/devsetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 deleted file mode 100644 index 4ec0b6d..0000000 --- a/devsetup/Private/Providers/Powershell/Install-PowershellModules.Tests.ps1 +++ /dev/null @@ -1,170 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-PowershellModules.ps1 - . $PSScriptRoot\Install-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - Mock Write-StatusMessage { } - Mock Test-RunningAsAdmin { return $true } - Mock Write-Error {} - Mock Write-Warning {} - Mock Write-Host {} -} - -Describe "Install-PowershellModules" { - - Context "When YAML configuration is missing PowerShell modules" { - It "Should return false" { - $yamlData = @{ devsetup = @{ dependencies = @{ powershell = @{ } } } } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When YAML configuration is missing dependencies" { - It "Should return false" { - $yamlData = @{ devsetup = @{ } } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When AllUsers scope is specified but not running as admin" { - It "Should return false" { - Mock Test-RunningAsAdmin { return $false } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - scope = "AllUsers" - modules = @("posh-git") - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When modules are installed successfully (string format)" { - It "Should install all modules and return true" { - $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { - param($ModuleName, $Force, $AllowClobber, $Scope, $Version) - $script:installCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git", "PSReadLine") - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $installCalls | Should -Contain "posh-git" - $installCalls | Should -Contain "PSReadLine" - } - } - - Context "When modules are installed successfully (object format)" { - It "Should install all modules and return true" { - $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { - param($ModuleName, $Force, $AllowClobber, $Scope, $Version) - $script:installCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @( - @{ name = "posh-git"; minimumVersion = "1.0.0"; scope = "CurrentUser"; force = $true; allowClobber = $true }, - @{ name = "PSReadLine"; minimumVersion = "2.2.6"; scope = "AllUsers"; force = $false; allowClobber = $false } - ) - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $installCalls | Should -Contain "posh-git" - $installCalls | Should -Contain "PSReadLine" - } - } - - Context "When some modules fail to install" { - It "Should continue and return true" { - $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { - param($ModuleName, $Force, $AllowClobber, $Scope, $Version) - $script:installCalls += $ModuleName - if ($ModuleName -eq "PSReadLine") { return $false } - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git", "PSReadLine", "PowerShellGet") - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $installCalls | Should -Contain "posh-git" - $installCalls | Should -Contain "PSReadLine" - $installCalls | Should -Contain "PowerShellGet" - } - } - - Context "When module entry is empty or missing name" { - It "Should skip invalid entries and return true" { - $script:installCalls = @() - Mock Install-PowerShellModule -MockWith { - param($ModuleName, $Force, $AllowClobber, $Scope, $Version) - $script:installCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @( - $null, - @{ minimumVersion = "1.0.0" }, - "posh-git" - ) - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $installCalls | Should -Contain "posh-git" - $installCalls.Count | Should -Be 1 - } - } - - Context "When an exception occurs during installation" { - It "Should catch and return false" { - Mock Install-PowerShellModule { throw "Unexpected error" } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git") - } - } - } - } - $result = Install-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Install-PowershellModules.ps1 b/devsetup/Private/Providers/Powershell/Install-PowershellModules.ps1 deleted file mode 100644 index 2b18c4e..0000000 --- a/devsetup/Private/Providers/Powershell/Install-PowershellModules.ps1 +++ /dev/null @@ -1,162 +0,0 @@ -<# -.SYNOPSIS - Installs PowerShell modules from YAML configuration data. - -.DESCRIPTION - This function processes YAML configuration data to install PowerShell modules using Install-PowerShellModule. - It supports both simple string formats and complex object formats for modules, allowing for detailed - configuration including versions, installation scope, and module-specific parameters. The function validates - administrator privileges when AllUsers scope is specified and provides comprehensive error handling and - progress reporting throughout the installation process. - -.PARAMETER YamlData - The YAML configuration data containing PowerShell module definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.powershell.modules and optionally devsetup.dependencies.powershell.scope - -.OUTPUTS - [System.Boolean] - Returns $true if installation completes successfully (even if individual modules fail). - Returns $false if configuration is invalid or critical errors occur. - -.EXAMPLE - $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml - Install-PowershellModules -YamlData $yamlData - - Installs PowerShell modules from a YAML configuration file. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - scope = "CurrentUser" - modules = @( - "posh-git", - @{ - name = "PSReadLine" - minimumVersion = "2.2.6" - }, - @{ - name = "PowerShellGet" - scope = "AllUsers" - force = $true - allowClobber = $false - } - ) - } - } - } - } - Install-PowershellModules -YamlData $yamlData - - Demonstrates the PSCustomObject structure and installs the configured modules. - -.NOTES - - Requires the YAML configuration to have devsetup.dependencies.powershell.modules structure - - Returns $false immediately if PowerShell modules configuration is missing or invalid - - Supports global scope setting with module-specific overrides - - Default scope is 'CurrentUser' if not specified - - Validates administrator privileges when AllUsers scope is requested - - Supports both string and object formats for module definitions - - Module object format supports: name (required), minimumVersion (optional), scope (optional), force (optional), allowClobber (optional) - - Skips empty or invalid entries in the configuration without stopping execution - - Uses Install-PowerShellModule function for actual installation - - Provides detailed progress reporting with color-coded status messages - - Individual installation failures do not stop the overall process - - Tracks and reports installation counts for all processed modules - - Uses parameter splatting for reliable module installation - -.LINK - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Bulk Installation, Configuration Processing, Module Management -#> - -Function Install-PowershellModules { - Param( - [Parameter(Mandatory=$true, Position=0)] - [ValidateNotNullOrEmpty()] - [PSCustomObject]$YamlData - ) - - try { - Write-StatusMessage "- Installing PowerShell modules from configuration:" -ForegroundColor Cyan - # Check if PowerShell modules dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.powershell -or -not $YamlData.devsetup.dependencies.powershell.modules) { - Write-Debug "PowerShell modules not found in YAML configuration. Skipping installation." - Write-StatusMessage "- PowerShell modules installation completed! Processed 0 modules." -ForegroundColor Green - Write-Host "" - return $false - } - - $modules = $YamlData.devsetup.dependencies.powershell.modules - - # Get global scope setting from YAML, default to CurrentUser - $globalScope = 'AllUsers' - if ($YamlData.devsetup.dependencies.powershell.scope) { - $globalScope = $YamlData.devsetup.dependencies.powershell.scope - } - - # Check if running as administrator when global scope is AllUsers - if ($globalScope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { - throw "PowerShell module installation to AllUsers scope requires administrator privileges. Please run as administrator or set powershellModuleScope to CurrentUser." - } - - - $moduleCount = 0 - - foreach ($module in $modules) { - if (-not $module) { continue } - - $moduleCount++ - - # Normalize module to object format - if ($module -is [string]) { - $moduleObj = @{ name = $module } - } else { - $moduleObj = $module - } - - # Validate module name - if ([string]::IsNullOrEmpty($moduleObj.name)) { - Write-Warning "Module entry #$moduleCount has no name specified, skipping" - continue - } - - # Determine scope for this module (module-specific overrides global) - $moduleScope = if ($moduleObj.scope) { $moduleObj.scope } else { $globalScope } - - # Set defaults and build parameters - $installParams = @{ - ModuleName = $moduleObj.name - Force = if ($moduleObj.force -is [bool]) { $moduleObj.force } else { $true } - AllowClobber = if ($moduleObj.allowClobber -is [bool]) { $moduleObj.allowClobber } else { $true } - Scope = $moduleScope - } - - if ($moduleObj.minimumVersion) { - $installParams.Version = $moduleObj.minimumVersion - Write-StatusMessage "- Installing PowerShell module: $($moduleObj.name) (version: $($moduleObj.minimumVersion), scope: $moduleScope)" -ForegroundColor Gray -Width 112 -NoNewLine -Indent 2 - } else { - Write-StatusMessage "- Installing PowerShell module: $($moduleObj.name) (latest version) to $moduleScope scope" -ForegroundColor Gray -Width 112 -NoNewLine -Indent 2 - } - - if ((Install-PowerShellModule @installParams)) { - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } - } - Write-StatusMessage "- PowerShell modules installation completed! Processed $moduleCount modules." -ForegroundColor Green - Write-Host "" - return $true - } - catch { - Write-Error "Error installing PowerShell modules: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 b/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 deleted file mode 100644 index 4c18e17..0000000 --- a/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.Tests.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 -} - -Describe "Test-PowershellModuleInstalled" { - - Context "When module is not installed" { - It "Should return NotInstalled" { - Mock Get-Module { return $null } - $result = Test-PowershellModuleInstalled -ModuleName "notfound" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } - - Context "When module is installed (any version, any scope)" { - It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { - Mock Get-Module { - [PSCustomObject]@{ - Name = "posh-git" - Version = "1.0.0" - Path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\posh-git" - } - } - $result = Test-PowershellModuleInstalled -ModuleName "posh-git" - $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When module is installed with matching version" { - It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { - Mock Get-Module { - [PSCustomObject]@{ - Name = "PSReadLine" - Version = "2.2.6" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" - } - } - $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" - $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When module is installed but version does not match" { - It "Should return Installed + GlobalVersionMet" { - Mock Get-Module { - [PSCustomObject]@{ - Name = "PSReadLine" - Version = "2.2.5" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\PSReadLine" - } - } - $result = Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" - $expected = [InstalledState]::Installed + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When module is installed in AllUsers scope" { - It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { - Mock Get-Module { - [PSCustomObject]@{ - Name = "PowerShellGet" - Version = "2.2.5" - Path = "$env:ProgramFiles\PowerShell\Modules\PowerShellGet" - } - } - $result = Test-PowershellModuleInstalled -ModuleName "PowerShellGet" -Scope "AllUsers" - $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When module is installed in CurrentUser scope" { - It "Should return Installed + MinimumVersionMet + RequiredVersionMet + GlobalVersionMet" { - Mock Get-Module { - [PSCustomObject]@{ - Name = "Az" - Version = "9.0.1" - Path = "$env:USERPROFILE\Documents\PowerShell\Modules\Az" - } - } - $result = Test-PowershellModuleInstalled -ModuleName "Az" -Scope "CurrentUser" - $expected = [InstalledState]::Installed + [InstalledState]::MinimumVersionMet + [InstalledState]::RequiredVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When Get-Module throws an exception" { - It "Should return NotInstalled" { - Mock Get-Module { throw "Unexpected error" } - $result = Test-PowershellModuleInstalled -ModuleName "Az" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 b/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 deleted file mode 100644 index cc5e943..0000000 --- a/devsetup/Private/Providers/Powershell/Test-PowershellModuleInstalled.ps1 +++ /dev/null @@ -1,157 +0,0 @@ -<# -.SYNOPSIS - Tests whether a PowerShell module is installed, with optional version and scope validation. - -.DESCRIPTION - Checks if a PowerShell module is installed on the system and optionally validates - specific version requirements and installation scope. Uses `Get-Module -ListAvailable` - to find installed modules and examines their installation paths to determine scope - (`CurrentUser` vs `AllUsers`). Supports multiple parameter sets to check different - combinations of module existence, version matching, and scope validation. - -.PARAMETER ModuleName - The name of the PowerShell module to check. - Mandatory for all parameter sets. - -.PARAMETER Version - The specific version of the module to validate. - Optional; only used in version-related parameter sets. - -.PARAMETER Scope - The installation scope to validate (`CurrentUser` or `AllUsers`). - Optional; only used in scope-related parameter sets. - -.OUTPUTS - `[InstalledState]` - Returns an InstalledState enum value indicating installation status and version/scope match. - Returns `[InstalledState]::NotInstalled` if not found or criteria are not met. - -.EXAMPLE - Test-PowershellModuleInstalled -ModuleName "posh-git" - # Checks if the posh-git module is installed (any version, any scope). - -.EXAMPLE - Test-PowershellModuleInstalled -ModuleName "PSReadLine" -Version "2.2.6" - # Checks if PSReadLine module version 2.2.6 is installed. - -.EXAMPLE - Test-PowershellModuleInstalled -ModuleName "PowerShellGet" -Scope "AllUsers" - # Checks if PowerShellGet module is installed in AllUsers scope. - -.EXAMPLE - Test-PowershellModuleInstalled -ModuleName "Az" -Version "9.0.1" -Scope "CurrentUser" - # Checks if Az module version 9.0.1 is installed in CurrentUser scope. - -.NOTES - **Module Paths:** - - CurrentUser (PS5.1): `$HOME\Documents\WindowsPowerShell\Modules` - - CurrentUser (PS7+): `$HOME\Documents\PowerShell\Modules` - - AllUsers (PS5.1): `$Env:ProgramFiles\WindowsPowerShell\Modules` - - AllUsers (PS7+): `$Env:ProgramFiles\PowerShell\Modules` - - **Parameter Sets:** - - `ModuleCheck`: Checks if module exists. - - `ModuleVersionCheck`: Checks existence and exact version match. - - `ModuleScopeCheck`: Checks existence and scope match. - - `ModuleVersionAndScopeCheck`: Checks existence, version, and scope match. - - **Behavior:** - - Returns the highest version when multiple versions are installed. - - Uses `[InstalledState]` enum for detailed status. - - Includes error handling and debug logging. - -.LINK - Get-Module - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Module Detection, Installation Verification, Scope Validation -#> - -Function Test-PowershellModuleInstalled { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, ParameterSetName='ModuleCheck')] - [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionCheck')] - [Parameter(Mandatory=$true, ParameterSetName='ModuleScopeCheck')] - [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] - [ValidateNotNullOrEmpty()] - [string]$ModuleName, - - [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionCheck')] - [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] - [string]$Version, - - [Parameter(Mandatory=$true, ParameterSetName='ModuleScopeCheck')] - [Parameter(Mandatory=$true, ParameterSetName='ModuleVersionAndScopeCheck')] - [ValidateSet("CurrentUser", "AllUsers")] - [string]$Scope - ) - - # CurrentUser ps5.1 - # $HOME\Documents\WindowsPowerShell\Modules - # CurrentUser ps7 - # $HOME\Documents\PowerShell\Modules - - # AllUsers ps5.1 - # $Env:ProgramFiles\WindowsPowerShell\Modules - # AllUsers ps7 - # $Env:ProgramFiles\PowerShell\Modules - $InstallPaths = @( - @{ - Path = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules"; - Scope = "CurrentUser" - }, - @{ - Path = "$env:USERPROFILE\Documents\PowerShell\Modules"; - Scope = "CurrentUser" - } - @{ - Path = "$env:ProgramFiles\PowerShell\Modules"; - Scope = "AllUsers" - }, - @{ - Path = "$env:ProgramFiles\WindowsPowerShell\Modules"; - Scope = "AllUsers" - } - ) - - [InstalledState]$installedState = [InstalledState]::NotInstalled - - try { - $module = Get-Module -Name $ModuleName -ListAvailable -ErrorAction Stop | - Sort-Object Version -Descending | - Select-Object -First 1 - - if ($module) { - $installedState = [InstalledState]::Installed - - if($PSBoundParameters.ContainsKey('Scope')) { - $InstallPaths | ForEach-Object { - if ($module.Path -like "$($_.Path)\*") { - if ($_.Scope -eq $Scope) { - $installedState += [InstalledState]::GlobalVersionMet - } - } - } - } else { - $installedState += [InstalledState]::GlobalVersionMet - } - - if ($PSBoundParameters.ContainsKey('Version')) { - if([Version]$module.Version -eq [Version]$Version) { - $installedState += [InstalledState]::MinimumVersionMet - $installedState += [InstalledState]::RequiredVersionMet - } - } else { - $installedState += [InstalledState]::MinimumVersionMet - $installedState += [InstalledState]::RequiredVersionMet - } - } - return $installedState - } catch { - return [InstalledState]::NotInstalled - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 b/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 deleted file mode 100644 index 1ebb2af..0000000 --- a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.Tests.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-PowershellModule.ps1 - . $PSScriptRoot\Test-PowershellModuleInstalled.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 - Mock Test-RunningAsAdmin { return $true } - Mock Write-Warning { } - Mock Write-Error { } - Mock Write-Debug { } -} - -Describe "Uninstall-PowershellModule" { - - Context "When module is not installed" { - It "Should return true and warn" { - Mock Test-PowershellModuleInstalled { return [InstalledState]::NotInstalled } - $result = Uninstall-PowershellModule -ModuleName "notfound" - $result | Should -Be $true - } - } - - Context "When module is installed for AllUsers but not running as admin" { - It "Should return false and warn" { - $callCount = 0 - Mock Test-PowershellModuleInstalled -MockWith { - param($ModuleName, $Scope) - $callCount++ - if ($callCount -eq 1) { return [InstalledState]::Installed } - if ($callCount -eq 2) { return [InstalledState]::Pass } - return [InstalledState]::NotInstalled - } - Mock Test-RunningAsAdmin { return $false } - $result = Uninstall-PowershellModule -ModuleName "Az" - $result | Should -Be $false - } - } - - Context "When module is installed and uninstall succeeds" { - It "Should remove and uninstall the module, returning true" { - $script:callCount = 0 - Mock Test-PowershellModuleInstalled -MockWith { - param($ModuleName, $Scope) - $script:callCount++ - if ($script:callCount -eq 1) { return [InstalledState]::Installed } - if ($script:callCount -eq 2) { return [InstalledState]::Installed } - if ($script:callCount -eq 3) { return [InstalledState]::NotInstalled } - return [InstalledState]::NotInstalled - } - $script:removeCalled = $false - $script:uninstallCalled = $false - Mock Remove-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - $script:removeCalled = $true - } - Mock Uninstall-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - $script:uninstallCalled = $true - } - $result = Uninstall-PowershellModule -ModuleName "posh-git" - $removeCalled | Should -Be $true - $uninstallCalled | Should -Be $true - $result | Should -Be $true - } - } - - Context "When uninstall fails with exception" { - It "Should return false and write error" { - $script:callCount = 0 - Mock Test-PowershellModuleInstalled -MockWith { - param($ModuleName, $Scope) - $script:callCount++ - if ($script:callCount -eq 1) { return [InstalledState]::Installed } - if ($script:callCount -eq 2) { return [InstalledState]::Installed } - return [InstalledState]::NotInstalled - } - Mock Remove-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - } - Mock Uninstall-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - throw "Uninstall failed" - } - $result = Uninstall-PowershellModule -ModuleName "PSReadLine" - $result | Should -Be $false - } - } - - Context "When module is installed but still present after uninstall" { - It "Should return false" { - $script:callCount = 0 - Mock Test-PowershellModuleInstalled -MockWith { - param($ModuleName, $Scope) - $script:callCount++ - if ($script:callCount -eq 1) { return [InstalledState]::Installed } - if ($script:callCount -eq 2) { return [InstalledState]::Installed } - if ($script:callCount -eq 3) { return [InstalledState]::Installed } - return [InstalledState]::NotInstalled - } - Mock Remove-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - } - Mock Uninstall-Module -MockWith { - param([string]$Name, [switch]$Force, [string]$ErrorAction) - } - $result = Uninstall-PowershellModule -ModuleName "PowerShellGet" - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 b/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 deleted file mode 100644 index 44e563d..0000000 --- a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModule.ps1 +++ /dev/null @@ -1,96 +0,0 @@ -<# -.SYNOPSIS - Uninstalls a PowerShell module from the system. - -.DESCRIPTION - This function removes a PowerShell module from the system by first removing it from the current session - using Remove-Module, then uninstalling it completely using Uninstall-Module. The function includes - validation to check if the module is installed before attempting removal, validates administrator - privileges for AllUsers scope modules, and provides comprehensive error handling throughout the - uninstallation process. - -.PARAMETER ModuleName - The name of the PowerShell module to uninstall. - This parameter is mandatory and must be a valid string representing an installed PowerShell module name. - -.OUTPUTS - [System.Boolean] - Returns $true if the module was successfully uninstalled or was not installed. - Returns $false if the uninstallation failed or insufficient privileges for AllUsers modules. - -.EXAMPLE - Uninstall-PowershellModule -ModuleName "posh-git" - - Uninstalls the posh-git module from the system. - -.EXAMPLE - $result = Uninstall-PowershellModule -ModuleName "PSReadLine" - if ($result) { - Write-Host "PSReadLine module removed successfully" - } else { - Write-Host "Failed to remove PSReadLine module" - } - - Demonstrates capturing the return value to check uninstallation success. - -.EXAMPLE - @("Module1", "Module2", "Module3") | ForEach-Object { - Uninstall-PowershellModule -ModuleName $_ - } - - Shows bulk uninstallation of multiple modules. - -.NOTES - - Uses Test-PowershellModuleInstalled to verify module existence before attempting removal - - Returns $true if module is not installed (considered successful since goal is achieved) - - Validates administrator privileges for AllUsers scope modules using Test-RunningAsAdmin - - Returns $false immediately if AllUsers module requires elevation but session is not elevated - - Performs two-step removal process: - 1. Remove-Module: Removes from current PowerShell session (with -Force flag) - 2. Uninstall-Module: Completely removes from system (with -Force flag) - - Uses -ErrorAction Stop for proper exception handling - - Includes comprehensive try-catch error handling with descriptive error messages - - Provides detailed debug logging for troubleshooting uninstallation issues - - Uses Write-Warning for non-critical issues (module not found, privilege issues) - - Uses Write-Error for actual uninstallation failures - -.LINK - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Module Management, Package Removal, System Cleanup -#> - -Function Uninstall-PowershellModule { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [String] $ModuleName - ) - - $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName - if ($installedState -eq [InstalledState]::NotInstalled) { - Write-Warning "PowerShell module '$ModuleName' is not installed. No action taken." - return $true - } - - $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName -Scope 'AllUsers' - if ($installedState.HasFlag([InstalledState]::Pass) -and (-not (Test-RunningAsAdmin))) { - Write-Warning "PowerShell module '$ModuleName' is installed for AllUsers but current session is not elevated. Cannot uninstall." - return $false - } - - try { - Write-Debug "Uninstalling PowerShell module '$ModuleName'..." - Remove-Module -Name $ModuleName -Force -ErrorAction SilentlyContinue - Uninstall-Module -Name $ModuleName -Force -ErrorAction Stop - Write-Debug "PowerShell module '$ModuleName' uninstalled successfully." - $installedState = Test-PowershellModuleInstalled -ModuleName $ModuleName - return ($installedState -eq [InstalledState]::NotInstalled) - } catch { - Write-Error "Failed to uninstall PowerShell module '$ModuleName': $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 b/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 deleted file mode 100644 index 17471fb..0000000 --- a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.Tests.ps1 +++ /dev/null @@ -1,170 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-PowershellModules.ps1 - . $PSScriptRoot\Uninstall-PowerShellModule.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Test-RunningAsAdmin { return $true } - Mock Write-Host { } -} - -Describe "Uninstall-PowershellModules" { - - Context "When YAML configuration is missing PowerShell modules" { - It "Should return false and warn" { - $yamlData = @{ devsetup = @{ dependencies = @{ powershell = @{ } } } } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When YAML configuration is missing dependencies" { - It "Should return false and warn" { - $yamlData = @{ devsetup = @{ } } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When AllUsers scope is specified but not running as admin" { - It "Should return false" { - Mock Test-RunningAsAdmin { return $false } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - scope = "AllUsers" - modules = @("posh-git") - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When modules are uninstalled successfully (string format)" { - It "Should uninstall all modules and return true" { - $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { - param($ModuleName) - $script:uninstallCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git", "PSReadLine") - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $uninstallCalls | Should -Contain "posh-git" - $uninstallCalls | Should -Contain "PSReadLine" - } - } - - Context "When modules are uninstalled successfully (object format)" { - It "Should uninstall all modules and return true" { - $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { - param($ModuleName) - $script:uninstallCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @( - @{ name = "posh-git"; minimumVersion = "1.0.0"; scope = "CurrentUser" }, - @{ name = "PSReadLine"; minimumVersion = "2.2.6"; scope = "AllUsers" } - ) - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $uninstallCalls | Should -Contain "posh-git" - $uninstallCalls | Should -Contain "PSReadLine" - } - } - - Context "When some modules fail to uninstall" { - It "Should continue and return true" { - $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { - param($ModuleName) - $script:uninstallCalls += $ModuleName - if ($ModuleName -eq "PSReadLine") { return $false } - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git", "PSReadLine", "PowerShellGet") - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $uninstallCalls | Should -Contain "posh-git" - $uninstallCalls | Should -Contain "PSReadLine" - $uninstallCalls | Should -Contain "PowerShellGet" - } - } - - Context "When module entry is empty or missing name" { - It "Should skip invalid entries and return true" { - $script:uninstallCalls = @() - Mock Uninstall-PowerShellModule -MockWith { - param($ModuleName) - $script:uninstallCalls += $ModuleName - return $true - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @( - $null, - @{ minimumVersion = "1.0.0" }, - "posh-git" - ) - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $true - $uninstallCalls | Should -Contain "posh-git" - $uninstallCalls.Count | Should -Be 1 - } - } - - Context "When an exception occurs during uninstallation" { - It "Should catch and return false" { - Mock Uninstall-PowerShellModule { throw "Unexpected error" } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - modules = @("posh-git") - } - } - } - } - $result = Uninstall-PowershellModules -YamlData $yamlData - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 b/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 deleted file mode 100644 index 50b4563..0000000 --- a/devsetup/Private/Providers/Powershell/Uninstall-PowershellModules.ps1 +++ /dev/null @@ -1,157 +0,0 @@ -<# -.SYNOPSIS - Uninstalls multiple PowerShell modules from the system based on YAML configuration. - -.DESCRIPTION - This function removes multiple PowerShell modules specified in a DevSetup YAML configuration. - It validates administrator privileges when required, parses the configuration for PowerShell - module definitions, and systematically uninstalls each module. The function supports both - simple string format and complex object format for module specifications, handles scope - settings, and provides comprehensive progress reporting during the uninstallation process. - -.PARAMETER YamlData - The parsed YAML configuration data containing PowerShell module definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.powershell.modules containing an array of module specifications. - -.OUTPUTS - [System.Boolean] - Returns $true if all modules are successfully processed (even if some individual uninstalls fail). - Returns $false if the operation encounters critical errors or cannot proceed. - -.EXAMPLE - $config = Read-ConfigurationFile -Path "environment.yaml" - Uninstall-PowershellModules -YamlData $config - - Uninstalls all PowerShell modules defined in the environment.yaml configuration. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - powershell = @{ - scope = "CurrentUser" - modules = @("PSReadLine", "Pester", "PowerShellGet") - } - } - } - } - Uninstall-PowershellModules -YamlData $yamlData - - Demonstrates uninstalling modules using a programmatically created configuration. - -.EXAMPLE - if (Uninstall-PowershellModules -YamlData $config) { - Write-Host "All PowerShell modules processed successfully" - } else { - Write-Host "PowerShell module uninstallation encountered errors" - } - - Shows checking the return value to verify uninstallation completion. - -.NOTES - - Requires administrator privileges when uninstalling from AllUsers scope - - Uses Test-RunningAsAdmin to validate privileges when scope is AllUsers - - Throws an exception if AllUsers scope is specified without administrator privileges - - Skips uninstallation gracefully if no PowerShell modules are found in configuration - - Supports two module specification formats: - * Simple string: "ModuleName" - * Complex object: @{ name = "ModuleName"; minimumVersion = "1.0.0"; scope = "CurrentUser" } - - Global scope setting defaults to CurrentUser if not specified in configuration - - Module-specific scope settings override the global scope setting - - Validates module names and skips entries with missing names - - Uses Uninstall-PowerShellModule for individual module removal - - Provides detailed progress reporting with module counts and version information - - Uses color-coded console output: Cyan for progress, Gray for module status, Green/Red for results - - Continues processing remaining modules even if individual uninstalls fail - - Returns $true for overall success even with individual module failures - - Includes comprehensive try-catch error handling with descriptive error messages - -.LINK - -.COMPONENT - DevSetup.Providers.PowerShell - -.FUNCTIONALITY - Package Management, Batch Uninstallation, Configuration Processing, Module Management -#> - -Function Uninstall-PowershellModules { - Param( - [Parameter(Mandatory=$true, Position=0)] - [ValidateNotNullOrEmpty()] - [PSCustomObject]$YamlData - ) - - try { - # Check if PowerShell modules dependencies exist - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.powershell -or -not $YamlData.devsetup.dependencies.powershell.modules) { - Write-Warning "PowerShell modules not found in YAML configuration. Skipping uninstallation." - return $false - } - - $modules = $YamlData.devsetup.dependencies.powershell.modules - - # Get global scope setting from YAML, default to CurrentUser - $globalScope = if ($YamlData.devsetup.dependencies.powershell.scope) { - $YamlData.devsetup.dependencies.powershell.scope - } else { - 'CurrentUser' - } - - # Check if running as administrator when global scope is AllUsers - if ($globalScope -eq 'AllUsers' -and (-not (Test-RunningAsAdmin))) { - throw "PowerShell module uninstallation to AllUsers scope requires administrator privileges. Please run as administrator or set powershellModuleScope to CurrentUser." - } - - Write-StatusMessage "- Uninstalling PowerShell modules from configuration:" -ForegroundColor Cyan - - $moduleCount = 0 - - foreach ($module in $modules) { - if (-not $module) { continue } - - $moduleCount++ - - # Normalize module to object format - if ($module -is [string]) { - $moduleObj = @{ name = $module } - } else { - $moduleObj = $module - } - - # Validate module name - if ([string]::IsNullOrEmpty($moduleObj.name)) { - Write-Warning "Module entry #$moduleCount has no name specified, skipping" - continue - } - - # Determine scope for this module (module-specific overrides global) - $moduleScope = if ($moduleObj.scope) { $moduleObj.scope } else { $globalScope } - - # Set defaults and build parameters - $installParams = @{ - ModuleName = $moduleObj.name - } - - if ($moduleObj.minimumVersion) { - Write-StatusMessage "- Uninstalling PowerShell module: $($moduleObj.name) (version: $($moduleObj.minimumVersion), scope: $moduleScope)" -ForegroundColor Gray -Width 100 -NoNewLine -Indent 2 - } else { - Write-StatusMessage "- Uninstalling PowerShell module: $($moduleObj.name) (latest version) to $moduleScope scope" -ForegroundColor Gray -Width 100 -NoNewLine -Indent 2 - } - - if ((Uninstall-PowerShellModule @installParams)) { - Write-StatusMessage "[OK]" -ForegroundColor Green - } else { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } - } - Write-StatusMessage "- PowerShell modules uninstallation completed! Processed $moduleCount modules." -ForegroundColor Green - Write-Host "" - return $true - } - catch { - Write-Error "Error uninstalling PowerShell modules: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 b/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 deleted file mode 100644 index c45ee0b..0000000 --- a/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.Tests.ps1 +++ /dev/null @@ -1,107 +0,0 @@ -BeforeAll { - function ConvertTo-Yaml { } - . $PSScriptRoot\Export-InstalledScoopPackages.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Read-ConfigurationFile.ps1 - Mock Test-ScoopInstalled { $true } - Mock Find-Scoop { "scoop" } - Mock Invoke-Expression { '{"buckets":[{"Name":"extras","Source":"https://github.com/ScoopInstaller/Extras"}],"apps":[{"Name":"git","Version":"2.40.0","Source":"extras","Info":"Global install"}]}' } - Mock Read-ConfigurationFile { @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @(); buckets = @() } } } } } - Mock ConvertTo-Yaml { param($obj) "yaml-output" } - Mock ConvertTo-Json { param($obj) "json-output" } - Mock Out-File { $true } - Mock Write-Host { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Write-Debug { } - Mock Write-Verbose { } -} - -Describe "Export-InstalledScoopPackages" { - - Context "When Scoop is not installed" { - It "Should warn and return false" { - Mock Test-ScoopInstalled { $false } - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Scoop is not installed" } - } - } - - Context "When Scoop command is not found" { - It "Should warn and return false" { - Mock Find-Scoop { $null } - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to find Scoop command" } - } - } - - Context "When no Scoop packages are found" { - It "Should warn and return true" { - Mock Invoke-Expression { $null } - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "No Scoop packages found" } - } - } - - Context "When Scoop export JSON is invalid" { - It "Should warn and show raw output" { - Mock Invoke-Expression { "not-json" } - Mock ConvertFrom-Json { throw "JSON error" } - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to parse scoop export JSON" } - } - } - - Context "When buckets and packages are found" { - It "Should add buckets and packages to YAML data" { - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeTrue - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding bucket: extras" } - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Adding package: git" } - } - } - - Context "When DryRun is used" { - It "Should display YAML output and not write to file" { - $result = Export-InstalledScoopPackages -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Times 0 -Scope It - Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -match "Dry Run" } - } - } - - Context "When OutFile is specified" { - It "Should write YAML output to the specified file" { - $result = Export-InstalledScoopPackages -Config "test.yaml" -OutFile "out.yaml" - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Yaml -Scope It - Assert-MockCalled Out-File -Scope It -ParameterFilter { $FilePath -eq "out.yaml" } - } - } - - Context "When YAML conversion fails" { - It "Should fallback to JSON output" { - Mock ConvertTo-Yaml { throw "YAML error" } - $result = Export-InstalledScoopPackages -Config "test.yaml" -DryRun - $result | Should -BeTrue - Assert-MockCalled ConvertTo-Json -Scope It - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Could not convert to YAML format" } - } - } - - Context "When Out-File fails" { - It "Should write error and return false" { - Mock Out-File { throw "File error" } - $result = Export-InstalledScoopPackages -Config "test.yaml" - $result | Should -BeFalse - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to save configuration" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 b/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 deleted file mode 100644 index db2107d..0000000 --- a/devsetup/Private/Providers/Scoop/Export-InstalledScoopPackages.ps1 +++ /dev/null @@ -1,412 +0,0 @@ -<# -.SYNOPSIS - Exports installed Scoop packages and buckets to a YAML configuration file. - -.DESCRIPTION - This function scans the system for installed Scoop packages and buckets, then exports them to a YAML - configuration file in DevSetup format. It uses 'scoop export' to retrieve comprehensive package information - including versions, buckets, and global installation status. The function can update existing configuration - files by merging new packages with existing ones, or create new configurations from scratch. - -.PARAMETER Config - The path to the YAML configuration file to read from and write to. - This parameter is mandatory and specifies both the input and output file unless OutFile is specified. - -.PARAMETER OutFile - The path to save the updated YAML configuration. - Optional parameter that allows saving to a different file than the input Config file. - -.PARAMETER DryRun - Switch parameter that prevents writing to files and displays the resulting configuration to the console. - Useful for previewing changes before committing them to a file. - -.OUTPUTS - [System.Boolean] - Returns $true if the export completes successfully or if Scoop is not installed (skipped). - Returns $false if there are errors during the export process. - -.EXAMPLE - Export-InstalledScoopPackages -Config "environment.yaml" - - Exports installed Scoop packages to the existing environment.yaml configuration file. - -.EXAMPLE - Export-InstalledScoopPackages -Config "current.yaml" -OutFile "backup.yaml" - - Reads from current.yaml and saves the updated configuration with installed packages to backup.yaml. - -.EXAMPLE - Export-InstalledScoopPackages -Config "dev-env.yaml" -DryRun - - Shows what the configuration would look like without actually saving to file. - -.NOTES - - Requires Scoop to be installed on the system (gracefully skips if not found) - - Uses 'scoop export' command to retrieve package and bucket information in JSON format - - Handles both local and global package installations using Info field detection - - Automatically skips the 'main' bucket as it's installed by default with Scoop - - Merges with existing YAML configuration, preserving other sections and structure - - Supports both simple string format and complex object format for packages and buckets - - Updates existing packages/buckets when versions or sources have changed - - Tracks global installation status and bucket information for each package - - Provides detailed console output with color-coded status messages for all operations - - Creates the devsetup.dependencies.scoop structure if it doesn't exist - - Processes buckets before packages to ensure proper dependency order - - Converts string entries to hashtable format when additional properties are needed - - Preserves existing package properties while updating changed values - - Includes comprehensive error handling for JSON parsing and file operations - - Returns $true even when no packages are found (successful empty result) - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Configuration Export, Package Discovery, YAML Generation -#> - -Function Export-InstalledScoopPackages { - Param( - [Parameter(Mandatory = $true)] - [string]$Config, - [string]$OutFile, - [switch]$DryRun - ) - - try { - # Check if Scoop is installed - if(-Not (Test-ScoopInstalled)) { - Write-Warning "Scoop is not installed. Cannot check for components." - return $false - } - - $scoopCommand = Find-Scoop - if (-not $scoopCommand) { - Write-Warning "Failed to find Scoop command. Cannot check for components." - return $false - } - - # Get list of installed Scoop packages - Write-Host "- Getting list of installed Scoop packages..." -ForegroundColor Gray - - # Get all packages (both local and global) using scoop export - $scoopListLocal = "" - - try { - # Use scoop export - it returns JSON with both local and global packages - $command = "& '$scoopCommand' export" - $scoopListLocal = Invoke-Expression $command 6>$null - if (-not $scoopListLocal) { - Write-Warning "No Scoop packages found or scoop export command failed." - return $true - } - } catch { - Write-Verbose "Could not get Scoop packages: $_" - } - - # TODO: - # scoop kinda sucks, they do so many weird things, for instance scoop install helm works fine and produces what you'd expect - # scoop install main/helm, totally kills what the source was in scoop list and provides a path to a json file - # scoop install main/helm@3.17.4 is even worse, it provides for the source - # in order to make sure we dont have problems with exported configurations we need to "look up" each package and see what bucket - # it actually belongs in while exporting so when someone imports it back in later, it provides a valid bucket to install from - # scoop search '^helm$' - - $scoopPackages = @() - $scoopBuckets = @() - - # Parse packages from scoop export JSON - if ($scoopListLocal) { - try { - # Convert JSON output to PowerShell object - $exportData = $scoopListLocal | ConvertFrom-Json - - # Parse buckets from the JSON structure - if ($exportData.buckets -and $exportData.buckets.Count -gt 0) { - foreach ($bucket in $exportData.buckets) { - # Skip the 'main' bucket as it's automatically installed with Scoop - if ($bucket.Name -eq "main") { - Write-Debug "Skipping 'main' bucket (automatically installed with Scoop)" - continue - } - - $bucketInfo = @{ - name = $bucket.Name - source = $bucket.Source - } - $scoopBuckets += $bucketInfo - Write-Debug "Found bucket: $($bucket.Name) (source: $($bucket.Source))" - } - } - - # Parse apps from the JSON structure - if ($exportData.apps -and $exportData.apps.Count -gt 0) { - foreach ($app in $exportData.apps) { - $packageName = $app.Name - $version = $app.Version - $bucket = $app.Source - - # Determine if this is a global install based on the Info field - $isGlobal = $app.Info -eq "Global install" - - Write-Debug "Found package from JSON export: $packageName (version: $version, bucket: $bucket, global: $isGlobal)" - $packageInfo = @{ - name = $packageName - version = $version - global = $isGlobal - } - - # Always include bucket information for clarity - if ($bucket) { - $packageInfo.bucket = $bucket - } - - $scoopPackages += $packageInfo - } - } else { - Write-Verbose "No apps found in scoop export JSON" - } - } catch { - Write-Warning "Failed to parse scoop export JSON: $_" - Write-Verbose "Raw export output: $scoopListLocal" - } - } - - if ($scoopPackages.Count -eq 0) { - Write-Warning "No Scoop packages found." - return $true - } - - Write-Debug "Found $($scoopPackages.Count) Scoop packages and $($scoopBuckets.Count) buckets" - - # Read existing YAML configuration - $YamlData = Read-ConfigurationFile -Config $Config - - # Ensure scoopPackages and scoopBuckets sections exist - if (-not $YamlData.devsetup) { $YamlData.devsetup = @{} } - if (-not $YamlData.devsetup.dependencies) { $YamlData.devsetup.dependencies = @{} } - if (-not $YamlData.devsetup.dependencies.scoop) { $YamlData.devsetup.dependencies.scoop = @{} } - if (-not $YamlData.devsetup.dependencies.scoop.packages) { $YamlData.devsetup.dependencies.scoop.packages = @() } - if (-not $YamlData.devsetup.dependencies.scoop.buckets) { $YamlData.devsetup.dependencies.scoop.buckets = @() } - - # Add buckets to YAML data first (packages may depend on these buckets) - foreach ($bucket in $scoopBuckets) { - # Check if bucket already exists - $existingBucket = $YamlData.devsetup.dependencies.scoop.buckets | Where-Object { - ($_ -is [string] -and $_ -eq $bucket.name) -or - ($_ -is [hashtable] -and $_.name -eq $bucket.name) - } - - if (-not $existingBucket) { - Write-Host " - Adding bucket: $($bucket.name) ($($bucket.source))" -ForegroundColor Gray - - # Create bucket object - $bucketObj = @{ - name = $bucket.name - source = $bucket.source - } - - $YamlData.devsetup.dependencies.scoop.buckets += $bucketObj - } else { - # Bucket exists, check if source has changed - $existingSource = $null - - if ($existingBucket -is [hashtable]) { - $existingSource = $existingBucket.source - } - - if ($existingSource -and $existingSource -ne $bucket.source) { - Write-Host " - Updating bucket: $($bucket.name) ($existingSource -> $($bucket.source))" -ForegroundColor Cyan - - # Find index and update - $index = $YamlData.devsetup.dependencies.scoop.buckets.IndexOf($existingBucket) - - if ($existingBucket -is [string]) { - # Convert string to hashtable with source - $bucketObj = @{ - name = $bucket.name - source = $bucket.source - } - - $YamlData.devsetup.dependencies.scoop.buckets[$index] = $bucketObj - } else { - # Update existing hashtable - $YamlData.devsetup.dependencies.scoop.buckets[$index].source = $bucket.source - } - } elseif (-not $existingSource) { - Write-Host " - Updating bucket: $($bucket.name)" -ForegroundColor Yellow - - # Find index and add source - $index = $YamlData.devsetup.dependencies.scoop.buckets.IndexOf($existingBucket) - - if ($existingBucket -is [string]) { - # Convert string to hashtable with source - $bucketObj = @{ - name = $bucket.name - source = $bucket.source - } - - $YamlData.devsetup.dependencies.scoop.buckets[$index] = $bucketObj - } else { - # Add source to existing hashtable - $YamlData.devsetup.dependencies.scoop.buckets[$index].source = $bucket.source - } - } else { - Write-Host " - Skipping bucket (No Change): $($bucket.name) ($($bucket.source))" -ForegroundColor Gray - } - } - } - - # Add packages to YAML data - foreach ($package in $scoopPackages) { - # Check if package already exists - $existingPackage = $YamlData.devsetup.dependencies.scoop.packages | Where-Object { - ($_ -is [string] -and $_ -eq $package.name) -or - ($_ -is [hashtable] -and $_.name -eq $package.name) - } - - if (-not $existingPackage) { - Write-Host " - Adding package: $($package.name) ($($package.version))" -ForegroundColor Gray - - # Create package object with all relevant properties - $packageObj = @{ - name = $package.name - version = $package.version - } - - if ($package.bucket) { - $packageObj.bucket = $package.bucket - } - - if ($package.global) { - $packageObj.global = $package.global - } - - $YamlData.devsetup.dependencies.scoop.packages += $packageObj - } else { - # Package exists, check if version has changed - $existingVersion = $null - $existingGlobal = $false - $existingBucket = $null - - if ($existingPackage -is [hashtable]) { - $existingVersion = $existingPackage.version - $existingGlobal = $existingPackage.global - $existingBucket = $existingPackage.bucket - } - - if ($existingVersion -and $existingVersion -ne $package.version) { - Write-Host " - Updating package: $($package.name) ($existingVersion -> $($package.version))" -ForegroundColor Cyan - - # Find index and update - $index = $YamlData.devsetup.dependencies.scoop.packages.IndexOf($existingPackage) - - # Preserve existing package structure but update version - if ($existingPackage -is [string]) { - # Convert string to hashtable with version and other properties - $packageObj = @{ - name = $package.name - version = $package.version - } - - if ($package.bucket) { - $packageObj.bucket = $package.bucket - } - - if ($package.global) { - $packageObj.global = $package.global - } - - $YamlData.devsetup.dependencies.scoop.packages[$index] = $packageObj - } else { - # Update existing hashtable - $YamlData.devsetup.dependencies.scoop.packages[$index].version = $package.version - - # Update bucket if changed - if ($package.bucket -and (-not $existingBucket -or $existingBucket -ne $package.bucket)) { - $YamlData.devsetup.dependencies.scoop.packages[$index].bucket = $package.bucket - } - - # Update global flag if changed - if ($package.global -ne $existingGlobal) { - $YamlData.devsetup.dependencies.scoop.packages[$index].global = $package.global - } - } - } elseif (-not $existingVersion) { - Write-Host " - Updating package: $($package.name)" -ForegroundColor Yellow - - # Find index and add version and other properties - $index = $YamlData.devsetup.dependencies.scoop.packages.IndexOf($existingPackage) - - if ($existingPackage -is [string]) { - # Convert string to hashtable with version and properties - $packageObj = @{ - name = $package.name - version = $package.version - } - - if ($package.bucket) { - $packageObj.bucket = $package.bucket - } - - if ($package.global) { - $packageObj.global = $package.global - } - - $YamlData.devsetup.dependencies.scoop.packages[$index] = $packageObj - } else { - # Add version and other properties to existing hashtable - $YamlData.devsetup.dependencies.scoop.packages[$index].version = $package.version - - if ($package.bucket -and -not $existingBucket) { - $YamlData.devsetup.dependencies.scoop.packages[$index].bucket = $package.bucket - } - - if ($package.global -and -not $existingGlobal) { - $YamlData.devsetup.dependencies.scoop.packages[$index].global = $package.global - } - } - } else { - Write-Host " - Skipping package (No Change): $($package.name) ($($package.version))" -ForegroundColor Gray - } - } - } - - # Convert to YAML - try { - $yamlOutput = $YamlData | ConvertTo-Yaml - } - catch { - Write-Warning "Could not convert to YAML format. Showing PowerShell object instead:" - $yamlOutput = $YamlData | ConvertTo-Json -Depth 10 - } - - # Handle output based on parameters - if ($DryRun) { - Write-Host "`nDry Run - Configuration would be saved as:" -ForegroundColor Cyan - Write-Host $yamlOutput -ForegroundColor White - Write-Host "`nNo files were modified (dry run mode)." -ForegroundColor Yellow - } else { - # Determine output file - $outputFile = if ($OutFile) { $OutFile } else { $Config } - - try { - Write-Debug "`nSaving configuration to: $outputFile" - $yamlOutput | Out-File -FilePath $outputFile - Write-Debug "Configuration saved successfully!" - } - catch { - Write-Error "Failed to save configuration to $outputFile`: $_" - return $false - } - } - - Write-Host "Scoop packages conversion completed!" -ForegroundColor Green - return $true - } - catch { - Write-Error "Error converting Scoop packages: $_" - return $false - } -} diff --git a/devsetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 b/devsetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 deleted file mode 100644 index 8b5c4b7..0000000 --- a/devsetup/Private/Providers/Scoop/Find-Scoop.Tests.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-EnvironmentVariable.ps1 -} - -Describe "Find-Scoop" { - Context "When scoop is found by Get-Command" { - BeforeEach { - Mock Get-Command { return 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' } - } - It "should return scoop" { - $scoop = Find-Scoop - $scoop | Should -Be "scoop" - } - } - - Context "When scoop is not found by Get-Command or any other option it should return null" { - BeforeEach { - Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - } - It "should return null" { - $scoop = Find-Scoop - $scoop | Should -BeNullOrEmpty - } - } - - Context "When scoop is not found by Get-Command but scoop.ps1 is found" { - BeforeEach { - Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.ps1' -Value 'Scoop PowerShell Script' - } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop.ps1" { - $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.ps1" - } - } - - Context "When scoop is not found by Get-Command but scoop.cmd is found" { - BeforeEach { - Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop.cmd' -Value 'Scoop Command Script' - } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop.cmd" { - $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop.cmd" - } - } - - Context "When scoop is not found by Get-Command but scoop is found" { - BeforeEach { - Mock Get-Command { return $null } - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - New-Item -Path 'TestDrive:\Users\Test User\scoop\shims' -ItemType Directory -Force | Out-Null - Set-Content 'TestDrive:\Users\Test User\scoop\shims\scoop' -Value 'Scoop Command Script' - } - It "should return TestDrive:\Users\Test User\scoop\shims\scoop" { - $scoop = Find-Scoop - $scoop | Should -Be "TestDrive:\Users\Test User\scoop\shims\scoop" - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Find-Scoop.ps1 b/devsetup/Private/Providers/Scoop/Find-Scoop.ps1 deleted file mode 100644 index 2acb5ee..0000000 --- a/devsetup/Private/Providers/Scoop/Find-Scoop.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -<# -.SYNOPSIS - Locates the Scoop package manager executable on the system. - -.DESCRIPTION - This function searches for the Scoop package manager executable using multiple detection methods. - It first attempts to find 'scoop' in the system PATH, and if not found, searches for Scoop - installation files in the default user profile directory. The function returns the appropriate - command or file path that can be used to execute Scoop operations. - -.OUTPUTS - [System.String] - Returns "scoop" if found in PATH, the full file path to the Scoop executable if found in the user profile, - or $null if Scoop cannot be located. - -.EXAMPLE - Find-Scoop - - Locates the Scoop executable on the current system. - -.EXAMPLE - $scoopCommand = Find-Scoop - if ($scoopCommand) { - & $scoopCommand list - } else { - Write-Warning "Scoop not found" - } - - Demonstrates using the returned command to execute Scoop operations. - -.EXAMPLE - switch (Find-Scoop) { - "scoop" { "Scoop found in PATH" } - { $_ -like "*scoop.ps1" } { "Found PowerShell script: $_" } - { $_ -like "*scoop.cmd" } { "Found batch file: $_" } - { $_ -like "*scoop" } { "Found executable: $_" } - $null { "Scoop not found" } - } - - Shows handling different types of Scoop installations. - -.NOTES - - Performs multiple checks to locate Scoop installations: - 1. Checks if 'scoop' command is available in PATH using Get-Command - 2. Searches for ~\scoop\shims\scoop.ps1 (PowerShell script) - 3. Searches for ~\scoop\shims\scoop.cmd (Command batch file) - 4. Searches for ~\scoop\shims\scoop (Executable) - - Returns the most accessible form first (PATH command before file paths) - - Suppresses errors when checking for the scoop command to avoid console output - - The returned value can be used directly with the call operator (&) or Invoke-Expression - - Does not verify that the found executable is functional, only that it exists - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Executable Location, Path Resolution -#> -Function Find-Scoop { - [CmdletBinding()] - Param () - - if (Get-Command scoop -ErrorAction SilentlyContinue) { - return "scoop" - } else { - # Check for Scoop in user profile directory - $userProfilePath = (Get-EnvironmentVariable USERPROFILE) - $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop.ps1" - if (Test-Path $scoopPath) { - return $scoopPath - } - - $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop.cmd" - if (Test-Path $scoopPath) { - return $scoopPath - } - - $scoopPath = Join-Path $userProfilePath "scoop\shims\scoop" - if (Test-Path $scoopPath) { - return $scoopPath - } - } - return $null -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 b/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 deleted file mode 100644 index 617a9d7..0000000 --- a/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.Tests.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-ScoopCacheFile.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Get-DevSetupCachePath.ps1 -} - -Describe "Get-ScoopCacheFile" { - Context "When scoop is found by Get-Command" { - BeforeEach { - Mock Get-DevSetupCachePath { return 'TestDrive:\Users\Test User\.devsetup\.cache' } - } - It "should return the correct scoop cache file path" { - $scoopCacheFile = Get-ScoopCacheFile - $scoopCacheFile | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache\scoop.cache" - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 b/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 deleted file mode 100644 index 755c406..0000000 --- a/devsetup/Private/Providers/Scoop/Get-ScoopCacheFile.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -<# -.SYNOPSIS - Gets the file path for the Scoop package cache file. - -.DESCRIPTION - This function constructs and returns the full path to the Scoop package cache file within the DevSetup - cache directory. The cache file is used to store information about installed Scoop packages and their - versions for performance optimization and offline reference. The function uses Get-DevSetupCachePath - to ensure the cache directory exists before returning the file path. - -.OUTPUTS - [System.String] - Returns the full path to the Scoop cache file (scoop.cache) within the DevSetup cache directory. - -.EXAMPLE - Get-ScoopCacheFile - - Returns the path to the Scoop cache file, e.g., "C:\Users\Username\.devsetup\.cache\scoop.cache" - -.EXAMPLE - $scoopCacheFile = Get-ScoopCacheFile - if (Test-Path $scoopCacheFile) { - $cachedData = Get-Content $scoopCacheFile - } - - Gets the cache file path and checks if it exists before reading cached data. - -.EXAMPLE - $cacheFile = Get-ScoopCacheFile - Export-Clixml -Path $cacheFile -InputObject $scoopPackages - - Uses the cache file path to save Scoop package information. - -.NOTES - - Uses Get-DevSetupCachePath to ensure the cache directory exists - - Returns a consistent file path (scoop.cache) within the DevSetup cache structure - - The cache file is used for storing Scoop package metadata and version information - - Does not create the cache file itself - only returns the path where it should be located - - Used by other Scoop-related functions for performance optimization and data persistence - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Path Management, Cache Management, File System Operations -#> - -Function Get-ScoopCacheFile { - [CmdletBinding()] - Param() - - # Get the DevSetup cache path - $cachePath = Get-DevSetupCachePath - - # Construct the full path to the cache file - $cacheFilePath = Join-Path -Path $cachePath -ChildPath "scoop.cache" - - return $cacheFilePath -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 b/devsetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 deleted file mode 100644 index 2e1807d..0000000 --- a/devsetup/Private/Providers/Scoop/Get-ScoopVersion.Tests.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-ScoopVersion.ps1 - . $PSScriptRoot\Find-Scoop.ps1 -} - -Describe "Get-ScoopVersion" { - Context "When scoop is not found" { - BeforeEach { - Mock Find-Scoop { return $null } - } - It "should return null" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be $null - } - } - - Context "When scoop is found but returns no version info" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - return '' - } - Mock Get-Content { - return $null - } - Mock Remove-Item { - return $null - } - } - It "should return null" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be $null - } - } - - Context "When scoop is found and returns version info" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - return "b588a06e (HEAD -> master, origin/master, origin/HEAD) chore(release): Bump to version 0.5.3 (resync) (#6436) -945371469 (HEAD -> master, origin/master, origin/HEAD) tailwindcss: Update to version 4.1.12 -7ecbe6adcc (HEAD -> master, origin/master, origin/HEAD) processing4: Update to version 1306-4.4.6" - } - Mock Remove-Item {} - } - It "should return the scoop version" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be "0.5.3" - } - } - - Context "When scoop is found and returns git hash info" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - return "b588a06e (HEAD -> master, origin/master, origin/HEAD) chore(release): -945371469 (HEAD -> master, origin/master, origin/HEAD) tailwindcss: -7ecbe6adcc (HEAD -> master, origin/master, origin/HEAD) processing4:" - } - Mock Remove-Item {} - } - It "should return the scoop git hash" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be "b588a06e" - } - } - - Context "When scoop is found and but the version format changed" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - return "(HEAD -> master, origin/master, origin/HEAD) chore(release): -(HEAD -> master, origin/master, origin/HEAD) tailwindcss: -(HEAD -> master, origin/master, origin/HEAD) processing4:" - } - Mock Remove-Item {} - } - It "should return installed" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be "installed" - } - } - - Context "When scoop is found and but the version format changed to not using git release rendering" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - return "Current Scoop version: -v0.5.3 - Released at 2025-08-11" - } - Mock Remove-Item {} - } - It "should return the scoop version" { - $scoopVersion = Get-ScoopVersion - $scoopVersion | Should -Be "0.5.3" - } - } - - Context "When scoop is found and but an error is thrown" { - BeforeEach { - Mock Find-Scoop { return 'scoop' } - Mock Invoke-Expression { - throw "This is a sample error" - } - Mock Remove-Item {} - } - It "should return installed" { - $scoopVersion = Get-ScoopVersion -WarningAction SilentlyContinue - $scoopVersion | Should -Be "installed" - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 b/devsetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 deleted file mode 100644 index b619966..0000000 --- a/devsetup/Private/Providers/Scoop/Get-ScoopVersion.ps1 +++ /dev/null @@ -1,103 +0,0 @@ -<# -.SYNOPSIS - Retrieves the version information for the installed Scoop package manager. - -.DESCRIPTION - This function queries the installed Scoop package manager to determine its version. It uses the 'scoop --version' - command and parses the output to extract version information. The function handles both tagged releases - (e.g., "v0.5.3") and development builds identified by commit hashes. Output is completely suppressed during - execution to avoid console clutter. - -.OUTPUTS - [System.String] - Returns the Scoop version string if found, "installed" if version cannot be determined but Scoop is present, - or $null if Scoop is not installed or cannot be found. - -.EXAMPLE - Get-ScoopVersion - - Retrieves the version of the currently installed Scoop package manager. - -.EXAMPLE - $version = Get-ScoopVersion - if ($version) { - Write-Host "Scoop version: $version" - } else { - Write-Host "Scoop is not installed" - } - - Demonstrates checking if Scoop is installed and displaying its version. - -.EXAMPLE - switch (Get-ScoopVersion) { - $null { "Scoop not found" } - "installed" { "Scoop is installed but version unknown" } - default { "Scoop version: $_" } - } - - Shows handling different return scenarios from the function. - -.NOTES - - Requires Scoop to be installed and accessible via Find-Scoop function - - Uses Start-Process with output redirection to completely suppress console output - - Parses version output with two fallback strategies: - 1. Tagged release format: "v0.5.3 - Released at..." - 2. Development build format: "ebd8c036 (HEAD -> master..." - - Creates temporary files for output capture which are automatically cleaned up - - Returns "installed" if Scoop responds but version cannot be parsed - - Returns $null if Scoop is not found or accessible - - Handles errors gracefully without stopping execution - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Version Detection, System Information -#> -Function Get-ScoopVersion { - [CmdletBinding()] - Param () - - $scoopVersion = $null - $scoopCommand = Find-Scoop - if ($scoopCommand) { - try { - # Use Start-Process with PowerShell to completely suppress console output - # Scoop is a PowerShell script, so we always need to run it through PowerShell - $command = "& '$scoopCommand' --version" - - $scoopVersionOutput = Invoke-Expression $command 6>$null - - if ($scoopVersionOutput) { - # Try to find version tag format first (e.g., "v0.5.3 - Released at...") - $outputLines = $scoopVersionOutput -split "`n" | Where-Object { $_ -and $_.Trim() } - $versionLine = $outputLines | Where-Object { $_ -match "[0-9]+\-?[0-9]?\.[0-9]+\.[0-9]+" } | Select-Object -First 1 - - if ($versionLine) { - if ($versionLine -match "([0-9]+\-?[0-9]?\.[0-9]+\.[0-9]+)") { - $scoopVersion = $matches[1] - } - } else { - # Fallback to commit hash format (e.g., "ebd8c036 (HEAD -> master...") - $hashLine = $outputLines | Where-Object { $_ -match "^[a-f0-9]{8,12}" } | Select-Object -First 1 - if ($hashLine) { - $hashParts = $hashLine.Split(' ') - if ($hashParts -and $hashParts.Length -gt 0) { - $scoopVersion = $hashParts[0] # Get just the commit hash - } - } else { - $scoopVersion = "installed" - } - } - } - } catch { - Write-Warning "Could not get Scoop version: $_" - $scoopVersion = "installed" - } - return $scoopVersion - } else { - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 b/devsetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 deleted file mode 100644 index ff0990f..0000000 --- a/devsetup/Private/Providers/Scoop/Install-Scoop.Tests.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-Scoop.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Get-ScoopVersion.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } -} - -Describe "Install-Scoop" { - Context "When scoop is installed" { - BeforeEach { - Mock Get-ScoopVersion { return '0.5.3' } - Mock Test-ScoopInstalled { return $true } - } - It "Should return true" { - Install-Scoop | Should -Be $true - } - } - - Context "When scoop is installed but the version cant be found" { - BeforeEach { - Mock Get-ScoopVersion { return $null } - Mock Test-ScoopInstalled { return $true } - } - - It "Should return false" { - Install-Scoop | Should -Be $false - } - } - - Context "When scoop is not installed" { - BeforeEach { - Mock Test-ScoopInstalled { return $false } - Mock Get-ScoopVersion { return '0.5.3' } - Mock Set-ExecutionPolicy { return $true } - Mock Invoke-RestMethod { return $true } - Mock Invoke-Expression { return $true } - } - It "Should install it and return true" { - Install-Scoop | Should -Be $true - } - } - - Context "When scoop is not installed" { - BeforeEach { - Mock Test-ScoopInstalled { return $false } - Mock Get-ScoopVersion { return '0.5.3' } - Mock Set-ExecutionPolicy { return $true } - Mock Invoke-RestMethod { return $true } - Mock Invoke-Expression { throw "Failed" } - } - It "Should try to install it and throw an error when it fails" { - { Install-Scoop } | Should -Throw "Failed to install scoop: Failed" - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-Scoop.ps1 b/devsetup/Private/Providers/Scoop/Install-Scoop.ps1 deleted file mode 100644 index 5baa4cf..0000000 --- a/devsetup/Private/Providers/Scoop/Install-Scoop.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -<# -.SYNOPSIS - Installs the Scoop package manager on the system. - -.DESCRIPTION - This function installs Scoop package manager by downloading and executing the official installation script - from get.scoop.sh. It automatically configures PowerShell execution policy settings and validates the - installation success. The function performs pre-installation checks to avoid duplicate installations - and uses Get-ScoopVersion to verify successful installation completion. - -.OUTPUTS - [System.Boolean] - Returns $true if Scoop was successfully installed or was already installed. - Returns $false if the installation verification fails. - -.EXAMPLE - Install-Scoop - - Installs Scoop package manager on the current system. - -.EXAMPLE - if (-not (Test-ScoopInstalled)) { - Install-Scoop - Write-Host "Scoop is now available for package management" - } - - Shows conditional installation only when Scoop is not already present. - -.NOTES - **Installation Process:** - - Checks if Scoop is already installed using Test-ScoopInstalled - - Sets execution policy to RemoteSigned for script download - - Downloads and executes installation script from get.scoop.sh with -RunAs parameter - - Sets execution policy to Bypass after installation - - Verifies installation using Get-ScoopVersion - - **Requirements:** - - Internet connection to download the installation script - - PowerShell execution policy modification permissions - - **Installation Method:** - - Uses `Invoke-RestMethod get.scoop.sh` to download the installation script - - Executes with `-RunAs` parameter for non-elevated user installation from elevated PowerShell - - Automatically handles execution policy configuration (RemoteSigned → Bypass) - - **Verification:** - - Uses Get-ScoopVersion to confirm successful installation - - Returns boolean based on version retrieval success - - Performs same verification check whether installing or if already installed - - **Error Handling:** - - Throws exception if installation script execution fails - - Uses SilentlyContinue for execution policy to avoid errors - - Suppresses installation output using Out-Null for clean console experience - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Manager Installation, System Setup -#> -Function Install-Scoop { - [CmdletBinding()] - Param () - - Write-StatusMessage "- Installing Scoop package manager" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - if(-not (Test-ScoopInstalled)) { - try { - Invoke-Expression "& {$(Invoke-RestMethod get.scoop.sh)} -RunAs" | Out-Null - } catch { - throw "Failed to install scoop: $_" - } - } - - $scoopVersion = Get-ScoopVersion - if(-not ([string]::IsNullOrEmpty($scoopVersion))) { - Write-StatusMessage "[OK]" -ForegroundColor Green - return $true - } else { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 deleted file mode 100644 index 24a4ac9..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopBucket.Tests.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-ScoopBucket.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 -} - -Describe "Install-ScoopBucket" { - Context "When scoop is not installed" { - It "Should return false" { - Mock Test-ScoopInstalled { return $false } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - Context "When scoop is not found" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return $null } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - Context "When a Bucket is already installed" { - It "Should return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $true - } - } - Context "When a Bucket is not already installed and it fails to install it" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Invoke-Command { - $global:LASTEXITCODE = 1 - return $null - } -Verifiable - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - - Context "When a Bucket is not already installed and it gets installed but fails to write the cache" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Invoke-Command { - $global:LASTEXITCODE = 0 - return $null - } -Verifiable - Mock Write-ScoopCache { return $false } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - Context "When a Bucket is not already installed and installing it causes an error to be thrown" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Invoke-Command { - throw 'Failed' - } -Verifiable - Mock Write-ScoopCache { return $true } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - Context "When a Bucket is not already installed and it gets installed and writes the cache" { - It "Should return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Invoke-Command { - $global:LASTEXITCODE = 0 - return $null - } -Verifiable - Mock Write-ScoopCache { return $true } - $result = Install-ScoopBucket -Name "extras" - $result | Should -Be $true - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 deleted file mode 100644 index d37f655..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopBucket.ps1 +++ /dev/null @@ -1,116 +0,0 @@ -<# -.SYNOPSIS - Adds a Scoop bucket to the system. - -.DESCRIPTION - This function adds a specified Scoop bucket by executing the 'scoop bucket add' command. - It includes validation to ensure Scoop is installed and available before attempting the bucket addition. - The function supports adding both official buckets (by name only) and custom buckets (with source URL). - It checks if the bucket is already installed before attempting to add it and provides error handling - with a boolean result indicating success or failure. - -.PARAMETER Name - The name of the Scoop bucket to add. - This parameter is mandatory and must be a valid string representing a bucket name. - -.PARAMETER Source - The source URL or Git repository for the bucket. - Optional parameter used for adding custom buckets. If not specified, Scoop will attempt to add an official bucket by name. - -.OUTPUTS - [System.Boolean] - Returns $true if the bucket was successfully added or is already installed, $false if the operation failed. - -.EXAMPLE - Install-ScoopBucket -Name "extras" - - Adds the official 'extras' bucket to Scoop. - -.EXAMPLE - Install-ScoopBucket -Name "nonportable" - - Adds the official 'nonportable' bucket to Scoop. - -.EXAMPLE - Install-ScoopBucket -Name "custom-bucket" -Source "https://github.com/user/scoop-bucket" - - Adds a custom bucket from a GitHub repository. - -.EXAMPLE - $result = Install-ScoopBucket -Name "games" - if ($result) { - Write-Host "Games bucket added successfully" - } else { - Write-Host "Failed to add games bucket" - } - - Demonstrates capturing the return value to check bucket addition success. - -.NOTES - - Requires Scoop to be installed on the system - - Uses Test-ScoopComponentInstalled to check if bucket is already installed before attempting to add it - - Returns $true if bucket is already installed (considered successful since goal is achieved) - - Returns $false immediately if Scoop is not installed or cannot be found - - Uses $LASTEXITCODE to verify command execution success - - Provides warning messages for common failure scenarios - - Uses try-catch error handling for robust failure management - - Official buckets can be added by name only (extras, nonportable, games, etc.) - - Custom buckets require both name and source URL parameters - - Suppresses command output using Out-Null to avoid console clutter - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Bucket Management, Repository Addition -#> -Function Install-ScoopBucket { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [string]$Name, - [string]$Source - ) - - if(-Not (Test-ScoopInstalled)) { - return $false - } - - $scoopCommand = Find-Scoop - if (-Not ($scoopCommand)) { - return $false - } - - try { - [InstalledState]$bucketState = Test-ScoopComponentInstalled -Bucket -Name $Name - if ($bucketState -ne [InstalledState]::Pass) { - $installArgs = @("bucket", "add", $Name) - - # If a source is provided, add it to the command arguments - if ($Source) { - $installArgs += $Source - } - - # Execute the command to add the bucket - $command = { - & $scoopCommand @installArgs *> $null - } - Invoke-Command -ScriptBlock $command | Out-Null - if ($LASTEXITCODE -ne 0) { - return $false - } - - if (-not (Write-ScoopCache)) { - return $false - } - - return $true - } else { - return $true - } - } catch { - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 deleted file mode 100644 index d59c1c3..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopComponents.Tests.ps1 +++ /dev/null @@ -1,123 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-ScoopComponents.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\Install-ScoopBucket.ps1 - . $PSScriptRoot\Install-ScoopPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } - Mock Write-Host {} - Mock Write-Error {} -} - -Describe "Install-ScoopComponents" { - - Context "When Scoop is not installed" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $false } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras") - packages = @("git") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When Scoop configuration is missing" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When Write-ScoopCache fails" { - It "Should return false and error" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $false } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras") - packages = @("git") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When only buckets are present and all install succeed" { - It "Should return true and process all buckets" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Install-ScoopBucket { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ buckets = @("extras", "versions") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When only packages are present and all install succeed" { - It "Should return true and process all packages" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Install-ScoopPackage { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When buckets and packages are present and some installs fail" { - It "Should return true and report failures" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - $bucketCallCount = 0 - Mock Install-ScoopBucket -MockWith { - $bucketCallCount++ - if ($bucketCallCount -eq 1) { return $false } else { return $true } - } - $packageCallCount = 0 - Mock Install-ScoopPackage -MockWith { - $packageCallCount++ - if ($packageCallCount -eq 2) { return $false } else { return $true } - } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras", "versions") - packages = @("git", "nodejs") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When no buckets or packages are present" { - It "Should return true and skip package installation" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When an exception occurs during package install" { - It "Should catch and continue, returning true" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Install-ScoopBucket { return $true } - Mock Install-ScoopPackage { throw "Unexpected error" } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When an exception occurs in the main try block" { - It "Should return false" { - Mock Test-ScoopInstalled { throw "Critical error" } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras") - packages = @("git") } } } } - $result = Install-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 deleted file mode 100644 index 699d8da..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopComponents.ps1 +++ /dev/null @@ -1,256 +0,0 @@ -<# -.SYNOPSIS - Installs Scoop buckets and packages from YAML configuration data. - -.DESCRIPTION - This function processes YAML configuration data to install Scoop buckets and packages in sequence. - It validates Scoop installation, updates the cache before proceeding, and processes buckets before - packages to ensure bucket availability. The function supports both simple string formats and complex - object formats for buckets and packages, allowing for detailed configuration including versions, - custom sources, and global installation scope. Progress is tracked and reported for both buckets - and packages using color-coded status messages. - -.PARAMETER YamlData - The YAML configuration data containing Scoop bucket and package definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.scoop.buckets and/or devsetup.dependencies.scoop.packages - -.OUTPUTS - [System.Boolean] - Returns $false if Scoop is not installed, cannot be found, configuration is invalid, or cache update fails. - Returns $true if installation completes successfully (even if individual items fail). - -.EXAMPLE - $yamlData = Get-Content "config.yaml" | ConvertFrom-Yaml - Install-ScoopComponents -YamlData $yamlData - - Installs Scoop buckets and packages from a YAML configuration file. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - scoop = @{ - buckets = @( - "extras", - @{ - name = "custom-bucket" - source = "https://github.com/user/scoop-bucket" - } - ) - packages = @( - "git", - @{ - name = "nodejs" - version = "18.17.0" - }, - @{ - name = "7zip" - global = $true - }, - @{ - name = "firefox" - bucket = "extras" - } - ) - } - } - } - } - Install-ScoopComponents -YamlData $yamlData - - Demonstrates the PSCustomObject structure and installs the configured components. - -.EXAMPLE - if (Install-ScoopComponents -YamlData $config) { - Write-Host "Scoop components installation completed" - } else { - Write-Host "Scoop components installation failed" - } - - Shows checking the return value to verify installation completion. - -.NOTES - - Requires Scoop to be installed on the system using Test-ScoopInstalled - - Returns $false immediately if Scoop is not installed or cannot be found - - Returns $false if YAML configuration structure is invalid or missing scoop section - - Updates Scoop cache using Write-ScoopCache before installation begins - - Returns $false if cache update fails to ensure accurate installation state - - Processes buckets before packages to ensure bucket availability for package installations - - Gracefully handles missing buckets or packages sections in configuration - - Supports two bucket specification formats: - * Simple string: "bucketname" - * Complex object: @{ name = "bucketname"; source = "https://github.com/user/scoop-bucket" } - - Supports two package specification formats: - * Simple string: "packagename" - * Complex object: @{ name = "packagename"; version = "1.0.0"; bucket = "extras"; global = $true } - - Validates component names and skips entries with missing names - - Uses Install-ScoopBucket and Install-ScoopPackage functions for actual installation - - Provides detailed progress reporting with component counts and property information - - Uses color-coded console output: Cyan for headers, Gray for items, Green/Red for status - - Displays formatted component information including version, bucket, and global flags - - Continues processing remaining components even if individual installations fail - - Returns $true for overall success even with individual component failures - - Includes comprehensive try-catch error handling with descriptive error messages - - Tracks and reports separate counts for buckets and packages processed - -.LINK - -.COMPONENT - DevSetup.Scoop - -.FUNCTIONALITY - Bulk Installation, Configuration Processing, Package Management -#> -Function Install-ScoopComponents { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [PSCustomObject]$YamlData - ) - - try { - if(-Not (Test-ScoopInstalled)) { - Write-StatusMessage "Scoop is not installed. Cannot check for components." -Verbosity "Warning" - return $false - } - - # Check if scoop packages exist in configuration - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.scoop) { - Write-StatusMessage "Scoop configuration not found in YAML. Skipping installation." -Verbosity "Warning" - return $false - } - - if (-not (Write-ScoopCache)) { - Write-Error "Failed to write Scoop cache file: $CacheFilePath" - return $false - } - - $bucketCount = 0 - Write-StatusMessage "- Installing Scoop buckets from configuration:" -ForegroundColor Cyan - # Handle buckets first if they exist in configuration - if ($YamlData.devsetup.dependencies.scoop.buckets) { - foreach ($bucket in $YamlData.devsetup.dependencies.scoop.buckets) { - if (-not $bucket) { continue } - - # Handle both string format and object format - $bucketName = if ($bucket -is [string]) { $bucket } else { $bucket.name } - $bucketSource = if ($bucket -is [hashtable] -and $bucket.source) { $bucket.source } else { $null } - - $installParams = @{ - Name = $bucketName - } - - if ($bucketSource) { - $installParams.Source = $bucketSource - } - - # Use Install-ScoopBucket function to handle bucket installation - if ($bucketName -and $bucketSource) { - Write-StatusMessage "- Adding Scoop bucket: $bucketName (source: $bucketSource)" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine - } else { - Write-StatusMessage "- Adding Scoop bucket: $bucketName" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine - } - - $installationStatus = Install-ScoopBucket @installParams - - if (-not $installationStatus) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } else { - $bucketCount++ - Write-StatusMessage "[OK]" -ForegroundColor Green - } - } - } - - Write-StatusMessage "- Scoop buckets installation completed! Processed $bucketCount buckets." -ForegroundColor Green - - Write-Host "" - - # Check if scoop packages exist in configuration - if (-not $YamlData.devsetup.dependencies.scoop.packages) { - Write-StatusMessage "Scoop packages not found in YAML configuration. Skipping package installation." -Verbosity "Warning" - return $true - } - - $scoopPackages = $YamlData.devsetup.dependencies.scoop.packages - Write-StatusMessage "- Installing Scoop packages from configuration:" -ForegroundColor Cyan - - $packageCount = 0 - - # Install packages - foreach ($package in $scoopPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Normalize package to object format - if ($package -is [string]) { - $packageObj = @{ name = $package } - } else { - $packageObj = $package - } - - # Validate package name - if ([string]::IsNullOrEmpty($packageObj.name)) { - Write-StatusMessage "- Skipping package entry, No name specified" -Verbosity "Warning" -Indent 2 -Width 112 - continue - } - - # Use Install-ScoopPackage function to handle the installation - try { - $displayName = $packageObj.name - $installParams = @{ - PackageName = $packageObj.name - } - - $versionDisplay = "" - if ($packageObj.version) { - $versionDisplay = "version: $($packageObj.version)" - $installParams.Version = $packageObj.version - } - - $bucketDisplay = "" - if ($packageObj.bucket) { - $bucketDisplay = "bucket: '$($packageObj.bucket)'" - $installParams.Bucket = $packageObj.bucket - } - - $globalDisplay = "" - if ($packageObj.global -eq $true) { - $globalDisplay = "global: true" - $installParams.Global = $true - } else { - $installParams.Global = $false - } - - if($versionDisplay -or $bucketDisplay -or $globalDisplay) { - $parts = @($versionDisplay, $bucketDisplay, $globalDisplay) | Where-Object { $_ } - $displayName += " (" + ($parts -join ", ") + ")" - } - Write-StatusMessage "- Installing Scoop package: $displayName" -ForegroundColor Gray -Indent 2 -Width 112 -NoNewLine - - ($result = Install-ScoopPackage @installParams) | Out-Null - - if (-not $result) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } else { - Write-StatusMessage "[OK]" -ForegroundColor Green - } - } catch { - Write-StatusMessage "Failed to install Scoop package '$($packageObj.name)': $_" -Verbosity "Error" - continue - } - } - - Write-StatusMessage "- Scoop packages installation completed! Processed $packageCount packages." -ForegroundColor Green - - Write-Host "" - - return $true - } - catch { - Write-StatusMessage "Error installing Scoop packages: $_" -Verbosity "Error" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 deleted file mode 100644 index 7c3f0df..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopPackage.Tests.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Install-ScoopPackage.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 - . $PSScriptRoot\Uninstall-ScoopPackage.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\Read-ScoopCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 -} - -Describe "Install-ScoopPackage" { - - Context "When Scoop is not installed" { - It "Should return false" { - Mock Test-ScoopInstalled { return $false } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When Scoop command cannot be found" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return $null } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When package is already installed with correct version and scope" { - It "Should return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $true - } - } - - Context "When package is installed but version/scope does not match" { - It "Should uninstall and reinstall the package" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - $callCount = 0 - Mock Test-ScoopComponentInstalled -MockWith { - $callCount++ - if ($callCount -eq 1) { [InstalledState]::Installed } - else { [InstalledState]::Pass } - } - Mock Read-ScoopCache { - return @{ - apps = @( - [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } - ) - } - } - Mock Uninstall-ScoopPackage { return $true } - Mock Invoke-Command { $global:LASTEXITCODE = 0 } - Mock Write-ScoopCache { return $true } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $true - } - } - - Context "When install command fails" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Uninstall-ScoopPackage { return $true } - Mock Invoke-Command { $global:LASTEXITCODE = 1 } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When Write-ScoopCache fails after install" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Uninstall-ScoopPackage { return $true } - Mock Invoke-Command { $global:LASTEXITCODE = 0 } - Mock Write-ScoopCache { return $false } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When installing with version, bucket, and global" { - It "Should pass correct arguments and return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - $callCount = 0 - Mock Test-ScoopComponentInstalled -MockWith { - $callCount++ - if ($callCount -eq 1) { [InstalledState]::Installed } - else { [InstalledState]::Pass } - } - Mock Uninstall-ScoopPackage { return $true } - Mock Invoke-Command { - param($ScriptBlock) - $global:LASTEXITCODE = 0 - # Optionally, check the arguments passed to scoop - return $null - } - Mock Write-ScoopCache { return $true } - $result = Install-ScoopPackage -PackageName "python" -Version "3.11.5" -Bucket "main" -Global - $result | Should -Be $true - } - } - - Context "When an exception occurs" { - It "Should return false" { - Mock Test-ScoopInstalled { throw "Unexpected error" } - $result = Install-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 b/devsetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 deleted file mode 100644 index 9d8a1c5..0000000 --- a/devsetup/Private/Providers/Scoop/Install-ScoopPackage.ps1 +++ /dev/null @@ -1,163 +0,0 @@ -<# -.SYNOPSIS - Installs a Scoop package on the system. - -.DESCRIPTION - This function installs a specified Scoop package by executing the 'scoop install' command. - It includes validation to ensure Scoop is installed and available before attempting the installation. - The function supports package versioning, bucket specification, and global installation scope. - If the package is already installed and the global scope matches, it will be uninstalled first to ensure a clean installation. - The function verifies successful installation using Test-ScoopComponentInstalled with version and scope validation. - -.PARAMETER PackageName - The name of the Scoop package to install. - This parameter is mandatory and must be a valid string representing a Scoop package. - -.PARAMETER Version - The specific version of the package to install. - Optional parameter that appends version specification to the package name (e.g., "package@1.2.3"). - -.PARAMETER Bucket - The bucket name where the package is located. - Optional parameter that prepends bucket specification to the package name (e.g., "extras/package"). - -.PARAMETER Global - Switch parameter to install the package globally. - When specified, adds the --global flag to the scoop install command. - -.OUTPUTS - [System.Boolean] - Returns $true if the package was successfully installed and verified, $false if the installation failed. - -.EXAMPLE - Install-ScoopPackage -PackageName "git" - - Installs the 'git' package from the main bucket. - -.EXAMPLE - Install-ScoopPackage -PackageName "nodejs" -Version "18.17.0" - - Installs a specific version of the 'nodejs' package. - -.EXAMPLE - Install-ScoopPackage -PackageName "7zip" -Global - - Installs the '7zip' package globally for all users. - -.EXAMPLE - Install-ScoopPackage -PackageName "firefox" -Bucket "extras" - - Installs the 'firefox' package from the 'extras' bucket. - -.EXAMPLE - Install-ScoopPackage -PackageName "python" -Version "3.11.5" -Bucket "main" -Global - - Installs a specific version of Python from the main bucket globally. - -.NOTES - - Requires Scoop to be installed on the system - - Only uninstalls existing package if it's already installed AND global scope matches exactly - - Uses Test-ScoopComponentInstalled to verify installation success with version and scope validation - - Supports bucket/package@version syntax for package specification - - Returns $false immediately if Scoop is not installed or cannot be found - - Provides detailed warning and error messages for failure scenarios - - Uses proper argument splatting for reliable command execution - - Includes comprehensive try-catch error handling for robust failure management - - Installation verification checks name, version (if specified), and global scope (if specified) - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Management, Package Installation -#> -Function Install-ScoopPackage { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [string]$PackageName, - - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Version, - - [Parameter(Mandatory=$false)] - [ValidateNotNullOrEmpty()] - [string]$Bucket, - - [Parameter(Mandatory=$false)] - [switch]$Global - ) - - try { - if(-Not (Test-ScoopInstalled)) { - return $false - } - - $scoopCommand = Find-Scoop - if (-not $scoopCommand) { - return $false - } - - $Params = @{ - Package = $true - Name = $PackageName - } - - if($PSBoundParameters.ContainsKey('Version') -and $Version) { - $Params.Version = $Version - } - - if($Global) { - $Params.Global = $Global - } - - [InstalledState]$packageState = Test-ScoopComponentInstalled @Params - if ($packageState -eq [InstalledState]::Pass) { - Write-Debug "Scoop package '$PackageName' is already installed with the specified version and global scope." - return $true - } - - if($packageState.HasFlag([InstalledState]::Installed)) { - Write-Debug "Scoop package '$PackageName' is installed but does not meet the global scope and/or version requirements. Reinstalling..." - Uninstall-ScoopPackage -PackageName $PackageName | Out-null - } - - $fullPackageName = $PackageName - if ($PSBoundParameters.ContainsKey('Bucket')) { - $fullPackageName = "$Bucket/$PackageName" - } - - # Add version if specified - if ($PSBoundParameters.ContainsKey('Version')) { - $fullPackageName += "@$Version" - } - - # Build arguments array for installation - $installArgs = @("install", $fullPackageName) - - # Add global flag if specified - if ($Global) { - $installArgs += "--global" - } - - # Execute the install command with proper argument parsing - $command = { - & $scoopCommand @installArgs *> $null - } - - Invoke-Command -ScriptBlock $command | Out-Null - if ($LASTEXITCODE -ne 0) { - return $false - } - - if (-not (Write-ScoopCache)) { - return $false - } - return Test-ScoopComponentInstalled @Params - } catch { - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 b/devsetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 deleted file mode 100644 index aae6571..0000000 --- a/devsetup/Private/Providers/Scoop/Read-ScoopCache.Tests.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Read-ScoopCache.ps1 - . $PSScriptRoot\Get-ScoopCacheFile.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 -} - -Describe "Read-ScoopCache" { - - Context "When cache file exists and contains valid JSON" { - It "Should return deserialized object" { - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Test-Path { return $true } - $json = '{"apps":[{"name":"git","version":"2.42.0"}]}' - Mock Get-Content { $json } - $result = Read-ScoopCache - $result.apps[0].name | Should -Be "git" - $result.apps[0].version | Should -Be "2.42.0" - } - } - - Context "When cache file does not exist and Write-ScoopCache succeeds" { - It "Should create cache and return deserialized object" { - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - $testPathCallCount = 0 - Mock Test-Path -MockWith { - $testPathCallCount++ - if ($testPathCallCount -eq 1) { return $false } - else { return $true } - } - Mock Write-ScoopCache { return $true } - $json = '{"apps":[{"name":"git","version":"2.42.0"}]}' - Mock Get-Content { $json } - # Do NOT mock ConvertFrom-Json here! - $result = Read-ScoopCache - $result.apps[0].name | Should -Be "git" - } - } - - Context "When cache file does not exist and Write-ScoopCache fails" { - It "Should throw an exception" { - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Test-Path { return $false } - Mock Write-ScoopCache { return $false } - { Read-ScoopCache } | Should -Throw "Failed to create Scoop cache file: C:\fakepath\scoop.cache" - } - } - - Context "When cache file contains invalid JSON" { - It "Should return null and write error" { - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Test-Path { return $true } - Mock Get-Content { return "not-json" } - Mock ConvertFrom-Json { throw "Invalid JSON" } - $result = Read-ScoopCache - $result | Should -Be $null - } - } - - Context "When Get-Content throws an exception" { - It "Should return null and write error" { - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Test-Path { return $true } - Mock Get-Content { throw "File read error" } - $result = Read-ScoopCache - $result | Should -Be $null - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Read-ScoopCache.ps1 b/devsetup/Private/Providers/Scoop/Read-ScoopCache.ps1 deleted file mode 100644 index 5b619a9..0000000 --- a/devsetup/Private/Providers/Scoop/Read-ScoopCache.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -<# -.SYNOPSIS - Reads cached Scoop package information from the DevSetup cache file. - -.DESCRIPTION - This function reads and deserializes cached Scoop package data from the DevSetup cache system. - It automatically handles cache file creation if the file doesn't exist by calling Write-ScoopCache, - and provides comprehensive error handling for file operations and JSON parsing. The function - returns the cached data as a PowerShell object for use by other Scoop-related functions. - -.OUTPUTS - [System.Object] - Returns the deserialized cache data as a PowerShell object if successful. - Returns $null if the cache file cannot be read or parsed. - -.EXAMPLE - Read-ScoopCache - - Reads the Scoop cache data and returns it as a PowerShell object. - -.EXAMPLE - $scoopCache = Read-ScoopCache - if ($scoopCache) { - Write-Host "Found $($scoopCache.Count) cached packages" - } else { - Write-Host "No cache data available" - } - - Demonstrates reading cache data and checking for successful retrieval. - -.EXAMPLE - $cachedPackages = Read-ScoopCache - $gitPackage = $cachedPackages | Where-Object { $_.name -eq "git" } - - Shows reading cache data and filtering for specific package information. - -.NOTES - - Uses Get-ScoopCacheFile to determine the cache file location - - Automatically creates cache file if it doesn't exist using Write-ScoopCache - - Throws an exception if cache file creation fails - - Uses ConvertFrom-Json to deserialize the cached data - - Provides comprehensive error handling for both file operations and JSON parsing - - Returns $null on any error to allow calling functions to handle gracefully - - Used by other Scoop functions to avoid repeated system queries for performance - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Cache Management, Data Deserialization, Performance Optimization -#> - -Function Read-ScoopCache { - [CmdletBinding()] - Param() - - $CacheFilePath = Get-ScoopCacheFile - - if (-Not (Test-Path $CacheFilePath)) { - Write-Debug "Scoop cache file not found: $CacheFilePath" - if (-not (Write-ScoopCache)) { - throw "Failed to create Scoop cache file: $CacheFilePath" - } - } - - try { - $cacheData = Get-Content $CacheFilePath | ConvertFrom-Json - return $cacheData - } catch { - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 b/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 deleted file mode 100644 index bb7e421..0000000 --- a/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.Tests.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 - . $PSScriptRoot\Read-ScoopCache.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 -} - -Describe "Test-ScoopComponentInstalled" { - - Context "When Scoop is not installed" { - It "Should return NotInstalled for package" { - Mock Test-ScoopInstalled { return $false } - $result = Test-ScoopComponentInstalled -Package -Name "git" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - It "Should return NotInstalled for bucket" { - Mock Test-ScoopInstalled { return $false } - $result = Test-ScoopComponentInstalled -Bucket -Name "extras" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } - - Context "When cache cannot be read" { - It "Should return NotInstalled for package" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { return $null } - $result = Test-ScoopComponentInstalled -Package -Name "git" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - It "Should return NotInstalled for bucket" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { return $null } - $result = Test-ScoopComponentInstalled -Bucket -Name "extras" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } - - Context "When package is not found in cache" { - It "Should return NotInstalled" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { return @{ apps = @(); buckets = @() } } - $result = Test-ScoopComponentInstalled -Package -Name "git" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } - - Context "When bucket is not found in cache" { - It "Should return NotInstalled" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { return @{ apps = @(); buckets = @() } } - $result = Test-ScoopComponentInstalled -Bucket -Name "extras" - $result | Should -BeExactly ([InstalledState]::NotInstalled) - } - } - - Context "When package is found without version or global" { - It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { - return @{ - apps = @( - [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } - ) - } - } - $result = Test-ScoopComponentInstalled -Package -Name "git" - $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When package is found with matching version" { - It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { - return @{ - apps = @( - [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Local Install" } - ) - } - } - $result = Test-ScoopComponentInstalled -Package -Name "git" -Version "2.42.0" - $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When package is found but version does not match" { - It "Should return Installed + GlobalVersionMet" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { - return @{ - apps = @( - [PSCustomObject]@{ Name = "git"; Version = "2.41.0"; Info = "Local Install" } - ) - } - } - $result = Test-ScoopComponentInstalled -Package -Name "git" -Version "2.42.0" - $expected = [InstalledState]::Installed + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When package is found with Global switch and global install" { - It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { - return @{ - apps = @( - [PSCustomObject]@{ Name = "git"; Version = "2.42.0"; Info = "Global Install" } - ) - } - } - $result = Test-ScoopComponentInstalled -Package -Name "git" -Global - $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } - - Context "When bucket is found in cache" { - It "Should return Installed + RequiredVersionMet + MinimumVersionMet + GlobalVersionMet" { - Mock Test-ScoopInstalled { return $true } - Mock Read-ScoopCache { - return @{ - buckets = @( - [PSCustomObject]@{ Name = "extras" } - ) - } - } - $result = Test-ScoopComponentInstalled -Bucket -Name "extras" - $expected = [InstalledState]::Installed + [InstalledState]::RequiredVersionMet + [InstalledState]::MinimumVersionMet + [InstalledState]::GlobalVersionMet - $result | Should -BeExactly $expected - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 b/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 deleted file mode 100644 index 0e29fcc..0000000 --- a/devsetup/Private/Providers/Scoop/Test-ScoopComponentInstalled.ps1 +++ /dev/null @@ -1,155 +0,0 @@ -<# -.SYNOPSIS - Tests whether a Scoop package or bucket is installed on the system. - -.DESCRIPTION - Checks if a specified Scoop package or bucket is installed by querying Scoop export data. - For packages, verifies installation status, version match, and global/local scope. - For buckets, verifies if the bucket is present in the Scoop configuration. - -.PARAMETER Package - Indicates checking for a package installation. - Cannot be used with `-Bucket`. - -.PARAMETER Bucket - Indicates checking for a bucket installation. - Cannot be used with `-Package`. - -.PARAMETER Name - The name of the package or bucket to check. - Required for all parameter sets. - -.PARAMETER Version - The specific version to check for when validating package installation. - Optional for package checks; not applicable for bucket checks. - -.PARAMETER Global - Specifies checking for global package installation. - Optional for package checks; not applicable for bucket checks. - -.OUTPUTS - `[InstalledState]` - Returns an InstalledState enum value indicating installation status and version match. - Returns `[InstalledState]::NotInstalled` if not found or Scoop is unavailable. - -.EXAMPLE - Test-ScoopComponentInstalled -Package -Name "git" - # Checks if the 'git' package is installed via Scoop. - -.EXAMPLE - Test-ScoopComponentInstalled -Package -Name "nodejs" -Version "18.17.0" - # Checks if the 'nodejs' package version 18.17.0 is installed via Scoop. - -.EXAMPLE - Test-ScoopComponentInstalled -Package -Name "7zip" -Global - # Checks if the '7zip' package is installed globally via Scoop. - -.EXAMPLE - Test-ScoopComponentInstalled -Bucket -Name "extras" - # Checks if the 'extras' bucket is added to Scoop. - -.NOTES - **Requirements:** - - Scoop must be installed. - - Uses `Read-ScoopCache` for cached export data. - - **Behavior:** - - Returns `[InstalledState]::NotInstalled` if Scoop is not installed. - - For packages, checks name, version, and global install status. - - For buckets, checks if the bucket name exists in the configuration. - - Returns an InstalledState enum value for detailed status. - - **Error Handling:** - - Provides debug and warning messages for missing Scoop or cache data. - - Returns `[InstalledState]::NotInstalled` for missing components. - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Management, Installation Verification -#> -Function Test-ScoopComponentInstalled { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] - [switch]$Package, - - [Parameter(Mandatory=$true, ParameterSetName='BucketCheck')] - [switch]$Bucket, - - [Parameter(Mandatory=$true, ParameterSetName='PackageCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] - [Parameter(Mandatory=$true, ParameterSetName='BucketCheck')] - [ValidateNotNullOrEmpty()] - [string]$Name, - - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] - [ValidateNotNullOrEmpty()] - [string]$Version, - - [Parameter(Mandatory=$true, ParameterSetName='PackageVersionGlobalCheck')] - [Parameter(Mandatory=$true, ParameterSetName='PackageGlobalCheck')] - [switch]$Global - ) - - if(-Not (Test-ScoopInstalled)) { - return [InstalledState]::NotInstalled - } - - $scoopComponents = Read-ScoopCache - if (-not $scoopComponents) { - return [InstalledState]::NotInstalled - } - if ($Package) { - $packageName = $Name - [InstalledState]$packageState = [InstalledState]::NotInstalled - if ($scoopComponents.apps) { - $scoopComponents.apps | ForEach-Object { - if ($_.Name -eq $packageName) { - $packageState += [InstalledState]::Installed - if ($PSBoundParameters.ContainsKey('Version')) { - if([Version]$_.Version -eq [Version]$Version) { - $packageState += [InstalledState]::RequiredVersionMet - $packageState += [InstalledState]::MinimumVersionMet - } - } else { - $packageState += [InstalledState]::RequiredVersionMet - $packageState += [InstalledState]::MinimumVersionMet - } - - if($Global) { - if ($_.Info -eq "Global Install") { - $packageState += [InstalledState]::GlobalVersionMet - } - } else { - $packageState += [InstalledState]::GlobalVersionMet - } - } - } - } - return $packageState - } elseif ($Bucket) { - [InstalledState]$bucketState = [InstalledState]::NotInstalled - if ($scoopComponents.buckets) { - $scoopComponents.buckets | ForEach-Object { - if ($_.Name -eq $Name) { - Write-Debug "Scoop bucket '$Name' is installed." - $bucketState += [InstalledState]::Installed - $bucketState += [InstalledState]::MinimumVersionMet - $bucketState += [InstalledState]::RequiredVersionMet - $bucketState += [InstalledState]::GlobalVersionMet - } - } - } - return $bucketState - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 b/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 deleted file mode 100644 index 2cd4774..0000000 --- a/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.Tests.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-ScoopInstalled.ps1 -} - -Describe "Test-ScoopInstalled" { - - Context "When scoop command is available in PATH" { - It "Should return true" { - Mock Get-Command { return @{ Name = "scoop" } } - $result = Test-ScoopInstalled - $result | Should -Be $true - } - } - - Context "When scoop command is not available but scoop.ps1 exists" { - It "Should return true" { - Mock Get-Command { return $null } - Mock Test-Path { param($path) if ($path -like "*scoop.ps1") { return $true } else { return $false } } - $result = Test-ScoopInstalled - $result | Should -Be $true - } - } - - Context "When scoop command is not available but scoop.cmd exists" { - It "Should return true" { - Mock Get-Command { return $null } - Mock Test-Path { param($path) if ($path -like "*scoop.cmd") { return $true } else { return $false } } - $result = Test-ScoopInstalled - $result | Should -Be $true - } - } - - Context "When scoop command is not available but scoop executable exists" { - It "Should return true" { - Mock Get-Command { return $null } - Mock Test-Path { param($path) if ($path -like "*scoop") { return $true } else { return $false } } - $result = Test-ScoopInstalled - $result | Should -Be $true - } - } - - Context "When scoop is not installed at all" { - It "Should return false" { - Mock Get-Command { return $null } - Mock Test-Path { return $false } - $result = Test-ScoopInstalled - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 b/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 deleted file mode 100644 index 047594e..0000000 --- a/devsetup/Private/Providers/Scoop/Test-ScoopInstalled.ps1 +++ /dev/null @@ -1,82 +0,0 @@ -<# -.SYNOPSIS - Tests whether Scoop package manager is installed on the system. - -.DESCRIPTION - This function checks if Scoop is installed and available on the system by first attempting to locate - the scoop command in the PATH, and if not found, checking for Scoop installation files in the default - user profile directory. It provides a comprehensive check for both standard installations and cases - where Scoop may not be properly added to the PATH environment variable. - -.OUTPUTS - [System.Boolean] - Returns $true if Scoop is installed and available, $false otherwise. - -.EXAMPLE - Test-ScoopInstalled - - Checks if Scoop is installed on the current system. - -.EXAMPLE - if (Test-ScoopInstalled) { - Write-Host "Scoop is available" - # Proceed with Scoop operations - } else { - Write-Host "Scoop is not installed" - # Install Scoop or handle the missing dependency - } - - Demonstrates using the function result to conditionally execute Scoop-dependent code. - -.EXAMPLE - $scoopAvailable = Test-ScoopInstalled - switch ($scoopAvailable) { - $true { "Scoop package manager detected" } - $false { "Scoop package manager not found" } - } - - Shows capturing the boolean result for later use. - -.NOTES - - Performs multiple checks to ensure reliable detection - - First checks if 'scoop' command is available in PATH using Get-Command - - Falls back to checking specific file paths in the user profile directory: - * ~\scoop\shims\scoop.ps1 (PowerShell script) - * ~\scoop\shims\scoop.cmd (Command batch file) - * ~\scoop\shims\scoop (Executable) - - Does not verify that Scoop is functional, only that installation files exist - - Suppresses errors when checking for the scoop command to avoid console output - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Installation Detection, Environment Validation -#> -Function Test-ScoopInstalled { - [CmdletBinding()] - Param () - - if (Get-Command scoop -ErrorAction SilentlyContinue) { - return $true - } else { - # Check for Scoop in user profile directory - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.ps1" - if (Test-Path $scoopPath) { - return $true - } - - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop.cmd" - if (Test-Path $scoopPath) { - return $true - } - - $scoopPath = Join-Path $env:USERPROFILE "scoop\shims\scoop" - if (Test-Path $scoopPath) { - return $true - } - } - return $false -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 deleted file mode 100644 index bb9d66f..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.Tests.ps1 +++ /dev/null @@ -1,84 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-ScoopBucket.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 -} - -Describe "Uninstall-ScoopBucket" { - - Context "When Scoop is not installed" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $false } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - - Context "When Scoop command cannot be found" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return $null } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - - Context "When bucket is already uninstalled" { - It "Should return true and debug" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::Pass } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $true - } - } - - Context "When bucket uninstall command fails" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Write-ScoopCache { return $true } - Mock Invoke-Expression { $global:LASTEXITCODE = 1 } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - - Context "When Write-ScoopCache fails after uninstall" { - It "Should return false and error" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Write-ScoopCache { return $false } - Mock Invoke-Expression { $global:LASTEXITCODE = 0 } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } - - Context "When bucket is successfully uninstalled" { - It "Should return true and debug" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { return [InstalledState]::NotInstalled } - Mock Write-ScoopCache { return $true } - Mock Invoke-Expression { $global:LASTEXITCODE = 0 } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $true - } - } - - Context "When an exception occurs during uninstall" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { throw "Unexpected error" } - $result = Uninstall-ScoopBucket -Name "extras" - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 deleted file mode 100644 index 14c2e15..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopBucket.ps1 +++ /dev/null @@ -1,106 +0,0 @@ -<# -.SYNOPSIS - Uninstalls a Scoop bucket from the system. - -.DESCRIPTION - This function removes a Scoop bucket using the 'scoop bucket rm' command. It validates - Scoop installation, locates the Scoop command, and checks if the bucket is currently - installed before attempting removal. The function provides comprehensive error handling - and updates the Scoop cache after successful removal operations. - -.PARAMETER Name - The name of the Scoop bucket to uninstall. - This parameter is mandatory and must be a valid, non-empty string representing an installed Scoop bucket name. - -.OUTPUTS - [System.Boolean] - Returns $true if the bucket is successfully uninstalled or already uninstalled. - Returns $false if the uninstallation fails or Scoop is not available. - -.EXAMPLE - Uninstall-ScoopBucket -Name "extras" - - Uninstalls the "extras" bucket from Scoop. - -.EXAMPLE - $result = Uninstall-ScoopBucket -Name "java" - if ($result) { - Write-Host "Java bucket removed successfully" - } else { - Write-Host "Failed to remove Java bucket" - } - - Demonstrates capturing the return value to check uninstallation success. - -.EXAMPLE - @("extras", "versions", "java") | ForEach-Object { - Uninstall-ScoopBucket -Name $_ - } - - Shows bulk uninstallation of multiple Scoop buckets. - -.NOTES - - Requires Scoop to be installed on the system - - Uses Test-ScoopInstalled to validate Scoop availability - - Uses Find-Scoop to locate the Scoop command executable - - Returns $false immediately if Scoop is not available or cannot be found - - Uses Test-ScoopComponentInstalled to check if bucket is currently installed - - Returns $true if bucket is already uninstalled (idempotent behavior) - - Executes 'scoop bucket rm' command with output suppression - - Uses $LASTEXITCODE to verify command execution success - - Updates Scoop cache using Write-ScoopCache after successful removal - - Provides debug logging for successful and skipped operations - - Includes comprehensive try-catch error handling with descriptive error messages - - Suppresses all command output using *> $null to avoid console clutter - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Management, Bucket Management, Repository Removal -#> - -Function Uninstall-ScoopBucket { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [string]$Name - ) - - if(-Not (Test-ScoopInstalled)) { - return $false - } - - $scoopCommand = Find-Scoop - if (-not $scoopCommand) { - return $false - } - - try { - $bucketState = Test-ScoopComponentInstalled -Bucket -Name $Name - if (-not ($bucketState.HasFlag([InstalledState]::Pass))) { - # If a source is provided, add it to the command arguments - Write-Debug "Removing Scoop bucket: $Name without source" - - # Execute the command to add the bucket - Invoke-Expression "& $scoopCommand bucket rm $Name" *> $null - if ($LASTEXITCODE -ne 0) { - return $false - } - - if (-not (Write-ScoopCache)) { - return $false - } - - Write-Debug "Scoop bucket '$Name' removed successfully." - return $true - } else { - Write-Debug "Scoop bucket '$Name' is already uninstalled." - return $true - } - } catch { - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 deleted file mode 100644 index f5e5d05..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.Tests.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-ScoopComponents.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\Uninstall-ScoopBucket.ps1 - . $PSScriptRoot\Uninstall-ScoopPackage.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Utils\Write-StatusMessage.ps1 - Mock Write-StatusMessage { } - Mock Write-Host {} - Mock Write-Error {} -} - -Describe "Uninstall-ScoopComponents" { - - Context "When Scoop is not installed" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $false } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras") - packages = @("git") } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When Scoop configuration is missing" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When Write-ScoopCache fails" { - It "Should return false and error" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $false } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ - buckets = @("extras") - packages = @("git") } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } - - Context "When only buckets are present and all uninstall succeed" { - It "Should return true and process all buckets" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Uninstall-ScoopBucket { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ buckets = @("extras", "versions") } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When only packages are present and all uninstall succeed" { - It "Should return true and process all packages" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Uninstall-ScoopPackage { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When buckets and packages are present and some uninstalls fail" { - It "Should return true and report failures" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - $bucketCallCount = 0 - Mock Uninstall-ScoopBucket -MockWith { - $bucketCallCount++ - if ($bucketCallCount -eq 1) { return $false } else { return $true } - } - $packageCallCount = 0 - Mock Uninstall-ScoopPackage -MockWith { - $packageCallCount++ - if ($packageCallCount -eq 2) { return $false } else { return $true } - } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - scoop = @{ - buckets = @("extras", "versions") - packages = @("git", "nodejs") - } - } - } - } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When no buckets or packages are present" { - It "Should return true and skip package uninstallation" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When an exception occurs during package uninstall" { - It "Should catch and continue, returning true" { - Mock Test-ScoopInstalled { return $true } - Mock Write-ScoopCache { return $true } - Mock Uninstall-ScoopBucket { return $true } - Mock Uninstall-ScoopPackage { throw "Unexpected error" } - $yamlData = @{ devsetup = @{ dependencies = @{ scoop = @{ packages = @("git", "nodejs") } } } } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $true - } - } - - Context "When an exception occurs in the main try block" { - It "Should return false" { - Mock Test-ScoopInstalled { throw "Critical error" } - $yamlData = @{ - devsetup = @{ - dependencies = @{ - scoop = @{ - buckets = @("extras") - packages = @("git") - } - } - } - } - $result = Uninstall-ScoopComponents -YamlData $yamlData - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 deleted file mode 100644 index d2b2f6e..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopComponents.ps1 +++ /dev/null @@ -1,225 +0,0 @@ -<# -.SYNOPSIS - Uninstalls multiple Scoop components (buckets and packages) from the system based on YAML configuration. - -.DESCRIPTION - This function removes multiple Scoop components specified in a DevSetup YAML configuration. - It validates Scoop installation, parses the configuration for bucket and package definitions, - and systematically uninstalls components in the correct order (buckets first, then packages). - The function supports both simple string format and complex object format for component - specifications, handles global installations, and provides comprehensive progress reporting - during the uninstallation process. - -.PARAMETER YamlData - The parsed YAML configuration data containing Scoop component definitions. - This parameter is mandatory and must be a PSCustomObject with the structure: - devsetup.dependencies.scoop containing buckets and/or packages arrays. - -.OUTPUTS - [System.Boolean] - Returns $true if all components are successfully processed (even if some individual uninstalls fail). - Returns $false if the operation encounters critical errors, Scoop is not installed, or cannot proceed. - -.EXAMPLE - $config = Read-ConfigurationFile -Path "environment.yaml" - Uninstall-ScoopComponents -YamlData $config - - Uninstalls all Scoop buckets and packages defined in the environment.yaml configuration. - -.EXAMPLE - $yamlData = @{ - devsetup = @{ - dependencies = @{ - scoop = @{ - buckets = @("extras", "versions") - packages = @("git", "nodejs", "python") - } - } - } - } - Uninstall-ScoopComponents -YamlData $yamlData - - Demonstrates uninstalling components using a programmatically created configuration. - -.EXAMPLE - if (Uninstall-ScoopComponents -YamlData $config) { - Write-Host "All Scoop components processed successfully" - } else { - Write-Host "Scoop component uninstallation encountered errors" - } - - Shows checking the return value to verify uninstallation completion. - -.NOTES - - Requires Scoop to be installed on the system - - Uses Test-ScoopInstalled to validate Scoop availability before proceeding - - Updates Scoop cache using Write-ScoopCache before uninstallation begins - - Processes components in specific order: buckets first, then packages - - Skips uninstallation gracefully if Scoop configuration sections are not found - - Supports two component specification formats for both buckets and packages: - * Simple string: "componentname" - * Complex object: @{ name = "componentname"; version = "1.0.0"; bucket = "extras"; global = $true } - - Bucket objects support: name and source properties - - Package objects support: name, version, bucket, and global properties - - Validates component names and skips entries with missing names - - Uses Uninstall-ScoopBucket and Uninstall-ScoopPackage for individual component removal - - Provides detailed progress reporting with component counts and property information - - Uses color-coded console output: Cyan for progress, Gray for component status, Green/Red for results - - Continues processing remaining components even if individual uninstalls fail - - Returns $true for overall success even with individual component failures - - Includes comprehensive try-catch error handling with descriptive error messages - - Displays formatted component information including version, bucket, and global flags - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Management, Batch Uninstallation, Configuration Processing, Component Management -#> - -Function Uninstall-ScoopComponents { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [PSCustomObject]$YamlData - ) - - try { - if(-Not (Test-ScoopInstalled)) { - Write-StatusMessage "Scoop is not installed. Cannot check for components." -Verbosity "Warning" - return $false - } - - # Check if scoop packages exist in configuration - if (-not $YamlData -or -not $YamlData.devsetup -or -not $YamlData.devsetup.dependencies -or -not $YamlData.devsetup.dependencies.scoop) { - Write-StatusMessage "Scoop configuration not found in YAML. Skipping uninstallation." -Verbosity "Warning" - return $false - } - - if (-not (Write-ScoopCache)) { - Write-Error "Failed to write Scoop cache file: $CacheFilePath" - return $false - } - - $bucketCount = 0 - # Handle buckets first if they exist in configuration - if ($YamlData.devsetup.dependencies.scoop.buckets) { - Write-StatusMessage "- Uninstalling Scoop buckets from configuration:" -ForegroundColor Cyan - foreach ($bucket in $YamlData.devsetup.dependencies.scoop.buckets) { - if (-not $bucket) { continue } - - # Handle both string format and object format - $bucketName = if ($bucket -is [string]) { $bucket } else { $bucket.name } - $bucketSource = if ($bucket -is [hashtable] -and $bucket.source) { $bucket.source } else { $null } - - $installParams = @{ - Name = $bucketName - } - - # Use Install-ScoopBucket function to handle bucket installation - if ($bucketName -and $bucketSource) { - Write-StatusMessage "- Removing Scoop bucket: $bucketName (source: $bucketSource)" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine - } else { - Write-StatusMessage "- Removing Scoop bucket: $bucketName" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine - } - - $installationStatus = Uninstall-ScoopBucket @installParams - - if (-not $installationStatus) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } else { - $bucketCount++ - Write-StatusMessage "[OK]" -ForegroundColor Green - } - } - } - - Write-StatusMessage "- Scoop buckets uninstallation completed! Processed $bucketCount buckets." -ForegroundColor Green - - Write-Host "" - - # Check if scoop packages exist in configuration - if (-not $YamlData.devsetup.dependencies.scoop.packages) { - Write-StatusMessage "Scoop packages not found in YAML configuration. Skipping package uninstallation." -Verbosity "Warning" - return $true - } - - $scoopPackages = $YamlData.devsetup.dependencies.scoop.packages - Write-StatusMessage "- Uninstalling Scoop packages from configuration:" -ForegroundColor Cyan - - $packageCount = 0 - - # Install packages - foreach ($package in $scoopPackages) { - if (-not $package) { continue } - - $packageCount++ - - # Normalize package to object format - if ($package -is [string]) { - $packageObj = @{ name = $package } - } else { - $packageObj = $package - } - - # Validate package name - if ([string]::IsNullOrEmpty($packageObj.name)) { - Write-StatusMessage "- Skipping package entry, No name specified" -Verbosity "Warning" -Indent 2 -Width 100 - continue - } - - # Use Install-ScoopPackage function to handle the installation - try { - $displayName = $packageObj.name - $installParams = @{ - PackageName = $packageObj.name - } - - $versionDisplay = "" - if ($packageObj.version) { - $versionDisplay = "version: $($packageObj.version)" - } - - $bucketDisplay = "" - if ($packageObj.bucket) { - $bucketDisplay = "bucket: '$($packageObj.bucket)'" - } - - $globalDisplay = "" - if ($packageObj.global -eq $true) { - $globalDisplay = "global: true" - $installParams.Global = $true - } - - if($versionDisplay -or $bucketDisplay -or $globalDisplay) { - $parts = @($versionDisplay, $bucketDisplay, $globalDisplay) | Where-Object { $_ } - $displayName += " (" + ($parts -join ", ") + ")" - } - Write-StatusMessage "- Uninstalling Scoop package: $displayName" -ForegroundColor Gray -Indent 2 -Width 100 -NoNewLine - - $result = Uninstall-ScoopPackage @installParams - - if (-not $result) { - Write-StatusMessage "[FAILED]" -ForegroundColor Red - } else { - Write-StatusMessage "[OK]" -ForegroundColor Green - } - } catch { - Write-StatusMessage "Failed to uninstall Scoop package '$($packageObj.name)': $_" -Verbosity "Error" - continue - } - } - - Write-StatusMessage "- Scoop packages uninstallation completed! Processed $packageCount packages." -ForegroundColor Green - - Write-Host "" - - return $true - } - catch { - Write-StatusMessage "Error uninstalling Scoop packages: $_" -Verbosity "Error" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 deleted file mode 100644 index 1c7cc21..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.Tests.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Uninstall-ScoopPackage.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\Test-ScoopComponentInstalled.ps1 - . $PSScriptRoot\..\..\..\..\DevSetup\Private\Enums\InstalledState.ps1 - -} - -Describe "Uninstall-ScoopPackage" { - - Context "When Scoop is not installed" { - It "Should return false" { - Mock Test-ScoopInstalled { return $false } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When Scoop command cannot be found" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return $null } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When package is not installed" { - It "Should return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { - return [InstalledState]::NotInstalled - } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $true - } - } - - Context "When uninstall succeeds" { - It "Should return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { - return [InstalledState]::Pass - } - Mock Invoke-Command { $global:LASTEXITCODE = 0 } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $true - } - } - - Context "When uninstall fails" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { - return [InstalledState]::Pass - } - Mock Invoke-Command { $global:LASTEXITCODE = 1 } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When uninstall throws an exception" { - It "Should return false" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { - return [InstalledState]::Pass - } - Mock Invoke-Command { throw "Unexpected error" } - $result = Uninstall-ScoopPackage -PackageName "git" - $result | Should -Be $false - } - } - - Context "When uninstalling a global package" { - It "Should pass --global and return true" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Test-ScoopComponentInstalled { - return [InstalledState]::Pass - } - Mock Invoke-Command { - param($ScriptBlock) - $global:LASTEXITCODE = 0 - # Optionally, you could inspect $ScriptBlock here - return $null - } - $result = Uninstall-ScoopPackage -PackageName "git" -Global - $result | Should -Be $true - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 b/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 deleted file mode 100644 index b411ba6..0000000 --- a/devsetup/Private/Providers/Scoop/Uninstall-ScoopPackage.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -<# -.SYNOPSIS - Uninstalls a Scoop package from the system. - -.DESCRIPTION - This function removes a specified Scoop package from the system by executing the 'scoop uninstall' command. - It includes validation to ensure Scoop is installed and available before attempting the uninstall operation. - The function checks if the package is installed before attempting removal and provides error handling with - a boolean result indicating success or failure. - -.PARAMETER PackageName - The name of the Scoop package to uninstall. - This parameter is mandatory and must be a valid string representing an installed Scoop package. - -.OUTPUTS - [System.Boolean] - Returns $true if the package was successfully uninstalled or if the package was not installed, - $false if the uninstall operation failed. - -.EXAMPLE - Uninstall-ScoopPackage -PackageName "git" - - Uninstalls the 'git' package from Scoop. - -.EXAMPLE - Uninstall-ScoopPackage -PackageName "nodejs" - - Removes the 'nodejs' package from the system via Scoop. - -.EXAMPLE - $result = Uninstall-ScoopPackage -PackageName "7zip" - if ($result) { - Write-Host "7zip successfully removed or was not installed" - } else { - Write-Host "Failed to remove 7zip" - } - - Demonstrates capturing the return value to check uninstall success. - -.NOTES - - Requires Scoop to be installed on the system - - Uses Test-ScoopPackageInstalled function to verify package existence before uninstall - - Returns $true if package is not installed (considered successful since goal is achieved) - - Returns $false immediately if Scoop is not installed or cannot be found - - Provides warning messages for common failure scenarios - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Package Management, Package Removal -#> -Function Uninstall-ScoopPackage { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true)] - [string]$PackageName, - [switch]$Global - ) - - if(-Not (Test-ScoopInstalled)) { - Write-Debug "Scoop is not installed. Cannot check for components." - return $false - } - - $scoopCommand = Find-Scoop - if (-not $scoopCommand) { - Write-Debug "Failed to find Scoop command. Cannot check for components." - return $false - } - - $packageState = Test-ScoopComponentInstalled -Package -Name $PackageName - if (-not ($packageState.HasFlag([InstalledState]::Pass))) { - Write-Debug "Package not installed, can not remove." - return $true - } - - try { - $uninstallArgs = @('uninstall', $PackageName) - if($Global) { - $uninstallArgs += '--global' - } - - $command = { - & $scoopCommand @uninstallArgs *> $null - } - - Invoke-Command -ScriptBlock $command | Out-Null - if ($LASTEXITCODE -eq 0) { - Write-Debug "Uninstalled Scoop package: $PackageName" - return $true - } else { - Write-Debug "Failed to uninstall Scoop package: $PackageName" - return $false - } - } catch { - Write-Debug "Failed to remove Scoop Package: $PackageName" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 b/devsetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 deleted file mode 100644 index 992761e..0000000 --- a/devsetup/Private/Providers/Scoop/Write-ScoopCache.Tests.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Write-ScoopCache.ps1 - . $PSScriptRoot\Test-ScoopInstalled.ps1 - . $PSScriptRoot\Find-Scoop.ps1 - . $PSScriptRoot\Get-ScoopCacheFile.ps1 -} - -Describe "Write-ScoopCache" { - - Context "When Scoop is not installed" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $false } - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - $result = Write-ScoopCache - $result | Should -Be $false - } - } - - Context "When Scoop command cannot be found" { - It "Should return false and warn" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return $null } - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - $result = Write-ScoopCache - $result | Should -Be $false - } - } - - Context "When cache file is written successfully" { - It "Should return true and debug" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Invoke-Expression { "exported data" } - Mock Set-Content { param($Path, $Value, $Force) return $null } - $result = Write-ScoopCache - $result | Should -Be $true - } - } - - Context "When writing cache file fails" { - It "Should return false and error" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Invoke-Expression { "exported data" } - Mock Set-Content { throw "Failed to write file" } - $result = Write-ScoopCache - $result | Should -Be $false - } - } - - Context "When scoop export throws an exception" { - It "Should return false and error" { - Mock Test-ScoopInstalled { return $true } - Mock Find-Scoop { return "scoop" } - Mock Get-ScoopCacheFile { return "C:\fakepath\scoop.cache" } - Mock Invoke-Expression { throw "export failed" } - $result = Write-ScoopCache - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Providers/Scoop/Write-ScoopCache.ps1 b/devsetup/Private/Providers/Scoop/Write-ScoopCache.ps1 deleted file mode 100644 index c1d978e..0000000 --- a/devsetup/Private/Providers/Scoop/Write-ScoopCache.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -<# -.SYNOPSIS - Writes current Scoop package information to the DevSetup cache file. - -.DESCRIPTION - This function exports the current Scoop package installation data and writes it to the DevSetup - cache file for performance optimization and offline reference. It validates Scoop installation, - locates the Scoop command, and uses 'scoop export' to generate package data before saving it - to the cache file. The function provides comprehensive error handling and validation throughout - the process. - -.OUTPUTS - [System.Boolean] - Returns $true if the cache file is successfully written. - Returns $false if Scoop is not installed, cannot be found, or the write operation fails. - -.EXAMPLE - Write-ScoopCache - - Exports current Scoop packages and writes them to the cache file. - -.EXAMPLE - if (Write-ScoopCache) { - Write-Host "Scoop cache updated successfully" - } else { - Write-Host "Failed to update Scoop cache" - } - - Demonstrates checking the return value to verify cache update success. - -.EXAMPLE - $cacheUpdated = Write-ScoopCache - if ($cacheUpdated) { - $cachedData = Read-ScoopCache - } - - Shows writing cache data and then reading it back for use. - -.NOTES - - Requires Scoop to be installed on the system - - Uses Test-ScoopInstalled to validate Scoop availability - - Uses Find-Scoop to locate the Scoop command executable - - Executes 'scoop export' to generate current package data - - Uses Get-ScoopCacheFile to determine the cache file location - - Overwrites existing cache file using -Force flag - - Provides debug logging for successful cache operations - - Returns $false immediately if Scoop is not available - - Includes comprehensive try-catch error handling for file operations - -.LINK - -.COMPONENT - DevSetup.Providers.Scoop - -.FUNCTIONALITY - Cache Management, Data Serialization, Performance Optimization -#> - -Function Write-ScoopCache { - [CmdletBinding()] - Param() - - $CacheFilePath = Get-ScoopCacheFile - if(-Not (Test-ScoopInstalled)) { - return $false - } - - $scoopCommand = Find-Scoop - if (-not $scoopCommand) { - return $false - } - - try { - Invoke-Expression "& $scoopCommand export" | Set-Content -Path $CacheFilePath -Force | Out-Null - Write-Debug "Scoop cache written successfully: $CacheFilePath" - return $true - } catch { - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 b/devsetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 deleted file mode 100644 index 5554228..0000000 --- a/devsetup/Private/Utils/ConvertFrom-Base64.Tests.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -BeforeAll { - . $PSScriptRoot\ConvertFrom-Base64.ps1 - Mock Write-Error { } -} - -Describe "ConvertFrom-Base64" { - - Context "When EncodedString is empty" { - It "Should write error and return false" { - $result = ConvertFrom-Base64 -EncodedString "" - $result | Should -Be $false - } - } - - Context "When EncodedString is valid and OutputFile is not provided" { - It "Should decode and return the string" { - $plainText = "Hello, world!" - $base64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainText)) - $result = ConvertFrom-Base64 -EncodedString $base64 - $result | Should -Be $plainText - } - } - - Context "When EncodedString is valid and OutputFile is provided" { - It "Should decode and write to file, returning true" { - $plainText = "Test file output" - $base64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($plainText)) - $testFile = "$PSScriptRoot\test_output.txt" - if (Test-Path $testFile) { Remove-Item $testFile } - $result = ConvertFrom-Base64 -EncodedString $base64 -OutputFile $testFile - $result | Should -Be $true - (Get-Content $testFile -Raw) | Should -Be $plainText - Remove-Item $testFile - } - } - - Context "When EncodedString is invalid base64" { - It "Should write error and return false" { - $result = ConvertFrom-Base64 -EncodedString "not_base64!" - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/ConvertFrom-Base64.ps1 b/devsetup/Private/Utils/ConvertFrom-Base64.ps1 deleted file mode 100644 index a06d4d3..0000000 --- a/devsetup/Private/Utils/ConvertFrom-Base64.ps1 +++ /dev/null @@ -1,29 +0,0 @@ -Function ConvertFrom-Base64 { - param ( - [string]$EncodedString, - [string]$OutputFile - ) - - if (-not $EncodedString) { - Write-Error "Base64 string is empty." - return $false - } - - try { - # Decode the base64 string - $decodedBytes = [System.Convert]::FromBase64String($EncodedString) - - if ($OutputFile) { - # Write to file if OutputFile is provided - [System.IO.File]::WriteAllBytes($OutputFile, $decodedBytes) - return $true - } else { - # Return the decoded string if no OutputFile is provided - $decodedString = [System.Text.Encoding]::UTF8.GetString($decodedBytes) - return $decodedString - } - } catch { - Write-Error "Failed to convert Base64: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/ConvertTo-Base64.Tests.ps1 b/devsetup/Private/Utils/ConvertTo-Base64.Tests.ps1 deleted file mode 100644 index 0f457b2..0000000 --- a/devsetup/Private/Utils/ConvertTo-Base64.Tests.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -BeforeAll { - . $PSScriptRoot\ConvertTo-Base64.ps1 - Mock Write-Error { } -} - -Describe "ConvertTo-Base64" { - - Context "When converting a string to Base64" { - It "Should return the correct Base64 string" { - $inputString = "Hello, world!" - $stringBytes = [System.Text.Encoding]::UTF8.GetBytes($inputString) - $expected = [System.Convert]::ToBase64String($stringBytes) - $result = ConvertTo-Base64 -InputString $inputString - $result | Should -Be $expected - } - } - - Context "When converting a file to Base64" { - It "Should return the correct Base64 string" { - $inputString = "File content" - $testFile = "${TestDrive}\test_input.txt" - Set-Content -Path $testFile -Value $inputString - $stringBytes = [System.IO.File]::ReadAllBytes($testFile) - $expected = [System.Convert]::ToBase64String($stringBytes) - $result = ConvertTo-Base64 -FilePath $testFile - $result | Should -Be $expected - Remove-Item $testFile - } - } - - Context "When file does not exist" { - It "Should write error and return null" { - $result = ConvertTo-Base64 -FilePath "nonexistent.txt" - $result | Should -Be $null - } - } - - Context "When an exception occurs" { - It "Should write error and return null" { - Mock Test-Path { throw "Unexpected error" } - $result = ConvertTo-Base64 -FilePath "anyfile.txt" - $result | Should -Be $null - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/ConvertTo-Base64.ps1 b/devsetup/Private/Utils/ConvertTo-Base64.ps1 deleted file mode 100644 index 47d2a8a..0000000 --- a/devsetup/Private/Utils/ConvertTo-Base64.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -Function ConvertTo-Base64 { - param ( - [Parameter(ParameterSetName = "File", Mandatory = $true)] - [string]$FilePath, - - [Parameter(ParameterSetName = "String", Mandatory = $true)] - [string]$InputString - ) - - try { - if ($PSCmdlet.ParameterSetName -eq "String") { - # Convert string to Base64 - $stringBytes = [System.Text.Encoding]::UTF8.GetBytes($InputString) - $base64String = [System.Convert]::ToBase64String($stringBytes) - return $base64String - } - else { - # Convert file to Base64 (existing functionality) - if (-not (Test-Path -Path $FilePath)) { - Write-Error "File not found: $FilePath" - return $null - } - - $fileBytes = [System.IO.File]::ReadAllBytes($FilePath) - $base64String = [System.Convert]::ToBase64String($fileBytes) - return $base64String - } - } catch { - Write-Error "Failed to convert to Base64: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Find-GitRepositories.ps1 b/devsetup/Private/Utils/Find-GitRepositories.ps1 deleted file mode 100644 index dd7aa66..0000000 --- a/devsetup/Private/Utils/Find-GitRepositories.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -Function Find-GitRepositories { - [CmdletBinding()] - Param( - [Parameter( - Position = 0, - HelpMessage = "The top level path to search" - )] - [ValidateScript({ - if (Test-Path $_) { - $True - } - else { - Throw "Cannot validate path $_" - } - })] - [string]$Path = "." - ) - - Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" - Write-Verbose "[PROCESS] Searching $(Convert-Path -path $path) for Git repositories" - - # Define directories to exclude from search (just the folder names) - $ExcludeFolders = @('Windows', 'Program Files', 'Program Files (x86)', '$RECYCLE.BIN') - - Write-Verbose "[PROCESS] Excluding system folders: $($ExcludeFolders -join ', ')" - - # Use a more efficient search strategy - function Search-GitRepos { - param([string]$SearchPath, [string[]]$ExcludeFolders) - - try { - # Get all directories first, excluding system folders at the top level - $directories = Get-ChildItem -Path $SearchPath -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -notin $ExcludeFolders } - - foreach ($dir in $directories) { - # Check if this directory IS a git repo - $gitDir = Join-Path $dir.FullName ".git" - if (Test-Path $gitDir) { - # Found a git repo, yield it - Get-Item $gitDir -Force -ErrorAction SilentlyContinue - } - - # Recursively search subdirectories (but don't exclude here since we're deeper) - Search-GitRepos -SearchPath $dir.FullName -ExcludeFolders @() - } - } - catch { - # Silently continue on errors - } - } - - # Collect all repositories in an array - $repositories = @() - - Search-GitRepos -SearchPath $Path -ExcludeFolders $ExcludeFolders | - ForEach-Object { - $gitItem = $_ - $repoPath = Split-Path $gitItem.FullName -Parent - Write-Verbose "Found repository at: $repoPath" - - # Get the branch information - $branchName = "unknown" - $remoteUrl = "none" - if ($repoPath -and (Test-Path $repoPath)) { - $originalLocation = Get-Location - try { - Write-Verbose "Changing to repository: $repoPath" - Set-Location -Path $repoPath - - # Get current branch - $branchOutput = & git rev-parse --abbrev-ref HEAD 2>$null - if ($LASTEXITCODE -eq 0 -and $branchOutput) { - $branchName = $branchOutput.Trim() - } - - # Get remote origin URL - $remoteOutput = & git remote get-url origin 2>$null - if ($LASTEXITCODE -eq 0 -and $remoteOutput) { - $remoteUrl = $remoteOutput.Trim() - } - } - catch { - Write-Verbose "Branch/Remote detection error for $repoPath`: $_" - $branchName = "error" - $remoteUrl = "error" - } - finally { - Set-Location -Path $originalLocation - } - } else { - Write-Verbose "Invalid repository path: '$repoPath'" - $branchName = "invalid-path" - $remoteUrl = "invalid-path" - } - - # Add to repositories collection - $repositories += [PSCustomObject]@{ - Repository = $repoPath - Branch = $branchName - RemoteUrl = $remoteUrl - } - } - - # Output formatted table - if ($repositories.Count -gt 0) { - Write-Host "`nFound $($repositories.Count) Git repositories:" -ForegroundColor Green - Write-Host "=" * 80 -ForegroundColor Gray - - $repositories | Sort-Object Repository | Format-Table -AutoSize -Wrap @( - @{Label="Repository"; Expression={$_.Repository}; Width=40}, - @{Label="Branch"; Expression={$_.Branch}; Width=20}, - @{Label="Remote URL"; Expression={$_.RemoteUrl}; Width=50} - ) - } else { - Write-Host "No Git repositories found in the specified path." -ForegroundColor Yellow - } - - Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" - -} #end function \ No newline at end of file diff --git a/devsetup/Private/Utils/Format-PrettyTable.ps1 b/devsetup/Private/Utils/Format-PrettyTable.ps1 deleted file mode 100644 index d897df9..0000000 --- a/devsetup/Private/Utils/Format-PrettyTable.ps1 +++ /dev/null @@ -1,130 +0,0 @@ -Function Format-PrettyTable { - [cmdletbinding()] - Param( - [Parameter(Mandatory=$true)] - $Columns, - [Parameter(Mandatory=$true)] - [array]$Rows, - [Parameter(Mandatory=$true)] - [hashtable]$TableFormat - ) - - # Double-line for outer edges - $edgeV = [char]0x2551 # ║ - $edgeH = [char]0x2550 # ═ - $edgeTL = [char]0x2554 # ╔ - $edgeTR = [char]0x2557 # ╗ - $edgeBL = [char]0x255A # ╚ - $edgeBR = [char]0x255D # ╝ - - $sepTD = [char]0x2564 # ╥ - $sepBU = [char]0x2567 # ╧ - $sepMD = [char]0x256A # ╪ - - # Light single-line for inner separators - $sepV = [char]0x2502 # │ - $sepH = [char]0x2500 # ─ - $sepT = [char]0x252C # ┬ - $sepM = [char]0x253C # ┼ - $sepB = [char]0x2534 # ┴ - - function Repeat-Char($char, $count) { -join (1..$count | ForEach-Object { $char }) } - function Center-Text($text, $width) { - $text = "$text" - $pad = $width - $text.Length - if ($pad -le 0) { return $text } - $left = [math]::Floor($pad / 2) - $right = $pad - $left - (' ' * $left) + $text + (' ' * $right) - } - - function Left-Text($text, $width) { - $text = " $text" - if ($text.Length -ge $width) { return $text } - return $text + (' ' * ($width - $text.Length)) - } - - function Right-Text($text, $width) { - $text = "$text " - if ($text.Length -ge $width) { return $text } - return (' ' * ($width - $text.Length)) + $text - } - - # Top border: double corners, light separators - $topBorder = $edgeTL - $middleBorder = $edgeV - $bottomBorder = $edgeBL - - $idx = 0; - foreach ($column in $Columns.Values) { - $topBorder += (Repeat-Char $edgeH $column.Width) - $middleBorder += (Repeat-Char $edgeH $column.Width) - $bottomBorder += (Repeat-Char $edgeH $column.Width) - - if ($idx -lt $Columns.Count -1) { - # Add light separators - $topBorder += $sepTD - $middleBorder += $sepMD - $bottomBorder += $sepBU - } - $idx++ - } - - $topBorder += $edgeTR - $middleBorder += $edgeV - $bottomBorder += $edgeBR - - Write-Host $topBorder -ForegroundColor $TableFormat.BorderColor - Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor -NoNewLine - - $idx = 0; - foreach ($column in $Columns.Values) { - $columnText = switch ($column.Alignment) { - "Left" { Left-Text $column.Name $column.Width } - "Center" { Center-Text $column.Name $column.Width } - "Right" { Right-Text $column.Name $column.Width } - default { $column.Name } - } - - Write-Host $columnText -ForegroundColor $column.Color -NoNewLine - - if ($idx -lt $Columns.Count -1) { - Write-Host $sepV -ForegroundColor $TableFormat.BorderColor -NoNewLine - } - $idx++ - } - - Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor - - Write-Host $middleBorder -ForegroundColor $TableFormat.BorderColor - - foreach ($row in $Rows) { - Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor -NoNewLine - $idx = 0; - foreach ($column in $Columns.Values) { - if ($row -is [hashtable]) { - $value = $row[$column.Key] - } else { - $value = $row.($column.Key) - } - - $columnText = switch ($column.Alignment) { - "Left" { Left-Text $value $column.Width } - "Center" { Center-Text $value $column.Width } - "Right" { Right-Text $value $column.Width } - default { $value } - } - - Write-Host $columnText -ForegroundColor $row.Color -NoNewLine - - if ($idx -lt $Columns.Count -1) { - Write-Host $sepV -ForegroundColor $TableFormat.BorderColor -NoNewLine - } - $idx++ - } - Write-Host $edgeV -ForegroundColor $TableFormat.BorderColor - } - - Write-Host $bottomBorder -ForegroundColor $TableFormat.BorderColor - -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 deleted file mode 100644 index 16f617e..0000000 --- a/devsetup/Private/Utils/Get-DevSetupCachePath.Tests.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupCachePath.ps1 - . $PSScriptRoot\Get-DevSetupPath.ps1 -} - -Describe "Get-DevSetupCachePath" { - BeforeEach { - Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } - } - It "should return the correct cache path for a valid user" { - $cachePath = Get-DevSetupCachePath - $cachePath | Should -Be "TestDrive:\Users\Test User\.devsetup\.cache" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupCachePath.ps1 b/devsetup/Private/Utils/Get-DevSetupCachePath.ps1 deleted file mode 100644 index 054a8fc..0000000 --- a/devsetup/Private/Utils/Get-DevSetupCachePath.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -<# -.SYNOPSIS - Gets the DevSetup cache directory path and ensures it exists. - -.DESCRIPTION - This function retrieves the cache directory path for the DevSetup module. The cache directory - is located at ".cache" within the main DevSetup directory and is used to store temporary files, - downloaded configurations, and other cached data. The function automatically creates the cache - directory if it doesn't exist, ensuring it's always available for use. - -.OUTPUTS - [System.String] - Returns the full path to the DevSetup cache directory. - -.EXAMPLE - Get-DevSetupCachePath - - Returns the path to the DevSetup cache directory, e.g., "C:\Users\Username\.devsetup\.cache" - -.EXAMPLE - $cachePath = Get-DevSetupCachePath - $tempFile = Join-Path $cachePath "temp-config.yaml" - - Gets the cache path and creates a path for a temporary file within it. - -.EXAMPLE - $cacheDir = Get-DevSetupCachePath - Get-ChildItem $cacheDir - - Gets the cache directory and lists its contents. - -.NOTES - - Uses Get-DevSetupPath to determine the base DevSetup directory - - Creates the cache directory (.cache) if it doesn't exist - - Returns the full path as a string for use in other functions - - The cache directory is hidden (starts with a dot) on Unix-like systems - - Suppresses output from New-Item using Out-Null for clean execution - - Ensures the cache directory is always available for DevSetup operations - -.LINK - -.COMPONENT - DevSetup.Utils - -.FUNCTIONALITY - Path Management, Directory Creation, Cache Management -#> - -Function Get-DevSetupCachePath { - $devSetupPath = Get-DevSetupPath - - # Define the cache path - $cachePath = Join-Path -Path $devSetupPath -ChildPath ".cache" - - if (-not (Test-Path -Path $cachePath)) { - New-Item -ItemType Directory -Path $cachePath | Out-Null - } - - return $cachePath -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 deleted file mode 100644 index 40b722f..0000000 --- a/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.Tests.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupCommunityEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupPath.ps1 - Mock Get-DevSetupEnvPath { "$TestDrive\Users\Test User\.devsetup\environments" } - Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\.devsetup" } - Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } -} - -Describe "Get-DevSetupCommunityEnvPath" { - It "Should call Get-DevSetupEnvPath and Join-Path, and return the correct path" { - $result = Get-DevSetupCommunityEnvPath - $result | Should -Be "$TestDrive\Users\Test User\.devsetup\environments\community" - Assert-MockCalled Get-DevSetupEnvPath -Exactly 1 -Scope It - Assert-MockCalled Join-Path -Exactly 1 -Scope It - } - - It "Should handle different base paths" { - Mock Get-DevSetupEnvPath { "$TestDrive\CustomPath" } - $result = Get-DevSetupCommunityEnvPath - $result | Should -Be "$TestDrive\CustomPath\community" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 b/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 deleted file mode 100644 index e7bee1f..0000000 --- a/devsetup/Private/Utils/Get-DevSetupCommunityEnvPath.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Get-DevSetupCommunityEnvPath { - # Get the DevSetup path - $devSetupEnvPath = Get-DevSetupEnvPath - - # Define the environments path - $communityEnvironmentsPath = Join-Path -Path $devSetupEnvPath -ChildPath "community" - - # Return the environments path - return $communityEnvironmentsPath -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 deleted file mode 100644 index f6a238d..0000000 --- a/devsetup/Private/Utils/Get-DevSetupEnvPath.Tests.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupPath.ps1 -} - -Describe "Get-DevSetupEnvPath" { - BeforeEach { - Mock Get-DevSetupPath { return 'TestDrive:\Users\Test User\.devsetup' } - } - It "should return the correct environment path for a valid user" { - $envPath = Get-DevSetupEnvPath - $envPath | Should -Be "TestDrive:\Users\Test User\.devsetup\environments" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupEnvPath.ps1 b/devsetup/Private/Utils/Get-DevSetupEnvPath.ps1 deleted file mode 100644 index 07308ba..0000000 --- a/devsetup/Private/Utils/Get-DevSetupEnvPath.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Get-DevSetupEnvPath { - # Get the DevSetup path - $devSetupPath = Get-DevSetupPath - - # Define the environments path - $environmentsPath = Join-Path -Path $devSetupPath -ChildPath "environments" - - # Return the environments path - return $environmentsPath -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 deleted file mode 100644 index b45d057..0000000 --- a/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.Tests.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupLocalEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupPath.ps1 - Mock Get-DevSetupEnvPath { "$TestDrive\Users\Test User\.devsetup\environments" } - Mock Get-DevSetupPath { return "$TestDrive\Users\Test User\.devsetup" } - Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } -} - -Describe "Get-DevSetupLocalEnvPath" { - It "Should call Get-DevSetupEnvPath and Join-Path, and return the correct path" { - $result = Get-DevSetupLocalEnvPath - $result | Should -Be "$TestDrive\Users\Test User\.devsetup\environments\local" - Assert-MockCalled Get-DevSetupEnvPath -Exactly 1 -Scope It - Assert-MockCalled Join-Path -Exactly 1 -Scope It - } - - It "Should handle different base paths" { - Mock Get-DevSetupEnvPath { "$TestDrive\CustomPath" } - $result = Get-DevSetupLocalEnvPath - $result | Should -Be "$TestDrive\CustomPath\local" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 b/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 deleted file mode 100644 index 722fadc..0000000 --- a/devsetup/Private/Utils/Get-DevSetupLocalEnvPath.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -function Get-DevSetupLocalEnvPath { - # Get the DevSetup path - $devSetupEnvPath = Get-DevSetupEnvPath - - # Define the environments path - $localEnvironmentsPath = Join-Path -Path $devSetupEnvPath -ChildPath "local" - - # Return the environments path - return $localEnvironmentsPath -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 deleted file mode 100644 index 5d32d9c..0000000 --- a/devsetup/Private/Utils/Get-DevSetupManifest.Tests.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupManifest.ps1 -} - -Describe "Get-DevSetupManifest" { - BeforeEach { - Mock Get-Module { - return @{ - ModuleBase = "$PSScriptRoot\..\..\..\DevSetup" - } - } - } - It "should return the manifest file and not null" { - $manifest = Get-DevSetupManifest - $manifest | Should -Not -BeNullOrEmpty - } - - It "should contain the RootModule" { - $manifest = Get-DevSetupManifest - $manifest.RootModule | Should -Not -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupManifest.ps1 b/devsetup/Private/Utils/Get-DevSetupManifest.ps1 deleted file mode 100644 index 67a38dc..0000000 --- a/devsetup/Private/Utils/Get-DevSetupManifest.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -Function Get-DevSetupManifest { - try { - $moduleBase = (Get-Module -Name "DevSetup").ModuleBase - if (-not $moduleBase) { - Write-Error "DevSetup module is not installed." - return $null - } - $manifestPath = Join-Path -Path $moduleBase -ChildPath "DevSetup.psd1" - if (-not (Test-Path -Path $manifestPath)) { - Write-Error "DevSetup module manifest not found at $manifestPath." - return $null - } - $manifest = Import-PowerShellDataFile -Path $manifestPath - if (-not $manifest) { - Write-Error "Failed to import DevSetup module manifest." - return $null - } - - # Return the manifest object - return $manifest - } - catch { - Write-Error "Failed to retrieve DevSetup manifest: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupPath.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupPath.Tests.ps1 deleted file mode 100644 index 913a919..0000000 --- a/devsetup/Private/Utils/Get-DevSetupPath.Tests.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupPath.ps1 - . $PSScriptRoot\Get-EnvironmentVariable.ps1 -} - -Describe "Get-DevSetupPath" { - BeforeEach { - Mock Get-EnvironmentVariable { return 'TestDrive:\Users\Test User' } - } - It "should return the correct devsetup for the current user" { - $envPath = Get-DevSetupPath - $envPath | Should -Be "TestDrive:\Users\Test User\devsetup" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupPath.ps1 b/devsetup/Private/Utils/Get-DevSetupPath.ps1 deleted file mode 100644 index b7d2393..0000000 --- a/devsetup/Private/Utils/Get-DevSetupPath.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -Function Get-DevSetupPath { - # Get user's home directory - $homeDirectory = Get-EnvironmentVariable USERPROFILE - - # Define .devsetup folder path - $devSetupPath = Join-Path -Path $homeDirectory -ChildPath "devsetup" - - return $devSetupPath -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 b/devsetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 deleted file mode 100644 index df90555..0000000 --- a/devsetup/Private/Utils/Get-DevSetupVersion.Tests.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-DevSetupVersion.ps1 - . $PSScriptRoot\Get-DevSetupManifest.ps1 -} - -Describe "Get-DevSetupVersion" { - BeforeEach { - Mock Get-DevSetupManifest { - return @{ - ModuleVersion = '1.0.0' - PrivateData = @{ - PSData = @{ - ProjectUri = 'https://github.com/your/repo' - } - } - } - } - - function Get-GitHubRelease {} - - Mock Get-GitHubRelease { - return @{ - tag_name = '1.0.0' - } - } - } - It "should return the correct version when looking locally" { - $version = Get-DevSetupVersion -Local - $version | Should -Be "1.0.0" - } - - It "should return the correct version when looking remotely" { - $version = Get-DevSetupVersion -Remote - $version | Should -Be "1.0.0" - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-DevSetupVersion.ps1 b/devsetup/Private/Utils/Get-DevSetupVersion.ps1 deleted file mode 100644 index 9c71bf2..0000000 --- a/devsetup/Private/Utils/Get-DevSetupVersion.ps1 +++ /dev/null @@ -1,104 +0,0 @@ -Function Get-DevSetupVersion { - <# - .SYNOPSIS - Retrieves the version of the DevSetup module. - - .DESCRIPTION - Get-DevSetupVersion returns the current version of the DevSetup module either from the locally installed version or from the latest GitHub release. - - .PARAMETER Local - Retrieves the version from the locally installed DevSetup module. This is the default behavior if no parameter is specified. - - .PARAMETER Remote - Retrieves the latest version from the GitHub repository using the latest release tag. - - .OUTPUTS - System.Version. Returns the version object of the DevSetup module. - - .EXAMPLE - Get-DevSetupVersion - Returns the version object of the locally installed DevSetup module. - - .EXAMPLE - Get-DevSetupVersion -Local - Returns the version object of the locally installed DevSetup module. - - .EXAMPLE - Get-DevSetupVersion -Remote - Returns the version object of the latest release from the GitHub repository. - - .EXAMPLE - $version = Get-DevSetupVersion -Local - Write-Host "Major: $($version.Major), Minor: $($version.Minor), Build: $($version.Build)" - Gets the local version and displays individual components. - - .NOTES - This function is used to check the installed version of the DevSetup module and returns a Version object for easy comparison and component access. - The Local and Remote parameters are mutually exclusive. If neither is specified, Local is used by default. - #> - - Param( - [Parameter(Mandatory = $false)] - [switch]$Local, - - [Parameter(Mandatory = $false)] - [switch]$Remote - ) - - # Validate that only one parameter is specified - if ($Local -and $Remote) { - Write-Error "Local and Remote parameters are mutually exclusive. Please specify only one." - return $null - } - - # Default to Local if no parameter is specified - if (-not $Local -and -not $Remote) { - $Local = $true - } - - $manifest = Get-DevSetupManifest - if (-not $manifest) { - Write-Error "Failed to retrieve DevSetup module manifest." - return $null - } - - if ($Local) { - if (-not $manifest.ModuleVersion) { - Write-Error "Version information not found in the DevSetup module manifest." - return $null - } - try { - $versionObject = [Version]::new($manifest.ModuleVersion) - return $versionObject - } - catch { - Write-Error "Failed to parse version '$($manifest.ModuleVersion)' as a valid version object: $_" - return $null - } - } - - if ($Remote) { - try { - $projectUri = $manifest.PrivateData.PSData.ProjectUri - if (-not $projectUri) { - Write-Error "ProjectUri not found in the DevSetup module manifest." - return $null - } - - $release = Get-GitHubRelease -Uri $projectUri - if (-not $release -or -not $release.tag_name) { - Write-Error "Failed to retrieve latest release information from GitHub." - return $null - } - - # Remove 'v' prefix if present in tag name - $versionString = $release.tag_name -replace '^v', '' - $versionObject = [Version]::new($versionString) - return $versionObject - } - catch { - Write-Error "Failed to retrieve or parse remote version: $_" - return $null - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 b/devsetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 deleted file mode 100644 index cd2c911..0000000 --- a/devsetup/Private/Utils/Get-EnvironmentVariable.Tests.ps1 +++ /dev/null @@ -1,35 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-EnvironmentVariable.ps1 -} - -Describe "Get-EnvironmentVariable" { - - Context "When the environment variable exists" { - It "Should return the value of the variable" { - $env:TEST_ENV_VAR = "TestValue" - $result = Get-EnvironmentVariable -Name "TEST_ENV_VAR" - $result | Should -Be "TestValue" - Remove-Item Env:\TEST_ENV_VAR - } - } - - Context "When the environment variable does not exist" { - It "Should return null" { - Remove-Item Env:\NOT_EXISTING_VAR -ErrorAction SilentlyContinue - $result = Get-EnvironmentVariable -Name "NOT_EXISTING_VAR" - $result | Should -Be $null - } - } - - Context "When called with pipeline input" { - It "Should return the value for each variable" { - $env:PIPE_VAR1 = "Value1" - $env:PIPE_VAR2 = "Value2" - $results = @("PIPE_VAR1", "PIPE_VAR2") | Get-EnvironmentVariable - $results | Should -Contain "Value1" - $results | Should -Contain "Value2" - Remove-Item Env:\PIPE_VAR1 - Remove-Item Env:\PIPE_VAR2 - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-EnvironmentVariable.ps1 b/devsetup/Private/Utils/Get-EnvironmentVariable.ps1 deleted file mode 100644 index b0f730c..0000000 --- a/devsetup/Private/Utils/Get-EnvironmentVariable.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Function Get-EnvironmentVariable { - [cmdletbinding()] - param ( - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string]$Name - ) - process { - Write-Output ([System.Environment]::GetEnvironmentVariable($Name)) - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-PwshVersion.Tests.ps1 b/devsetup/Private/Utils/Get-PwshVersion.Tests.ps1 deleted file mode 100644 index a034fa5..0000000 --- a/devsetup/Private/Utils/Get-PwshVersion.Tests.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Get-PwshVersion.ps1 -} - -Describe "Get-PwshVersion" { - - Context "When called in a typical PowerShell environment" { - It "Should return a hashtable with Major, Minor, and Patch keys" { - $result = Get-PwshVersion - $result | Should -BeOfType 'hashtable' - $result.Keys | Should -Contain 'Major' - $result.Keys | Should -Contain 'Minor' - $result.Keys | Should -Contain 'Patch' - } - - It "Should return correct version numbers from \$PSVersionTable" { - $expectedMajor = $PSVersionTable.PSVersion.Major - $expectedMinor = $PSVersionTable.PSVersion.Minor - $expectedPatch = $PSVersionTable.PSVersion.Build - $result = Get-PwshVersion - $result.Major | Should -Be $expectedMajor - $result.Minor | Should -Be $expectedMinor - $result.Patch | Should -Be $expectedPatch - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Get-PwshVersion.ps1 b/devsetup/Private/Utils/Get-PwshVersion.ps1 deleted file mode 100644 index 4b39bd7..0000000 --- a/devsetup/Private/Utils/Get-PwshVersion.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Function Get-PwshVersion { - [CmdletBinding()] - Param() - - return @{ - Major = $PSVersionTable.PSVersion.Major - Minor = $PSVersionTable.PSVersion.Minor - Patch = $PSVersionTable.PSVersion.Build - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 b/devsetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 deleted file mode 100644 index 643628f..0000000 --- a/devsetup/Private/Utils/Initialize-DevSetupEnvs.Tests.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -BeforeAll { - Function Get-GitHubRepository { } - Function Install-GitRepository { } - - . $PSScriptRoot\Initialize-DevSetupEnvs.ps1 - . $PSScriptRoot\Write-StatusMessage.ps1 - . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupManifest.ps1 - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } - Mock Get-DevSetupManifest { - @{ - PrivateData = @{ - PSData = @{ - EnvironmentsProjectUri = "https://github.com/example/envrepo" - } - } - } - } - Mock Get-GitHubRepository { @{ clone_url = "https://github.com/example/envrepo.git" } } - Mock Test-Path { $false } - Mock Install-GitRepository { $true } - Mock Write-StatusMessage { } - Mock Optimize-DevSetupEnvs { } - Mock Write-Error { } - Mock Write-Verbose { } - Mock Get-DevSetupLocalEnvPath { "TestDrive:\DevSetupEnvs\environments\local" } - Mock Get-DevSetupCommunityEnvPath { "TestDrive:\DevSetupEnvs\environments\community" } -} - -Describe "Initialize-DevSetupEnvs" { - - Context "When manifest cannot be retrieved" { - It "Should write error and return null" { - Mock Get-DevSetupManifest { $null } - $result = Initialize-DevSetupEnvs - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to retrieve DevSetup module manifest" } - } - } - - Context "When EnvironmentsProjectUri is missing" { - It "Should write error and return null" { - Mock Get-DevSetupManifest { @{ PrivateData = @{ PSData = @{ } } } } - $result = Initialize-DevSetupEnvs - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "EnvironmentsProjectUri not found" } - } - } - - Context "When EnvironmentsProjectUri is not a .git URL and GitHub API fails" { - It "Should write error and return null" { - Mock Get-GitHubRepository { $null } - $result = Initialize-DevSetupEnvs - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to retrieve repository information or clone_url" } - } - } - - Context "When Get-GitHubRepository throws" { - It "Should write error and return null" { - Mock Get-GitHubRepository { throw "API error" } - $result = Initialize-DevSetupEnvs - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to get repository information from GitHub" } - } - } - - Context "When EnvironmentsProjectUri is a .git URL" { - It "Should use the URI directly and clone the repository" { - Mock Get-DevSetupManifest { - @{ - PrivateData = @{ - PSData = @{ - EnvironmentsProjectUri = "https://github.com/example/envrepo.git" - } - } - } - } - $result = Initialize-DevSetupEnvs - $result | Should -BeOfType [hashtable] - $result.local | Should -Be "TestDrive:\DevSetupEnvs\environments\local" - $result.community | Should -Be "TestDrive:\DevSetupEnvs\environments\community" - Assert-MockCalled Test-Path -Scope It -Exactly 3 - Assert-MockCalled Get-GithubRepository -Scope It -Exactly 0 - Assert-MockCalled Write-Verbose -Scope It -Exactly 0 -ParameterFilter { $Message -match "Environments repository already exists*" } - Assert-MockCalled Write-StatusMessage -Scope It -Exactly 2 - #Assert-MockCalled Install-GitRepository -Scope It -ParameterFilter { $RepositoryUrl -eq "https://github.com/example/envrepo.git" } - } - } - - Context "When repository path already exists" { - It "Should not clone and should write verbose" { - Mock Test-Path { $true } - $result = Initialize-DevSetupEnvs - $result | Should -BeOfType [hashtable] - $result.local | Should -Be "TestDrive:\DevSetupEnvs\environments\local" - $result.community | Should -Be "TestDrive:\DevSetupEnvs\environments\community" - Assert-MockCalled Install-GitRepository -Times 0 -Scope It - Assert-MockCalled Write-Verbose -Scope It -ParameterFilter { $Message -match "already exists" } - } - } - - Context "When Install-GitRepository fails" { - It "Should write failed status message" { - Mock Test-Path { $false } - Mock Install-GitRepository { $null } - $global:LASTEXITCODE = 1 - $result = Initialize-DevSetupEnvs - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } - } - } - - Context "When Install-GitRepository succeeds" { - It "Should write OK status message" { - Mock Test-Path { $false } - Mock Install-GitRepository { $null } - $global:LASTEXITCODE = 0 - $result = Initialize-DevSetupEnvs - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } - } - } - - Context "When Optimize-DevSetupEnvs is called" { - It "Should call Optimize-DevSetupEnvs after cloning" { - $result = Initialize-DevSetupEnvs - Assert-MockCalled Optimize-DevSetupEnvs -Scope It - } - } - - Context "When an unexpected error occurs" { - It "Should write error and return null" { - Mock Get-DevSetupEnvPath { throw "Unexpected error" } - $result = Initialize-DevSetupEnvs - $result | Should -Be $null - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to initialize DevSetup environment" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Initialize-DevSetupEnvs.ps1 b/devsetup/Private/Utils/Initialize-DevSetupEnvs.ps1 deleted file mode 100644 index d8e29ee..0000000 --- a/devsetup/Private/Utils/Initialize-DevSetupEnvs.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -Function Initialize-DevSetupEnvs { - try { - # Define environments repository path - $environmentsPath = Get-DevSetupEnvPath - $localEnvironmentsPath = Get-DevSetupLocalEnvPath - $communityEnvironmentsPath = Get-DevSetupCommunityEnvPath - - # Get the environments repository URL from the manifest - $manifest = Get-DevSetupManifest - if (-not $manifest) { - Write-Error "Failed to retrieve DevSetup module manifest." - return $null - } - - $environmentsProjectUri = $manifest.PrivateData.PSData.EnvironmentsProjectUri - if (-not $environmentsProjectUri) { - Write-Error "EnvironmentsProjectUri not found in the DevSetup module manifest." - return $null - } - - # Check if the URI ends with .git, if not use Get-GitHubRepository to get clone_url - if ($environmentsProjectUri -notlike "*.git") { - try { - Set-GitHubConfiguration -DisableTelemetry - #Write-Host "GitHub API access is required to retrieve repository information." -ForegroundColor Yellow - #Write-Host "Please create a GitHub Personal Access Token with 'repo' scope at:" -ForegroundColor Yellow - #Write-Host "https://github.com/settings/tokens" -ForegroundColor Cyan - #Write-Host "" - - # Prompt for GitHub access token with masked input - #$secureToken = Read-Host "Enter your GitHub Personal Access Token" -AsSecureString - #if (-not $secureToken -or $secureToken.Length -eq 0) { - # Write-Error "GitHub access token is required to continue." - # return $null - #} - - #$cred = New-Object System.Management.Automation.PSCredential "token", $secureToken - #Set-GitHubAuthentication -Credential $cred - #$secureToken = $null # clear this out now that it's no longer needed - #$cred = $null # clear this out now that it's no longer needed - $repository = Get-GitHubRepository -Uri $environmentsProjectUri 3>$null - if (-not $repository -or -not $repository.clone_url) { - Write-Error "Failed to retrieve repository information or clone_url from GitHub." - return $null - } - $repositoryUrl = $repository.clone_url - } - catch { - Write-Error "Failed to get repository information from GitHub: $_" - return $null - } - } else { - $repositoryUrl = $environmentsProjectUri - } - - if(-not (Test-Path $environmentsPath)) { - New-Item -Path $environmentsPath -Type Directory | Out-Null - } - - if(-not (Test-Path $localEnvironmentsPath)) { - New-Item -Path $localEnvironmentsPath -Type Directory | Out-Null - } - - - # Clone the environments repository if it doesn't exist - if (-not (Test-Path -Path $communityEnvironmentsPath)) { - Write-StatusMessage "- Cloning $repositoryUrl" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline - Install-GitRepository -RepositoryUrl $repositoryUrl -DestinationPath $communityEnvironmentsPath -UpdateExisting:$true *>$null - if($LASTEXITCODE -ne 0) { - Write-StatusMessage "[Failed]" -ForegroundColor Red - } else { - Write-StatusMessage "[OK]" -ForegroundColor Green - } - } else { - Write-Verbose "Environments repository already exists at: $communityEnvironmentsPath" - } - - Optimize-DevSetupEnvs | Out-Null - - # Return the path for use by other functions - return @{ - Local = $localEnvironmentsPath - Community = $communityEnvironmentsPath - } - } - catch { - Write-Error "Failed to initialize DevSetup environment: $_" - return $null - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 b/devsetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 deleted file mode 100644 index 9818354..0000000 --- a/devsetup/Private/Utils/Optimize-DevSetupEnvs.Tests.ps1 +++ /dev/null @@ -1,127 +0,0 @@ -BeforeAll { - Function ConvertFrom-Yaml { } - . $PSScriptRoot\Optimize-DevSetupEnvs.ps1 - . $PSScriptRoot\Get-DevSetupEnvPath.ps1 - . $PSScriptRoot\Get-DevSetupPath.ps1 - . $PSScriptRoot\Write-StatusMessage.ps1 - . $PSScriptRoot\Read-ConfigurationFile.ps1 - Mock Get-DevSetupEnvPath { "$TestDrive\DevSetupEnvs" } - Mock Get-DevSetupPath { "$TestDrive\DevSetup" } - Mock Join-Path { Param($Path, $ChildPath) "$Path\$ChildPath" } - Mock Write-StatusMessage { } - Mock Write-Warning { } - Mock Write-Error { } - Mock Write-Debug { } - Mock Write-Host { } - Mock ConvertTo-Json { param($obj) "json-output" } - Mock Out-File { } - Mock Read-ConfigurationFile { - param($Config) - switch ($Config) { - "$TestDrive\DevSetupEnvs\env1.yaml" { - @{ devsetup = @{ configuration = @{ os = @{ name = "Windows" }; version = "1.0.0" } } } - } - "$TestDrive\DevSetupEnvs\env2.yaml" { - @{ devsetup = @{ configuration = @{ os = @{ name = "Linux" }; version = "2.0.0" } } } - } - default { $null } - } - } -} - -Describe "Optimize-DevSetupEnvs" { - - Context "When environments path is missing or invalid" { - It "Should warn and return false" { - Mock Get-DevSetupEnvPath { $null } - $result = Optimize-DevSetupEnvs - $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } - } - - It "Should warn and return null if path does not exist" { - Mock Get-DevSetupEnvPath { "TestDrive:\DevSetupEnvs" } - Mock Test-Path { $false } - $result = Optimize-DevSetupEnvs - $result | Should -Be $false - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "DevSetup environments path not found" } - } - } - - Context "When no YAML files are found" { - It "Should write status message and return empty array" { - Mock Test-Path { $true } - Mock Get-ChildItem { @() } - $result = Optimize-DevSetupEnvs - $result | Should -BeOfType 'bool' - $result | Should -Be $true - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -match "Indexing 0 environment files" } - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[OK]" } - } - } - - Context "When YAML files are found and processed successfully" { - It "Should return environments array and write status messages" { - Mock Test-Path { $true } - Mock Get-ChildItem { - @( - @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" }, - @{ Name = "env2.yaml"; FullName = "$TestDrive\DevSetupEnvs\env2.yaml" } - ) - } - $result = Optimize-DevSetupEnvs - Assert-MockCalled Write-Error -Scope It -Exactly 0 - #Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Object -match "Indexing 2 environment files" } - #Assert-MockCalled Write-Host -Scope It -ParameterFilter { $Object -eq "[OK]" } - $result | Should -BeOfType 'bool' - $result | Should -Be $false - } - } - - Context "When a YAML file fails to process" { - It "Should warn and continue processing other files" { - Mock Test-Path { $true } - Mock Get-ChildItem { - @( - @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" }, - @{ Name = "bad.yaml"; FullName = "$TestDrive\DevSetupEnvs\bad.yaml" } - ) - } - Mock Read-ConfigurationFile { - param($Config) - if ($Config -eq "$TestDrive\DevSetupEnvs\bad.yaml") { throw "YAML error" } - @{ devsetup = @{ configuration = @{ os = @{ name = "Windows" }; version = "1.0.0" } } } - } - $result = Optimize-DevSetupEnvs - Assert-MockCalled Write-Error -Scope It -Exactly 0 - Assert-MockCalled Write-Warning -Scope It -ParameterFilter { $Message -match "Failed to process bad.yaml" } - $result | Should -BeOfType 'bool' - $result | Should -Be $false - - } - } - - Context "When writing environments.json fails" { - It "Should write failed status message and return null" { - Mock Test-Path { $true } - Mock Get-ChildItem { - @( - @{ Name = "env1.yaml"; FullName = "$TestDrive\DevSetupEnvs\env1.yaml" } - ) - } - Mock Out-File { throw "File error" } - $result = Optimize-DevSetupEnvs - $result | Should -Be $false - Assert-MockCalled Write-StatusMessage -Scope It -ParameterFilter { $Message -eq "[Failed]" } - } - } - - Context "When an unexpected error occurs" { - It "Should write error and return null" { - Mock Get-DevSetupEnvPath { throw "Unexpected error" } - $result = Optimize-DevSetupEnvs - $result | Should -Be $false - Assert-MockCalled Write-Error -Scope It -ParameterFilter { $Message -match "Failed to optimize DevSetup environments" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Optimize-DevSetupEnvs.ps1 b/devsetup/Private/Utils/Optimize-DevSetupEnvs.ps1 deleted file mode 100644 index d262fea..0000000 --- a/devsetup/Private/Utils/Optimize-DevSetupEnvs.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -Function Optimize-DevSetupEnvs { - try { - # Get the DevSetup environments path - $envPath = Get-DevSetupEnvPath - if (-not $envPath -or -not (Test-Path $envPath)) { - Write-Warning "DevSetup environments path not found or doesn't exist: $envPath" - return $false - } - - # Get all YAML files in the environments path - $devsetupEnvFiles = Get-ChildItem -Path $envPath -Filter "*.devsetup" -File -Recurse - if (-not $devsetupEnvFiles) { - #Write-Host "No YAML environment files found in: $envPath" -ForegroundColor Yellow - Write-StatusMessage "- Indexing 0 environment files" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline | Out-Null - Write-StatusMessage "[OK]" -ForegroundColor Green | Out-Null - return $true - } - - #Write-Host "Found $($yamlFiles.Count) environment file(s) to process..." -ForegroundColor Cyan - Write-StatusMessage "- Indexing $($devsetupEnvFiles.Count) environment files" -ForegroundColor Gray -Indent 2 -Width 77 -NoNewline | Out-Null - - $environments = @() - - foreach ($devsetupEnvFile in $devsetupEnvFiles) { - try { - Write-Debug "Processing: $($devsetupEnvFile.Name)" - - # Read the YAML configuration - $config = Read-ConfigurationFile -Config $devsetupEnvFile.FullName - - # Extract environment name (filename without extension) - $envName = [System.IO.Path]::GetFileNameWithoutExtension($devsetupEnvFile.Name) - - # Extract platform information - $platform = $null - if ($config -and $config.devsetup -and $config.devsetup.configuration -and $config.devsetup.configuration.os -and $config.devsetup.configuration.os.name) { - $platform = $config.devsetup.configuration.os.name - } - - # Extract version information - $version = "Unknown" - if ($config -and $config.devsetup -and $config.devsetup.configuration -and $config.devsetup.configuration -and $config.devsetup.configuration.version) { - $version = $config.devsetup.configuration.version - } - - $provider = ($devsetupEnvFile.FullName.Split([System.IO.Path]::DirectorySeparatorChar))[-2] - - if($provider -ne 'local') { - $envName = $provider + ":" + $envName - } - - # Create environment entry - $envEntry = @{ - name = $envName - platform = $platform - version = $version - file = $devsetupEnvFile.Name - provider = $provider - } - - $environments += $envEntry - $platformDisplay = if ($platform) { $platform } else { 'Not specified' } - Write-Debug " - Name: $envName, Version: $version, Platform: $platformDisplay" - } - catch { - Write-Warning "Failed to process $($devsetupEnvFile.Name): $_" - continue - } - } - - # Write results to environments.json - $devSetupPath = Get-DevSetupPath - $environmentsJsonPath = Join-Path -Path $devSetupPath -ChildPath "environments.json" - - try { - $jsonOutput = $environments | ConvertTo-Json -Depth 10 - $jsonOutput | Out-File -FilePath $environmentsJsonPath -Encoding UTF8 - Write-Debug "Environment index written to: $environmentsJsonPath" - #Write-Host "Processed $($environments.Count) environment(s) successfully" -ForegroundColor Green - Write-StatusMessage "[OK]" -ForegroundColor Green - } - catch { - Write-StatusMessage "[Failed]" -ForegroundColor Red - return $false - } - - return $true - } - catch { - Write-Error "Failed to optimize DevSetup environments: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 b/devsetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 deleted file mode 100644 index 9ebed88..0000000 --- a/devsetup/Private/Utils/Read-ConfigurationFile.Tests.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -BeforeAll { - function ConvertFrom-Yaml { } - . $PSScriptRoot\Read-ConfigurationFile.ps1 - Mock Get-Content { } - Mock ConvertFrom-Yaml { } -} - -Describe "Read-ConfigurationFile" { - - Context "When configuration file exists and contains valid YAML" { - It "Should return parsed YAML data" { - Mock Get-Content { "key: value" } - Mock ConvertFrom-Yaml { @{ key = "value" } } - $result = Read-ConfigurationFile -Config "config.yaml" - $result | Should -BeOfType System.Collections.Hashtable - $result.key | Should -Be "value" - } - } - - Context "When configuration file does not exist" { - It "Should throw an error" { - Mock Get-Content { throw "File not found" } - { Read-ConfigurationFile -Config "missing.yaml" } | Should -Throw "File not found" - } - } - - Context "When YAML is invalid" { - It "Should throw an error from ConvertFrom-Yaml" { - Mock Get-Content { "invalid: yaml: -" } - Mock ConvertFrom-Yaml { throw "Invalid YAML" } - { Read-ConfigurationFile -Config "bad.yaml" } | Should -Throw "Invalid YAML" - } - } - - Context "When ConvertFrom-Yaml returns $null" { - It "Should return null" { - Mock Get-Content { "key: value" } - Mock ConvertFrom-Yaml { $null } - $result = Read-ConfigurationFile -Config "config.yaml" - $result | Should -Be $null - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Read-ConfigurationFile.ps1 b/devsetup/Private/Utils/Read-ConfigurationFile.ps1 deleted file mode 100644 index 8a39e57..0000000 --- a/devsetup/Private/Utils/Read-ConfigurationFile.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -Function Read-ConfigurationFile { - param ( - [string]$Config - ) - $YamlData = ConvertFrom-Yaml (Get-Content -Path $Config -Raw) - return $YamlData -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Test-OperatingSystem.Tests.ps1 b/devsetup/Private/Utils/Test-OperatingSystem.Tests.ps1 deleted file mode 100644 index 9c3d61b..0000000 --- a/devsetup/Private/Utils/Test-OperatingSystem.Tests.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-OperatingSystem.ps1 - . $PSScriptRoot\Get-PwshVersion.ps1 -} - -Describe "Test-OperatingSystem" { - - if ($PSVersionTable.PSVersion.Major -eq 5) { - BeforeAll { Mock Get-PwshVersion { [PSCustomObject]@{ Major = $PSVersionTable.PSVersion.Major } } } - - Context "When called with -Windows on PowerShell 5.1" { - It "Should return $true" { - $result = Test-OperatingSystem -Windows - $result | Should -Be $true - } - } - - Context "When called with -Linux on PowerShell 5.1" { - It "Should return $false" { - $result = Test-OperatingSystem -Linux - $result | Should -Be $false - } - } - - Context "When called with -MacOS on PowerShell 5.1" { - It "Should return $false" { - $result = Test-OperatingSystem -MacOS - $result | Should -Be $false - } - } - - Context "When called with no parameters on PowerShell 5.1" { - It "Should return $null" { - $result = Test-OperatingSystem - $result | Should -Be $null - } - } - } - - if ($PSVersionTable.PSVersion.Major -ge 6) { - BeforeAll { Mock Get-PwshVersion { [PSCustomObject]@{ Major = $PSVersionTable.PSVersion.Major } } } - - Context "When called with -Windows on PowerShell 7+" { - It "Should return value of `$IsWindows (default: $true)" { - $result = Test-OperatingSystem -Windows - $result | Should -Be $true - } - It "Should return value of `$IsLinux (default: $false)" { - $result = Test-OperatingSystem -Linux - $result | Should -Be $false - } - It "Should return value of `$IsMacOS (default: $false)" { - $result = Test-OperatingSystem -MacOS - $result | Should -Be $false - } - It "Should return $null if no parameter is specified" { - $result = Test-OperatingSystem - $result | Should -Be $null - } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Test-OperatingSystem.ps1 b/devsetup/Private/Utils/Test-OperatingSystem.ps1 deleted file mode 100644 index ebb1f27..0000000 --- a/devsetup/Private/Utils/Test-OperatingSystem.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -Function Test-OperatingSystem { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$false)] - [switch]$Windows, - - [Parameter(Mandatory=$false)] - [switch]$Linux, - - [Parameter(Mandatory=$false)] - [switch]$MacOS - ) - - if((Get-PwshVersion).Major -lt 6) { - $IsWindows = $true - $IsLinux = $false - $IsMacOS = $false - } - - if($Windows) { - return $IsWindows - } - if($Linux) { - return $IsLinux - } - if($MacOS) { - return $IsMacOS - } - return $null -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 b/devsetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 deleted file mode 100644 index 6865457..0000000 --- a/devsetup/Private/Utils/Test-RunningAsAdmin.Tests.ps1 +++ /dev/null @@ -1,46 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Test-RunningAsAdmin.ps1 - . $PSScriptRoot\Test-OperatingSystem.ps1 - Mock Test-OperatingSystem { param($Windows) $false } -} - -Describe "Test-RunningAsAdmin" { - - Context "When not running on Windows" { - It "Should return true (assume sufficient privileges)" { - Mock Test-OperatingSystem { param($Windows) $false } - $result = Test-RunningAsAdmin - $result | Should -Be $true - } - } - - Context "When running on Windows as administrator" { - It "Should return true" { - Mock Test-OperatingSystem { param($Windows) $true } - class MockPrincipal { - [bool] IsInRole([object]$role) { return $true } - } - Mock 'New-Object' -MockWith { - param($type) - return [MockPrincipal]::new() - } - $result = Test-RunningAsAdmin - $result | Should -Be $true - } - } - - Context "When running on Windows but not as administrator" { - It "Should return false" { - Mock Test-OperatingSystem { param($Windows) $true } - class MockPrincipal { - [bool] IsInRole([object]$role) { return $false } - } - Mock 'New-Object' -MockWith { - param($type) - return [MockPrincipal]::new() - } - $result = Test-RunningAsAdmin - $result | Should -Be $false - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Test-RunningAsAdmin.ps1 b/devsetup/Private/Utils/Test-RunningAsAdmin.ps1 deleted file mode 100644 index 85bf9d5..0000000 --- a/devsetup/Private/Utils/Test-RunningAsAdmin.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -Function Test-RunningAsAdmin { - # Check if we're on Windows - Windows security principals are Windows-only - if (-not (Test-OperatingSystem -Windows)) { - # On non-Windows platforms, assume we have sufficient privileges - return $true - } - - $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) - if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - return $false - } - return $true -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Write-NewConfig.ps1 b/devsetup/Private/Utils/Write-NewConfig.ps1 deleted file mode 100644 index df37b3e..0000000 --- a/devsetup/Private/Utils/Write-NewConfig.ps1 +++ /dev/null @@ -1,223 +0,0 @@ -Function Write-NewConfig { - Param( - [Parameter(Mandatory = $true)] - [string]$OutFile - ) - - try { - # Check if running as administrator - if (-not (Test-RunningAsAdmin)) { - throw "This operation requires administrator privileges. Please run as administrator." - } - - # Create base config file - #Write-Host "Creating base configuration file: $OutFile" -ForegroundColor Cyan - - # Get OS information in a PowerShell 5.1 compatible way - $platform = [System.Environment]::OSVersion.Platform.ToString() - $osArchitecture = if ([System.Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } - - # Make platform more user-friendly - $friendlyPlatform = switch ($platform) { - "Win32NT" { "Windows" } - "Unix" { - # Check if it's macOS or Linux in a PS 5.1 compatible way - $uname = "" - try { - $uname = (& uname -s 2>$null) - } catch {} - if ($uname -eq "Darwin") { - "macOS" - } else { - "Linux" - } - } - default { $platform } - } - - # Get friendly OS version - $friendlyOsVersion = switch ($platform) { - "Win32NT" { - try { - $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue - if ($osInfo) { - $osInfo.Caption -replace "Microsoft ", "" - } else { - [System.Environment]::OSVersion.VersionString - } - } - catch { - [System.Environment]::OSVersion.VersionString - } - } - "Unix" { - if ($friendlyPlatform -eq "macOS") { - try { - $macVersion = (& sw_vers -productVersion 2>$null) - if ($macVersion) { - "macOS $macVersion" - } else { - [System.Environment]::OSVersion.VersionString - } - } - catch { - [System.Environment]::OSVersion.VersionString - } - } else { - # Linux - try { - $linuxVersion = "" - if (Test-Path "/etc/os-release") { - $osRelease = Get-Content "/etc/os-release" | Where-Object { $_ -like "PRETTY_NAME=*" } - if ($osRelease) { - $linuxVersion = ($osRelease -split '=')[1] -replace '"', '' - } - } - if ($linuxVersion) { - $linuxVersion - } else { - [System.Environment]::OSVersion.VersionString - } - } - catch { - [System.Environment]::OSVersion.VersionString - } - } - } - default { - [System.Environment]::OSVersion.VersionString - } - } - - # Handle versioning and preserve existing config - $currentVersion = "1.0.0" # Default version for new files - $baseConfig = @{ - devsetup = @{ - dependencies = @{ - chocolatey = @{ - packages = @() - } - powershell = @{ - modules = @() - scope = "CurrentUser" - } - scoop = @{ - packages = @() - buckets = @() - } - } - commands = @() - configuration = @{ - description = "Auto-generated development environment configuration" - version = $currentVersion - createdDate = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - createdBy = $env:USERNAME - os = @{ - name = $friendlyPlatform - version = $friendlyOsVersion - architecture = $osArchitecture - } - powershell = @{ - version = $PSVersionTable.PSVersion.ToString() - edition = $PSVersionTable.PSEdition - } - } - } - } - - if (Test-Path $OutFile) { - try { - Write-Host "- Using existing configuration..." -ForegroundColor Gray - $existingConfig = Read-ConfigurationFile -Config $OutFile - if ($existingConfig -and $existingConfig.devsetup) { - # Preserve existing dependencies - if ($existingConfig.devsetup.dependencies) { - $baseConfig.devsetup.dependencies = $existingConfig.devsetup.dependencies - } - - # Preserve existing commands - if ($existingConfig.devsetup.commands) { - $baseConfig.devsetup.commands = $existingConfig.devsetup.commands - } - - # Handle version increment - if ($existingConfig.devsetup.configuration -and $existingConfig.devsetup.configuration.version) { - $existingVersionString = $existingConfig.devsetup.configuration.version - - try { - # Parse version using System.Version - $existingVersion = [System.Version]$existingVersionString - $newMinor = $existingVersion.Minor + 1 - $currentVersion = "$($existingVersion.Major).$newMinor.$($existingVersion.Build)" - $baseConfig.devsetup.configuration.version = $currentVersion - Write-Host "- Version: $existingVersionString -> $currentVersion" -ForegroundColor Gray - } - catch { - Write-Warning "- Version: $currentVersion" - } - } else { - Write-Host "- Version: $currentVersion" -ForegroundColor Gray - } - - # Preserve other configuration fields but update system info - if ($existingConfig.devsetup.configuration) { - $baseConfig.devsetup.configuration.description = $existingConfig.devsetup.configuration.description - $baseConfig.devsetup.configuration.createdBy = $existingConfig.devsetup.configuration.createdBy - if ($existingConfig.devsetup.configuration.createdDate) { - # Keep original creation date, but we could add a lastModified field - $baseConfig.devsetup.configuration.createdDate = $existingConfig.devsetup.configuration.createdDate - $baseConfig.devsetup.configuration.lastModified = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - } - } - } - } - catch { - Write-Warning "Failed to read existing configuration for merging: $_" - Write-Host "- Using new configuration with default version: $currentVersion" -ForegroundColor Gray - } - } else { - Write-Host "- Using new configuration file, starting with version: $currentVersion" -ForegroundColor Green - } - - try { - $yamlOutput = $baseConfig | ConvertTo-Yaml - $yamlOutput | Out-File -FilePath $OutFile -Encoding UTF8 - Write-Debug "Base configuration file created successfully!" - } - catch { - Write-Error "Failed to create base configuration file: $_" - return $false - } - - # Convert from installed Chocolatey packages - Write-Host "`nScanning installed Chocolatey packages..." -ForegroundColor Cyan - if (-not (Export-InstalledChocolateyPackages -Config $OutFile)) { - Write-Warning "Failed to convert Chocolatey packages, but continuing..." - } - - # Convert from installed Scoop packages - Write-Host "`nScanning installed Scoop packages..." -ForegroundColor Cyan - if (-not (Export-InstalledScoopPackages -Config $OutFile)) { - Write-Warning "Failed to convert Scoop packages, but continuing..." - } - - # Convert from installed PowerShell modules - Write-Host "`nScanning installed PowerShell modules..." -ForegroundColor Cyan - if (-not (Export-InstalledPowershellModules -Config $OutFile)) { - Write-Warning "Failed to convert PowerShell modules, but continuing..." - } - - ConvertFrom-3rdPartyInstall -Config $OutFile - - Write-Host "`nConfiguration file generation completed!" -ForegroundColor Green - Write-Host "- Configuration saved to: $OutFile" -ForegroundColor Gray - Write-Host "" - - Optimize-DevSetupEnvs - return $true - } - catch { - Write-Error "Error creating new configuration: $_" - return $false - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Write-StatusMessage.Tests.ps1 b/devsetup/Private/Utils/Write-StatusMessage.Tests.ps1 deleted file mode 100644 index c9cc458..0000000 --- a/devsetup/Private/Utils/Write-StatusMessage.Tests.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -BeforeAll { - . $PSScriptRoot\Write-StatusMessage.ps1 - Mock Write-Host { param($Message) } - Mock Write-Verbose { param($Object) } - Mock Write-Debug { param($Object) } - Mock Write-Warning { param($Object) } - Mock Write-Error { param($Object) } -} - -Describe "Write-StatusMessage" { - - Context "When called with default parameters" { - It "Should call Write-Host with the message" { - Write-StatusMessage -Message "Hello" - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "Hello" } - } - } - - Context "When Verbosity is Verbose" { - It "Should call Write-Verbose" { - Write-StatusMessage -Message "Verbose message" -Verbosity "Verbose" - Assert-MockCalled Write-Verbose -Exactly 1 -Scope It - } - } - - Context "When Verbosity is Debug" { - It "Should call Write-Debug" { - Write-StatusMessage -Message "Debug message" -Verbosity "Debug" - Assert-MockCalled Write-Debug -Exactly 1 -Scope It - } - } - - Context "When Verbosity is Warning" { - It "Should call Write-Warning" { - Write-StatusMessage -Message "Warning message" -Verbosity "Warning" - Assert-MockCalled Write-Warning -Exactly 1 -Scope It - } - } - - Context "When Verbosity is Error" { - It "Should call Write-Error" { - Write-StatusMessage -Message "Error message" -Verbosity "Error" - Assert-MockCalled Write-Error -Exactly 1 -Scope It - } - } - - Context "When Indent is specified" { - It "Should indent the message" { - Write-StatusMessage -Message "Indented" -Indent 4 - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq " Indented" } - } - } - - Context "When Width is specified and message is longer" { - It "Should truncate the message with ellipsis" { - Write-StatusMessage -Message "This is a long message" -Width 10 - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "This is..." } - } - } - - Context "When Width is specified and message is shorter" { - It "Should pad the message to the specified width" { - Write-StatusMessage -Message "Short" -Width 10 - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $Object -eq "Short " } - } - } - - Context "When NoNewLine is specified" { - It "Should pass NoNewLine to Write-Host" { - Write-StatusMessage -Message "NoNewLine" -NoNewLine - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $NoNewLine -eq $true } - } - } - - Context "When ForegroundColor is specified" { - It "Should pass ForegroundColor to Write-Host" { - Write-StatusMessage -Message "Color" -ForegroundColor "Green" - Assert-MockCalled Write-Host -Exactly 1 -Scope It -ParameterFilter { $ForegroundColor -eq "Green" } - } - } -} \ No newline at end of file diff --git a/devsetup/Private/Utils/Write-StatusMessage.ps1 b/devsetup/Private/Utils/Write-StatusMessage.ps1 deleted file mode 100644 index 35d6223..0000000 --- a/devsetup/Private/Utils/Write-StatusMessage.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -Function Write-StatusMessage { - [CmdletBinding()] - Param( - [Parameter(Mandatory=$true, Position=0)] - [string]$Message, - [Parameter(Mandatory=$false)] - [string]$ForegroundColor = "Gray", - [Parameter(Mandatory=$false)] - [int]$Indent = 0, - [Parameter(Mandatory=$false)] - [ValidateSet("Default", "Verbose", "Debug", "Warning", "Error")] - [string]$Verbosity = "Default", - [Parameter(Mandatory=$false)] - [int]$Width = 0, - [Parameter(Mandatory=$false)] - [switch]$NoNewLine - ) - - if ($Indent -gt 0) { - $Message = "$(' ' * $Indent)$Message" - } - - if ($Width -gt 0) { - if($Message.Length -gt $Width) { - $Message = $Message.Substring(0, $Width - 3) + "..."; - } else { - $Message = $Message.PadRight($Width, " "); - } - } - - $messageParams = @{ } - - if($Verbosity -eq "Default") { - $messageParams.Object = $Message - $messageParams.ForegroundColor = $ForegroundColor - $messageParams.NoNewLine = $NoNewLine.IsPresent - } else { - $messageParams.Message = $Message - } - #$messageParams.Object = $Message - - switch($Verbosity) { - "Verbose" { - Write-Verbose @messageParams - } - "Debug" { - Write-Debug @messageParams - } - "Warning" { - Write-Warning @messageParams - } - "Error" { - Write-Error @messageParams - } - "Default" { - Write-Host @messageParams - } - } -} \ No newline at end of file diff --git a/devsetup/Public/Use-DevSetup.ps1 b/devsetup/Public/Use-DevSetup.ps1 deleted file mode 100644 index 71f6379..0000000 --- a/devsetup/Public/Use-DevSetup.ps1 +++ /dev/null @@ -1,370 +0,0 @@ -Function Use-DevSetup { - <# - .SYNOPSIS - Manages development environment configurations using the DevSetup module. - - .DESCRIPTION - Use-DevSetup is the main function for managing development environments. It provides actions to install, update, initialize, export, list, and uninstall development environment configurations. - The function supports multiple installation sources including local configurations by name, remote URLs, and local file paths. - - Run 'Use-DevSetup -Init' first to set up the DevSetup environment and initialize the necessary directory structure and configuration files. - - .PARAMETER Install - Installs a development environment from a configuration file. Can be used with Name, Url, or FilePath parameters. - - .PARAMETER Update - Updates an existing development environment configuration. Requires the Name parameter. - - .PARAMETER Init - Initializes the DevSetup environment and sets up the necessary directory structure and configuration files. This should be run first before using other actions. - - .PARAMETER Export - Exports the current development environment to a configuration file. Requires the Name parameter to specify the name for the exported configuration. - - .PARAMETER List - Lists all available development environment configurations. - - .PARAMETER Uninstall - Uninstalls a development environment configuration. Requires the Name parameter. - - .PARAMETER Name - The name of the environment configuration to use. Required for Install, Update, Export, and Uninstall actions when using local configurations. - - .PARAMETER Url - The URL of a remote configuration file to install. Used with the Install action for remote installations. - - .PARAMETER Path - The local file path to a configuration file to install. Used with the Install action for local file installations. - - .PARAMETER Platform - The platform to filter environments by when using the List action. Use "current" (default) to show environments for the current platform, "all" to show all environments, or specify a platform like "Windows", "Linux", "macOS". - - .OUTPUTS - [System.Boolean] - Returns $true if the action completes successfully, $false otherwise. - - .EXAMPLE - Use-DevSetup -Init - - Initializes the DevSetup environment. Run this first to set up the necessary directory structure and configuration files. - - .EXAMPLE - Use-DevSetup -List - - Lists development environment configurations for the current platform. - - .EXAMPLE - Use-DevSetup -List -Platform "all" - - Lists all available development environment configurations regardless of platform. - - .EXAMPLE - Use-DevSetup -List -Platform "Linux" - - Lists development environment configurations specifically for Linux. - - .EXAMPLE - Use-DevSetup -Install -Name "WebDev" - - Installs the development environment using the "WebDev" configuration from local configurations. - - .EXAMPLE - Use-DevSetup -Install -Url "https://raw.githubusercontent.com/user/configs/main/webdev.devsetup" - - Installs a development environment from a remote configuration file URL. - - .EXAMPLE - Use-DevSetup -Install -Path "C:\Configs\MySetup.devsetup" - - Installs a development environment from a local configuration file path. - - .EXAMPLE - Use-DevSetup -Update - - Updates the devsetup system with any new environments or changes. - - .EXAMPLE - Use-DevSetup -Export -Name "MyCurrentSetup" - - Exports the current system's installed packages and tools to a new configuration file named "MyCurrentSetup". - - .EXAMPLE - Use-DevSetup -Uninstall -Name "WebDev" - - Uninstalls all packages and tools associated with the "WebDev" configuration. - - .NOTES - - Run 'Use-DevSetup -Init' first to initialize the DevSetup environment before using other actions - - Only one action can be specified at a time using parameter sets - - Supports three installation methods: - * By Name: Uses local configuration files from the DevSetup directory - * By URL: Downloads and installs from a remote configuration file - * By Path: Installs from a local file path outside the DevSetup directory - - The function validates input and provides appropriate error messages for invalid combinations - - Displays formatted progress headers with color-coded output for better user experience - - Includes comprehensive try-catch error handling with descriptive error messages - - Update and Uninstall actions are marked as TODO and not yet implemented - - .LINK - - .COMPONENT - DevSetup.Public - - .FUNCTIONALITY - Environment Management, Configuration Installation, System Setup -#> - - Param( - [Parameter(Mandatory = $true, ParameterSetName = "Install")] - [Parameter(Mandatory = $true, ParameterSetName = "InstallUrl")] - [Parameter(Mandatory = $true, ParameterSetName = "InstallPath")] - [switch]$Install, - - [Parameter(Mandatory = $true, ParameterSetName = "Update")] - [switch]$Update, - - [Parameter(Mandatory = $true, ParameterSetName = "Init")] - [switch]$Init, - - [Parameter(Mandatory = $true, ParameterSetName = "Export")] - [switch]$Export, - - [Parameter(Mandatory = $true, ParameterSetName = "List")] - [switch]$List, - - [Parameter(Mandatory = $true, ParameterSetName = "Uninstall")] - [switch]$Uninstall, - - [Parameter(Mandatory = $true, ParameterSetName = "Install")] - [Parameter(Mandatory = $true, ParameterSetName = "Export")] - [Parameter(Mandatory = $true, ParameterSetName = "Uninstall")] - [string]$Name, - - [Parameter(Mandatory = $true, ParameterSetName = "InstallUrl")] - [string]$Url, - - [Parameter(Mandatory = $true, ParameterSetName = "InstallPath")] - [string]$Path, - - [Parameter(Mandatory = $false, ParameterSetName = "List")] - [string]$Platform = "current" - ) - - try { - # Determine which action was selected based on parameter set - $selectedAction = $PSCmdlet.ParameterSetName.ToLower() - - - function Repeat-Char($char, $count) { -join (1..$count | ForEach-Object { $char }) } - - # Display fancy action header - #Write-Host "" - #Write-Host "+===============================================================================+" -ForegroundColor Cyan - #Write-Host "|" -ForegroundColor Cyan -NoNewline - #Write-Host " DEVSETUP " -ForegroundColor Yellow -NoNewline - #Write-Host "|" -ForegroundColor Cyan - #Write-Host "|" -ForegroundColor Cyan -NoNewline - #Write-Host " Development Environment Manager " -ForegroundColor White -NoNewline - #Write-Host "|" -ForegroundColor Cyan - #Write-Host "+===============================================================================+" -ForegroundColor Cyan -# Define box drawing characters using [char] codes - $b = [char]0x2588 # █ (full block) - $tl = [char]0x2554 # ╔ (top-left) - $tr = [char]0x2557 # ╗ (top-right) - $bl = [char]0x255A # ╚ (bottom-left) - $br = [char]0x255D # ╝ (bottom-right) - $h = [char]0x2550 # ═ (horizontal) - $v = [char]0x2551 # ║ (vertical) - $ml = [char]0x2560 # - $mr = [char]0x2563 - - $tb = "$tl" + (Repeat-Char $h 118) + "$tr" - $bm = "$ml" + (Repeat-Char $h 118) + "$mr" - $bb = "$bl" + (Repeat-Char $h 118) + "$br" - $sp = "$v" + (Repeat-Char " " 118) + "$v" - - Write-Host "" - Write-Host "$tb" -ForegroundColor Cyan - Write-Host "$sp" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan - - #Write-Host "$v $b$b$tl$h$h$b$b$tr$b$b$tl$h$h$h$h$br$b$b$v $b$b$v$b$b$tl$h$h$h$h$br$b$b$tl$h$h$h$h$br$bl$h$h$b$b$tl$h$h$br$b$b$v $b$b$v$b$b$tl$h$h$b$b$tr $v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$h$h$br" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$h$h$br" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$h$h$br$bl$h$h" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$br" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan - - #Write-Host "$v $b$b$v $b$b$v$b$b$b$b$b$tr $b$b$v $b$b$v$b$b$b$b$b$b$b$tr$b$b$b$b$b$tr $b$b$v $b$b$v $b$b$v$b$b$b$b$b$b$tl$br $v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br" (Repeat-Char " " 23) "$v" -ForegroundColor Cyan - - #Write-Host "$v $b$b$v $b$b$v$b$b$tl$h$h$br $bl$b$b$tr $b$b$tl$br$bl$h$h$h$h$b$b$v$b$b$tl$h$h$br $b$b$v $b$b$v $b$b$v$b$b$tl$h$h$h$br $v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$br $bl" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br$bl$h$h$h$h" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$br " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$h$h$h$br" (Repeat-Char " " 24) "$v" -ForegroundColor Cyan - - #Write-Host "$v $b$b$b$b$b$b$tl$br$b$b$b$b$b$b$b$tr $bl$b$b$b$b$tl$br $b$b$b$b$b$b$b$v$b$b$b$b$b$b$b$tr $b$b$v $bl$b$b$b$b$b$b$tl$br$b$b$v $v" -ForegroundColor Cyan - Write-Host "$v" (Repeat-Char " " 25) -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr $bl" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tr " -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v $bl" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b$b$b$b$b" -ForegroundColor White -NoNewLine - Write-Host "$tl$br" -ForegroundColor Cyan -NoNewLine - Write-Host "$b$b" -ForegroundColor White -NoNewLine - Write-Host "$v" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan - - Write-Host "$v" (Repeat-Char " " 24) "$bl$h$h$h$h$h$br $bl$h$h$h$h$h$h$br $bl$h$h$h$br $bl$h$h$h$h$h$h$br$bl$h$h$h$h$h$h$br $bl$h$br $bl$h$h$h$h$h$br $bl$h$br" (Repeat-Char " " 28) "$v" -ForegroundColor Cyan - - Write-Host "$v" -ForegroundColor Cyan -NoNewline - $version = Get-DevSetupVersion -Local - $versionDisplay = "Development Environment Manager v$version" - $paddedAction = $versionDisplay.PadLeft(($versionDisplay.Length + 118) / 2).PadRight(118) - Write-Host "$paddedAction" -ForegroundColor White -NoNewline - Write-Host "$v" -ForegroundColor Cyan - Write-Host "$sp" -ForegroundColor Cyan - Write-Host "$bm" -ForegroundColor Cyan - - - $actionDisplay = switch ($selectedAction) { - 'install' { ">> INSTALLING Development Environment" } - 'installpath' { ">> INSTALLING Development Environment From Path" } - 'installurl' { ">> INSTALLING Development Environment From Url" } - 'update' { ">> UPDATING DevSetup System" } - 'init' { ">> INITIALIZING DevSetup System" } - 'export' { ">> EXPORTING Current Configuration" } - 'list' { ">> LISTING Available Environments" } - 'uninstall' { ">> UNINSTALLING Development Environment" } - } - - $paddedAction = $actionDisplay.PadLeft(($actionDisplay.Length + 118) / 2).PadRight(118) - Write-Host "$v" -ForegroundColor Cyan -NoNewline - Write-Host "$paddedAction" -ForegroundColor Yellow -NoNewline - Write-Host "$v" -ForegroundColor Cyan - Write-Host "$bb" -ForegroundColor Cyan - Write-Host "" - - switch ($selectedAction) { - {$_ -eq 'install' -or $_ -eq 'installpath' -or $_ -eq 'installurl'} { - Write-Host "Installing development environment..." -ForegroundColor Yellow - $ParameterCopy = [hashtable]$PSBoundParameters - $ParameterCopy.Remove('Install') - Install-DevSetupEnv @ParameterCopy - } - 'update' { - Write-Host "Updating devsetup system..." -ForegroundColor Yellow - Update-DevSetup | Out-Null - } - 'init' { - Write-Host "Initializing DevSetup system..." -ForegroundColor Yellow - Initialize-DevSetup | Out-Null - } - 'export' { - Write-Host "Exporting current development environment..." -ForegroundColor Yellow - Export-DevSetupEnv -Name $Name - } - 'list' { - Show-DevSetupEnvList -Platform $Platform - } - 'uninstall' { - Write-Host "Uninstalling development environment..." -ForegroundColor Yellow - Uninstall-DevSetupEnv -Name $Name - } - } - - #Write-Host "DevSetup action '$selectedAction' completed successfully!" -ForegroundColor Green - } - catch { - Write-Error "Error executing DevSetup action '$selectedAction': $_" - } -} \ No newline at end of file