Copy-ToModulesDirectory.psm1

#requires -version 5
<#
.SYNOPSIS
  Copy modules to the PowerShell Module Directory
 
.DESCRIPTION
  Makes it a bit easier to copy the modules you are working on, to the PowerShell modules directory
  on the same machine. It tries to fix the most annoying problems that I have encountered during use,
  such as missing paths, writing restrictions, name conflict, absent manifest file etc.
 
  Short alias "ctmd" is also available.
 
.INPUTS
  None
 
.OUTPUTS
  None
 
.EXAMPLE
  PS C:\> Copy-ToModulesDirectory -Directory "Stop-CpuEaters" -Force
  True
 
  PS C:\> Copy-ToModulesDirectory -Directory "Join-Object"
  True
 
  PS C:\> Copy-ToModulesDirectory -Directory "Write-List", "ConvertTo-Object", "Invoke-DownloadFile" -Force
  True
  True
  True
 
#>


function Write-ModuleDirectoryStructure {
    write-host
    write-host "A valid module has to have a structure:"
    write-host
    write-host " SomeModule - a directory, containing two mandatory files with matching names"
    write-host " |-----SomeModule.psm1 - a script, containing the module logic and ending with 'Export-ModuleMember' command"
    write-host " +-----SomeModule.psd1 - a manifest, containing the metadata of the module"
    write-host
}

function Write-ManifestCreationHelp {
    param(
        [string]$RelativePsdPath,
        [string]$RelativePsmPath,
        [string]$ModuleBaseName
    )

    $username = $ENV:USERNAME

    write-host
    write-host "You can create the manifest with a command like:"
    write-host " New-ModuleManifest -Path $RelativePsdPath -RootModule $RelativePsmPath -FunctionsToExport $ModuleBaseName -Author '$username'"
    write-host
}

function CreateManifestFile {
    param(
        [string]$PsdPath,
        [string]$PsmPath,
        [string]$ModuleBaseName
    )
    write-debug "Going to create the manifest"

    $username = $ENV:USERNAME
    $company = $ENV:COMPUTERNAME
    $copyright = "(c) {0} {1}. All rights reserved." -f (get-date -Format yyyy), $username
    $description = "This manifest was created automatically by Copy-ToModulesDirectory module"

    New-ModuleManifest -Path $PsdPath -RootModule $PsmPath -Author $username -CompanyName $company -Copyright $copyright -FunctionsToExport $ModuleBaseName -Description $description
}


function Copy-ToModulesDirectory {
    [alias("ctmd")]

    [CmdletBinding()]
    param(
        [Parameter(Position = 1, Mandatory)]
        [string[]]$Directory,

        [switch]$Force

    )

    begin {

        # obtain the user's modules directory to use as the target
        $modulesSystemDirectory = $ENV:PSModulePath -split ';' | Where-Object { $_ -like "$home*" } | Select-Object -first 1

        # if the user's modules directory is not accessible, get the one in Program Files
        if ($modulesSystemDirectory.count -eq 0) {
            write-debug "The User's profile has no PowerShell Modules Directories registered"
            $modulesSystemDirectory = $ENV:PSModulePath -split ';' | Where-Object { $_ -ne '$env:ProgramFiles*' } | Select-Object -first 1
            if ($modulesSystemDirectory.count -eq 0) {
                write-debug "No PowerShell Modules Directories registered in '$env:ProgramFiles'"
                $modulesSystemDirectory = $ENV:PSModulePath -split ';' | Select-Object -first 1
            }
        }

        if ($modulesSystemDirectory.count -eq 0) {
            write-host
            write-host "No PowerShell Modules Directories were found"
            write-host
            throw "The system critical path is missing"
        }

        # is it accessible?
        if (-not(test-path $modulesSystemDirectory -PathType Container)) {
            # try to create it
            mkdir $modulesSystemDirectory -Force -ErrorAction Continue
            if (-not $?) {
                Write-Host
                write-host "PowerShell Modules Directory doesn't exist AND couldn't be created"
                Write-Host
                throw "The system critical path doesn't exist"
            }
        }

        # is it writable?
        $testPath = Join-Path $modulesSystemDirectory "test.txt"
        out-file -InputObject "test" -FilePath $testPath -ErrorAction Continue
        if (-not $?) {
            write-host
            write-host "Write Access test failed: can't create a file in the directory '$modulesSystemDirectory'"
            write-host
            throw "The system critical directory is not writable"
        }
        Remove-Item $testPath -Force

        write-debug "PowerShell Modules Directory: $modulesSystemDirectory"

    }


    process {

        foreach ($d in $Directory) {

            write-debug "LOOP STARTED: $d"

            # is it a directory?
            if (-not (test-path $d -PathType Container)) {
                write-host
                write-host "You must specify a path to the module's directory, not the file or anything else."
                Write-ModuleDirectoryStructure
                throw "The path is invalid"
            }

            $sourceDirectory = Resolve-Path $d | Select-Object -ExpandProperty Path
            write-debug "Resolved path: $sourceDirectory"

            # are there the module files with matching names?
            $moduleParentDirectory = Split-Path $sourceDirectory -Parent
            $moduleBaseName = Split-Path $sourceDirectory -Leaf

            $psmName = "$moduleBaseName.psm1"
            $psmPath = join-path $sourceDirectory $psmName
            $relativePsmPath = resolve-path $psmPath -Relative -ErrorAction Ignore

            if (-not (test-path $psmPath -Type Leaf)) {
                write-host
                write-host "The file '$psmName' was expected in the directory '$sourceDirectory'"
                Write-ModuleDirectoryStructure
                throw "A mandatory file is missing"
            }

            $psdName = "$moduleBaseName.psd1"
            $psdPath = join-path $Directory $psdName
            $relativePsdPath = resolve-path $psdPath -Relative -ErrorAction Ignore

            if (-not (test-path $psdPath -Type Leaf)) {

                write-debug "No manifest at '$relativePsdPath'"

                if ($Force) {
                    # create the manifest for the user
                    CreateManifestFile -PsdPath $psdPath -PsmPath $psmPath -ModuleBaseName $moduleBaseName
                }

                if (-not (test-path $psdPath -Type Leaf)) {

                    if ($Force) {
                        write-debug "It seems my attempt to generate the manifest file has failed."
                    }

                    # give a hint and fail
                    write-host
                    write-host "A module must have a manifest file, but I failed to create it."
                    Write-ManifestCreationHelp
                    throw "The manifest file is missing"
                }
            }

            # does the target directory contain a subdir with the same name?
            $targetDirectory = join-path $modulesSystemDirectory $moduleBaseName
            $targetTempName = "{0}-{1}" -f $moduleBaseName, (Get-Date -Format HH-mm-ss-fff)
            $targetTempDirectory = join-path $modulesSystemDirectory $targetTempName
            if (test-path ($targetDirectory) -Type Container) {
                if (-not $Force) {
                    write-host
                    write-host "The directory '$targetDirectory' already exists."
                    write-host
                    write-host "Possible solutions:"
                    write-host "1. Delete it using the command:"
                    write-host " Remove-Item -Recurse -Path $targetDirectory"
                    write-host
                    write-host "2. Run this command again with the -Force argument:"
                    write-host " Copy-ToModulesDirectory -Directory $d -Force"
                    write-host
                    throw "The target folder already exists"
                }
                else {
                    write-debug "PowerShell Modules Directory already has a directory named '$moduleBaseName'"
                    write-debug "FORCE MODE: going to fix it"
                    write-debug "Rename '$moduleBaseName' => '$targetTempName'"
                    Move-Item $targetDirectory $targetTempDirectory -Force -ErrorAction Continue
                    if (-not $?) {
                        Remove-Item $targetTempDirectory -Recurse -Force -ErrorAction Stop
                        write-host
                        write-host "Failed to rename the directory '$targetDirectory'."
                        write-host
                        write-host "Possible fixes:"
                        write-host "a. Check the user rights"
                        write-host "b. Try to execute this command in elevated environment (as Administrator)"
                        write-host "c. Ensure this directory is not opened in editors etc."
                        write-host "d. Check the path for invalid characters"
                        write-host
                        throw "Failed to rename a folder"
                    }
                    else {

                        write-debug "The conflicting directory has been renamed to '$targetTempDirectory'"
                    }

                }
            }

            # finally, copy it
            write-debug "Copy the module '$moduleBaseName' to the PowerShell Module Directory"
            Copy-Item $sourceDirectory $modulesSystemDirectory -Recurse -ErrorAction Continue
            if ($?) {

                write-debug "Copied successfully"

                # if the copy succeed, and there is a temp directory exists, remove it
                if (test-path $targetTempDirectory -type Container) {
                    write-debug "Delete the directory $targetTempDirectory"
                    Remove-Item $targetTempDirectory -Recurse -Force -ErrorAction Continue
                    if (-not $?) {
                        write-host
                        write-host "Failed to remove the temporary folder '$targetTempDirectory'"
                        Write-Host
                        throw "Failed to delete a folder"
                    }

                }

            }
            else {
                write-debug "The copy has failed"
                # if the copy failed: remove debris, rename the temp folder back
                if (test-path ($targetDirectory) -Type Container) {
                    write-debug "Removing unfinished files"
                    Remove-Item $targetDirectory -Recurse -Force -ErrorAction Stop
                }

                if (test-path ($targetTempDirectory) -Type Container) {
                    write-debug "Restoring the previous contents"
                    Remove-Item $targetDirectory -Recurse -Force -ErrorAction Stop
                }
            }

            write-debug "Return True if '$moduleBaseName' is found in PowerShell Modules Directory; otherwise return False"

            Test-Path $targetDirectory -PathType Container -ErrorAction Continue

        } #foreach

    } # process
}

# if not specified, all functions will be exported
Export-ModuleMember -Function 'Copy-ToModulesDirectory' -Alias "ctmd"