Netscoot.Core/Public/Move-PowerShellModule.ps1

function Move-PowerShellModule {
    <#
    .SYNOPSIS
        Move a PowerShell module folder and reconcile its manifest, delegating manifest
        edits to Update-ModuleManifest rather than hand-editing the .psd1.
 
    .DESCRIPTION
        Moves a module directory (git mv when tracked), then rewrites RootModule,
        NestedModules and FileList in the .psd1 via Update-ModuleManifest so relative
        references stay valid. Validates the result with Test-ModuleManifest.
 
        Limits (warned, not fixed): Dot-sourced relative paths inside .psm1/.ps1 files,
        and any path computed at runtime, cannot be reconciled automatically.
 
    .PARAMETER ModulePath
        Path to the module folder, or directly to its .psd1 manifest.
 
    .PARAMETER Destination
        Where to move the module folder, following `git mv` rules: An existing directory means move
        into it (keeping the name); otherwise it is the module's new folder path.
 
    .PARAMETER Force
        Proceed with a plain file move when git is unavailable instead of aborting. The plain move is a PowerShell `Move-Item` (same on every platform) and does not preserve git history.
 
    .PARAMETER NoJournal
        Skip recording this move in the undo journal for this call, even when journaling is enabled
        (Undo-Netscoot will not see this move).
 
    .OUTPUTS
        Netscoot.PSModuleMoveResult
 
    .EXAMPLE
        # Preview; reconciles RootModule/NestedModules/FileList via Update-ModuleManifest
        Move-PowerShellModule -ModulePath ./tools/Mayo -Destination ./modules/Mayo -WhatIf
        # Move it for real
        Move-PowerShellModule -ModulePath ./tools/Mayo -Destination ./modules/Mayo
        # Point at the .psd1 instead of the folder - same result
        Move-PowerShellModule -ModulePath ./tools/Mayo/Mayo.psd1 -Destination ./modules/Mayo
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType('Netscoot.PSModuleMoveResult')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'Path', 'PSPath')]
        [ValidateNotNullOrEmpty()]
        [string]$ModulePath,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Destination,
        [switch]$Force,
        [switch]$NoJournal
    )

    process {
    $src = Resolve-FullPath $ModulePath
    if ($src -match '\.psd1$') {
        $manifestName = Split-Path -Leaf $src
        $moduleDir    = Split-Path -Parent $src
    } else {
        $moduleDir = $src
        $manifest  = Get-ChildItem -LiteralPath $moduleDir -Filter '*.psd1' | Select-Object -First 1
        if (-not $manifest) { throw "No .psd1 manifest found in $moduleDir" }
        $manifestName = $manifest.Name
    }

    # git mv semantics: an existing destination directory means "move the module folder into it";
    # otherwise Destination is the module's new folder path.
    $newDir = Resolve-MoveTarget -Source $moduleDir -Destination $Destination
    if (Test-Path -LiteralPath $newDir) { throw "Destination already exists: $newDir" }

    Write-Verbose "Plan: move module $manifestName $moduleDir -> $newDir"
    $newManifest = Join-Path $newDir $manifestName

    $performed = $false
    $skippedCount = 0

    if ($PSCmdlet.ShouldProcess("$moduleDir -> $newDir", 'Move PowerShell module and reconcile manifest')) {
        $ctx = Resolve-MoveContext -Cmdlet $PSCmdlet -Force:$Force -TargetForError $moduleDir
        if (-not $ctx) { return }

        # The manifest refresh + validate happens after the move (reads the new layout).
        $manifestFix = {
            param($NewDir, $NewManifest)
            $files = Get-ChildItem -LiteralPath $NewDir -Recurse -File |
                ForEach-Object { $_.FullName.Substring($NewDir.Length).TrimStart('\', '/') }
            try { Update-ModuleManifest -Path $NewManifest -FileList $files; Write-Verbose "Manifest FileList refreshed ($($files.Count) files)." }
            catch { Write-Warning "Update-ModuleManifest failed: $_" }
            $r = Test-ModuleManifest -Path $NewManifest -ErrorAction SilentlyContinue
            if ($r) { Write-Verbose "Test-ModuleManifest OK: $($r.Name) $($r.Version)" }
            else { Write-Warning "Test-ModuleManifest reported problems for $NewManifest" }
        }
        $items = @( New-MoveItem -Description "refresh manifest $manifestName (FileList + validate)" -Reattach $manifestFix -ReattachArgs @($newDir, $newManifest) )

        $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository }
        $repoRoot = Get-RepositoryRoot -StartPath $moduleDir
        $planResult = Invoke-MovePlan -Caption "Move module $manifestName" -Items $items -Move $move `
            -MoveArgs @($ctx.UseGit, $moduleDir, $newDir, $repoRoot) `
            -RepositoryRoot $repoRoot -Command 'Move-PowerShellModule' -Engine 'powershell' -Source $moduleDir -Destination $newDir `
            -UndoParams @{ ModulePath = $newDir; Destination = $moduleDir; Force = [bool]$Force } -NoJournal:$NoJournal
        $performed = $true
        $skippedCount = $planResult.Skipped

        Write-Warning "Reminder: dot-sourced relative paths inside .psm1/.ps1 are not auto-fixed. Grep the module for '. \$PSScriptRoot' style references if depth changed."
    }

    New-MoveResult -TypeName 'Netscoot.PSModuleMoveResult' -Engine 'powershell' -Source $moduleDir -Destination $newDir `
        -Performed $performed -SkippedCount $skippedCount -Extra @{ Manifest = $manifestName }
    }
}