Public/Initialize-UserAdminModule.ps1

#requires -Version 5.1
function Initialize-UserAdminModule {
    <#
    .SYNOPSIS
        One-time setup that configures UserAdminModule for a new administrator.
 
    .DESCRIPTION
        Sets up the UserAdminModule framework for first use by:
          - Storing the custom submodules path in $env:APPDATA\UserAdminModule\config.json
          - Creating the custom path directory if it does not already exist
          - Optionally writing 'Import-Module UserAdminModule' to $PROFILE
 
        Once configured, Import-PersonalModules and Invoke-PersonalModulesMenu will
        automatically discover any submodules you create under the configured path.
        Use New-PSM1Module to scaffold your first submodule.
 
        Reference: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/import-module
 
    .PARAMETER Path
        The folder where your custom submodule folders will be stored.
        Example: C:\MyModules
 
    .PARAMETER UpdateProfile
        If specified, appends 'Import-Module UserAdminModule' to the current user's
        PowerShell profile ($PROFILE). A .bak backup of the existing profile is
        created before any change is made.
 
    .PARAMETER UseSharedProfile
        When combined with -UpdateProfile, writes a dot-source line for the bundled
        SharedPowershellProfile.ps1 (PS 7+) or SharedWindowsPowershellProfile.ps1
        (PS 5.1) instead of a plain Import-Module line. The correct file is chosen
        automatically based on $PSEdition. The shared profile configures the full
        shell UX: admin prompt, console sizing, PSReadLine prediction, greeting,
        and startup timer.
 
    .EXAMPLE
        Initialize-UserAdminModule -Path 'C:\MyModules'
 
        Configures the custom modules path. Submodules created here are auto-discovered
        by Import-PersonalModules and Invoke-PersonalModulesMenu.
 
    .EXAMPLE
        Initialize-UserAdminModule -Path 'C:\MyModules' -UpdateProfile
 
        Configures the custom path and adds the Import-Module line to $PROFILE so
        UserAdminModule loads automatically in every new session.
 
    .EXAMPLE
        Initialize-UserAdminModule -Path 'C:\MyModules' -UpdateProfile -UseSharedProfile
 
        Configures the custom path and writes the full shared profile dot-source line
        to $PROFILE. On PS 7+ this loads SharedPowershellProfile.ps1; on PS 5.1 it
        loads SharedWindowsPowershellProfile.ps1. Both configure the admin prompt,
        console sizing, PSReadLine prediction, greeting, and startup timer.
 
    .EXAMPLE
        Initialize-UserAdminModule -Path 'C:\MyModules' -WhatIf
 
        Shows what would happen without making any changes.
 
    .OUTPUTS
        System.Management.Automation.PSCustomObject
 
        Properties: CustomModulesPath, ConfigPath, ProfileUpdated
 
    .NOTES
        Author: Luke Leigh
        Config: $env:APPDATA\UserAdminModule\config.json
        Tested on: PowerShell 5.1 and 7+
 
        Reference: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/import-module
 
    .LINK
        Get-UserAdminModuleConfig
        Import-PersonalModules
        New-PSM1Module
        Invoke-PersonalModulesMenu
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0, HelpMessage = 'Path to store your custom submodule folders.')]
        [ValidateNotNullOrEmpty()]
        [string]$Path,

        [Parameter()]
        [switch]$UpdateProfile,

        [Parameter()]
        [switch]$UseSharedProfile
    )

    begin {
        trap {
            Write-Error "Failed to initialise UserAdminModule: $_"
            break
        }

        $configDir  = Join-Path $env:APPDATA 'UserAdminModule'
        $configPath = Join-Path $configDir 'config.json'
    }

    process {
        trap {
            Write-Error "Failed during UserAdminModule initialisation: $_"
            continue
        }

        # 1 — Resolve and create the custom modules directory
        $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)

        if (-not (Test-Path $resolvedPath)) {
            if ($PSCmdlet.ShouldProcess($resolvedPath, 'Create custom modules directory')) {
                New-Item -Path $resolvedPath -ItemType Directory -Force | Out-Null
                Write-Verbose "Created custom modules directory: $($resolvedPath)"
            }
        }
        else {
            Write-Verbose "Custom modules directory already exists: $($resolvedPath)"
        }

        # 1a — Resolve shared profile path NOW using $Script:UAMModuleRoot.
        # $Script:UAMModuleRoot is set to $PSScriptRoot in UserAdminModule.psm1 at
        # load time — always a single string regardless of how many module instances
        # are in the session. Fallback: Split-Path $PSScriptRoot -Parent walks up
        # from Public\ to the module root.
        $sharedProfilePath = $null
        if ($UseSharedProfile) {
            $_uamRoot = if ($Script:UAMModuleRoot) {
                $Script:UAMModuleRoot
            } else {
                Split-Path $PSScriptRoot -Parent
            }
            $_sharedFile = if ($PSEdition -eq 'Desktop') {
                'SharedWindowsPowershellProfile.ps1'
            } else {
                'SharedPowershellProfile.ps1'
            }
            $sharedProfilePath = Join-Path $_uamRoot "profiles\$($_sharedFile)"
            Write-Verbose "Resolved shared profile path: $($sharedProfilePath)"
            if (-not (Test-Path $sharedProfilePath)) {
                Write-Warning "Shared profile not found at: $($sharedProfilePath). -UseSharedProfile will be skipped."
                $sharedProfilePath = $null
            }
        }

        # 2 — Create config directory and write config (includes SharedProfilePath when set)
        if (-not (Test-Path $configDir)) {
            New-Item -Path $configDir -ItemType Directory -Force | Out-Null
        }

        $config = [PSCustomObject]@{
            CustomModulesPath = $resolvedPath
            ConfigVersion     = '1.0'
            SharedProfilePath = $sharedProfilePath
        }

        if ($PSCmdlet.ShouldProcess($configPath, 'Write UserAdminModule config')) {
            $config | ConvertTo-Json | Set-Content -Path $configPath -Encoding UTF8 -Force
            Write-Verbose "Config written to: $($configPath)"
        }

        # 3 — Optionally update $PROFILE
        $profileUpdated = $false

        if ($UpdateProfile) {
            $profilePath = $PROFILE

            # Build the profile line to write.
            # -UseSharedProfile: writes a block that reads SharedProfilePath from
            # config.json at session startup. No hardcoded paths, no PSModulePath
            # dependency. Re-running Initialize-UserAdminModule updates config.json
            # and the new path is picked up automatically in the next session.
            if ($UseSharedProfile -and $sharedProfilePath) {
                $importLine = @'
 
# UserAdminModule shared profile — loaded via config.json (update by re-running Initialize-UserAdminModule)
$_uamCfg = Join-Path $env:APPDATA 'UserAdminModule\config.json'
if (Test-Path $_uamCfg) {
    $_uamShared = (Get-Content $_uamCfg -Raw | ConvertFrom-Json).SharedProfilePath
    if ($_uamShared -and (Test-Path $_uamShared)) { . $_uamShared }
}
Remove-Variable _uamCfg, _uamShared -ErrorAction SilentlyContinue
'@

                $_matchPattern = 'UserAdminModule shared profile|SharedPowershellProfile|SharedWindowsPowershellProfile'
            } else {
                $importLine    = 'Import-Module UserAdminModule -ErrorAction SilentlyContinue'
                $_matchPattern = 'Import-Module UserAdminModule'
            }

            # Ensure profile file exists
            if (-not (Test-Path $profilePath)) {
                if ($PSCmdlet.ShouldProcess($profilePath, 'Create PowerShell profile file')) {
                    $profileDir = Split-Path $profilePath -Parent
                    if (-not (Test-Path $profileDir)) {
                        New-Item -Path $profileDir -ItemType Directory -Force | Out-Null
                    }
                    New-Item -Path $profilePath -ItemType File -Force | Out-Null
                    Write-Verbose "Created profile file: $($profilePath)"
                }
            }

            $existingContent = Get-Content -Path $profilePath -Raw -ErrorAction SilentlyContinue
            # Treat null/empty as empty string
            if (-not $existingContent) { $existingContent = '' }

            Write-Verbose "Profile path: $($profilePath)"
            Write-Verbose "Profile writable: $(try { [System.IO.File]::OpenWrite($profilePath).Close(); 'Yes' } catch { 'NO - ' + $_.Exception.Message })"

            if ($existingContent -notmatch $_matchPattern) {
                if ($PSCmdlet.ShouldProcess($profilePath, "Add profile line to $($profilePath)")) {
                    # Backup first
                    $backupPath = "$profilePath.bak"
                    Copy-Item -Path $profilePath -Destination $backupPath -Force -ErrorAction SilentlyContinue
                    Write-Verbose "Profile backup created at: $($backupPath)"

                    # Read-modify-write: build complete new content and write entire file back.
                    # Avoids Add-Content encoding and buffering issues.
                    $separator = if ($existingContent.Length -gt 0) { "`n`n" } else { '' }
                    $newContent = $existingContent.TrimEnd() + $separator + $importLine + "`n"

                    [System.IO.File]::WriteAllText($profilePath, $newContent, [System.Text.Encoding]::UTF8)

                    # Verify the write actually worked
                    $verify = Get-Content -Path $profilePath -Raw -ErrorAction SilentlyContinue
                    if ($verify -and $verify -match [regex]::Escape('UserAdminModule')) {
                        Write-Verbose "Profile write verified: $($profilePath)"
                        $profileUpdated = $true
                    }
                    else {
                        Write-Warning "Profile write to $($profilePath) could not be verified. Check file permissions and try again."
                        # Restore backup
                        Copy-Item -Path $backupPath -Destination $profilePath -Force -ErrorAction SilentlyContinue
                    }
                }
            }
            else {
                Write-Verbose 'Profile already contains a UserAdminModule entry — no change made.'
                $profileUpdated = $true
            }
        }

        # 4 — Return result object
        [PSCustomObject]@{
            CustomModulesPath = $resolvedPath
            ConfigPath        = $configPath
            ProfileUpdated    = $profileUpdated
        }
    }

    end {
        trap {
            Write-Error "Failed in Initialize-UserAdminModule end block: $_"
            continue
        }

        Write-Information "UserAdminModule configured. Custom modules path: $($resolvedPath)" -InformationAction Continue
        Write-Information "Next: use New-PSM1Module -folderPath '$($resolvedPath)\MyCategory' to scaffold your first submodule." -InformationAction Continue

        if ($UpdateProfile -and -not $UseSharedProfile) {
            Write-Information 'Tip: re-run with -UseSharedProfile to configure the full shell UX (admin prompt, greeting, PSReadLine prediction). Example: Initialize-UserAdminModule -Path <path> -UpdateProfile -UseSharedProfile' -InformationAction Continue
        }

        Write-Verbose 'Initialize-UserAdminModule completed.'
    }
}