Public/New-TfvcMigrationConfig.ps1

function New-TfvcMigrationConfig {
    <#
    .SYNOPSIS
        Interactive (or non-interactive) configuration generator for the TFVC-to-GitHub migration tool.
    .DESCRIPTION
        Prompts the user for Azure DevOps Server and GitHub details, tests the connection,
        and writes a config.json file compatible with the migration commands.
    .PARAMETER OutputPath
        Path where the config file will be saved. Defaults to ./config.json.
    .PARAMETER NonInteractive
        When set, skips interactive prompts and uses the values supplied via parameters.
    .PARAMETER ServerUrl
        Azure DevOps Server URL (non-interactive mode).
    .PARAMETER Collection
        TFS collection name (non-interactive mode).
    .PARAMETER Project
        Team project name (non-interactive mode).
    .PARAMETER Pat
        Personal Access Token (non-interactive mode).
    .PARAMETER TfvcPath
        TFVC source path, e.g. $/Project/Folder (non-interactive mode).
    .PARAMETER GitRemoteUrl
        GitHub remote URL (non-interactive mode).
    .PARAMETER OutputDir
        Migration output directory (non-interactive mode).
    .EXAMPLE
        New-TfvcMigrationConfig
    .EXAMPLE
        New-TfvcMigrationConfig -NonInteractive -ServerUrl https://tfs:8080/tfs -Project MyProject -Pat $pat -TfvcPath '$/MyProject/App' -GitRemoteUrl https://github.com/org/repo.git
    #>

    [CmdletBinding()]
    param(
        [string]$OutputPath = './config.json',

        [switch]$NonInteractive,

        [string]$ServerUrl,
        [string]$Collection,
        [string]$Project,
        [string]$Pat,
        [string]$TfvcPath,
        [string]$GitRemoteUrl,
        [string]$OutputDir
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # --- Banner ---
    function Show-Banner {
        Write-Host ''
        Write-Host ' =======================================================╗' -ForegroundColor Cyan
        Write-Host ' | TFVC -> GitHub Migration Configurator |' -ForegroundColor Cyan
        Write-Host ' | |' -ForegroundColor Cyan
        Write-Host ' | Generates a config.json for the migration tool. |' -ForegroundColor Cyan
        Write-Host ' =======================================================╝' -ForegroundColor Cyan
        Write-Host ''
    }

    function Read-Prompt {
        param(
            [Parameter(Mandatory)][string]$Label,
            [string]$Default
        )
        $suffix = if ($Default) { " [$Default]" } else { '' }
        $value = Read-Host " $Label$suffix"
        if ([string]::IsNullOrWhiteSpace($value) -and $Default) { return $Default }
        if ([string]::IsNullOrWhiteSpace($value)) {
            Write-Host " Value is required." -ForegroundColor Yellow
            return Read-Prompt -Label $Label -Default $Default
        }
        $value
    }

    function Read-SecurePat {
        $secure = Read-Host ' Personal Access Token (PAT)' -AsSecureString
        $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
        try { [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
        finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
    }

    function Test-TfvcConnection {
        param(
            [Parameter(Mandatory)][hashtable]$Connection
        )
        Write-Host ''
        Write-Host ' Testing connection...' -ForegroundColor Yellow
        try {
            $cs = Get-TfvcChangesets -Connection $Connection -Top 1
            $count = @($cs).Count
            Write-Host " [+] Connection successful - retrieved $count changeset(s)." -ForegroundColor Green
            return $true
        }
        catch {
            Write-Host " [x] Connection failed: $($_.Exception.Message)" -ForegroundColor Red
            return $false
        }
    }

    function Show-ConfigSummary {
        param([hashtable]$Config)
        Write-Host ''
        Write-Host ' ┌------------------─ Configuration Summary ------------------─┐' -ForegroundColor DarkCyan
        Write-Host " | Server URL : $($Config.adoServerUrl)" -ForegroundColor DarkCyan
        Write-Host " | Collection : $($Config.collection)" -ForegroundColor DarkCyan
        Write-Host " | Project : $($Config.project)" -ForegroundColor DarkCyan
        Write-Host " | API Version : $($Config.apiVersion)" -ForegroundColor DarkCyan
        $authDisplay = if ($Config.pat -and $Config.pat.Length -ge 4) { "****" + (($Config.pat)[-4..-1] -join '') } elseif ($Config.pat) { "****" } else { "(Windows Auth)" }
        Write-Host " | Auth : $authDisplay" -ForegroundColor DarkCyan
        Write-Host " | Source Mappings:" -ForegroundColor DarkCyan
        foreach ($m in $Config.sourceMappings) {
            $dest = if ($m.destinationPath) { $m.destinationPath } else { '(root)' }
            Write-Host " | $($m.tfvcPath) -> $dest" -ForegroundColor DarkCyan
        }
        Write-Host " | Git Remote : $($Config.gitRemoteUrl)" -ForegroundColor DarkCyan
        Write-Host " | Output Dir : $($Config.outputDir)" -ForegroundColor DarkCyan
        Write-Host " | LFS Threshold : $([math]::Round($Config.lfsThresholdBytes / 1MB))MB" -ForegroundColor DarkCyan
        Write-Host " | LFS Patterns : $($Config.lfsPatterns -join ', ')" -ForegroundColor DarkCyan
        Write-Host " | Config File : $OutputPath" -ForegroundColor DarkCyan
        Write-Host ' └------------------------------------------------------------─┘' -ForegroundColor DarkCyan
        Write-Host ''
    }

    # --- Main ---

    Show-Banner

    if ($NonInteractive) {
        # -- Non-interactive mode --
        $requiredParams = @{
            ServerUrl    = $ServerUrl
            Project      = $Project
            Pat          = $Pat
            TfvcPath     = $TfvcPath
            GitRemoteUrl = $GitRemoteUrl
        }
        foreach ($kv in $requiredParams.GetEnumerator()) {
            if ([string]::IsNullOrWhiteSpace($kv.Value)) {
                throw "Parameter -$($kv.Key) is required in non-interactive mode."
            }
        }

        $cfgCollection  = if ($Collection) { $Collection } else { 'DefaultCollection' }
        $cfgOutputDir   = if ($OutputDir)  { $OutputDir }  else { './migration-output' }

        $config = @{
            adoServerUrl      = $ServerUrl
            collection        = $cfgCollection
            project           = $Project
            apiVersion        = '7.0'
            pat               = $Pat
            sourceMappings    = @(
                @{ tfvcPath = $TfvcPath; destinationPath = '' }
            )
            gitRemoteUrl      = $GitRemoteUrl
            outputDir         = $cfgOutputDir
            lfsThresholdBytes = 52428800
            lfsPatterns       = @('*.dll', '*.exe', '*.zip', '*.nupkg')
        }
    }
    else {
        # -- Interactive mode --
        Write-Host ' Answer each prompt to generate your migration config.' -ForegroundColor Gray
        Write-Host ' Values in [brackets] are defaults - press Enter to accept.' -ForegroundColor Gray
        Write-Host ''

        # 1. Server connection
        Write-Host ' -- Azure DevOps Server --' -ForegroundColor White
        $cfgServerUrl  = Read-Prompt -Label 'ADO Server URL (e.g. https://tfs.company.com:8080/tfs)'
        $cfgCollection = Read-Prompt -Label 'Collection' -Default 'DefaultCollection'
        $cfgProject    = Read-Prompt -Label 'Project'
        $cfgApiVersion = Read-Prompt -Label 'API Version' -Default '7.0'

        # 2. PAT (secure entry)
        Write-Host ''
        Write-Host ' -- Authentication --' -ForegroundColor White
        $cfgPat = Read-SecurePat

        # 3. Source mappings (loop)
        Write-Host ''
        Write-Host ' -- Source Mappings --' -ForegroundColor White
        Write-Host ' Map one or more TFVC folders to destinations in the Git repo.' -ForegroundColor Gray
        $mappings = [System.Collections.Generic.List[hashtable]]::new()
        do {
            $tfvc = Read-Prompt -Label 'TFVC path (e.g. $/Project/Folder)'
            $dest = Read-Host ' Destination path in Git repo (empty = repo root) []'
            if ([string]::IsNullOrWhiteSpace($dest)) { $dest = '' }
            $mappings.Add(@{ tfvcPath = $tfvc; destinationPath = $dest })
            $more = Read-Host ' Add another mapping? (y/n) [n]'
        } while ($more -eq 'y' -or $more -eq 'Y')

        # 4. Git remote
        Write-Host ''
        Write-Host ' -- GitHub --' -ForegroundColor White
        $cfgGitRemote = Read-Prompt -Label 'Git remote URL (e.g. https://github.com/org/repo.git)'

        # 5. Output directory
        Write-Host ''
        Write-Host ' -- Output --' -ForegroundColor White
        $cfgOutputDir = Read-Prompt -Label 'Output directory' -Default './migration-output'

        # 6. LFS settings
        Write-Host ''
        Write-Host ' -- Git LFS --' -ForegroundColor White
        $lfsRaw = Read-Prompt -Label 'LFS threshold (e.g. 50MB)' -Default '50MB'
        # Parse human-friendly size (e.g. "50MB", "100MB")
        if ($lfsRaw -match '^\s*(\d+)\s*MB\s*$') {
            $cfgLfsBytes = [int]$Matches[1] * 1MB
        }
        elseif ($lfsRaw -match '^\s*(\d+)\s*$') {
            $cfgLfsBytes = [int]$Matches[1]
        }
        else {
            Write-Host " Could not parse '$lfsRaw', using default 50MB." -ForegroundColor Yellow
            $cfgLfsBytes = 52428800
        }
        $lfsPatRaw = Read-Prompt -Label 'LFS patterns (comma-separated)' -Default '*.dll,*.exe,*.zip,*.nupkg'
        $cfgLfsPatterns = $lfsPatRaw -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }

        $config = @{
            adoServerUrl      = $cfgServerUrl
            collection        = $cfgCollection
            project           = $cfgProject
            apiVersion        = $cfgApiVersion
            pat               = $cfgPat
            sourceMappings    = @($mappings)
            gitRemoteUrl      = $cfgGitRemote
            outputDir         = $cfgOutputDir
            lfsThresholdBytes = $cfgLfsBytes
            lfsPatterns       = @($cfgLfsPatterns)
        }
    }

    # -- Test connection --
    $conn = New-TfvcConnection `
        -ServerUrl  $config.adoServerUrl `
        -Collection $config.collection `
        -Project    $config.project `
        -Pat        $config.pat `
        -ApiVersion $config.apiVersion

    $ok = Test-TfvcConnection -Connection $conn

    if (-not $ok) {
        Write-Host ''
        Write-Host ' Connection test failed. Config was NOT saved.' -ForegroundColor Red
        Write-Host ' Please verify your Server URL, Collection, Project, and PAT.' -ForegroundColor Red
        return
    }

    # -- Save config --
    $json = $config | ConvertTo-Json -Depth 5
    $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
    $dir = Split-Path $resolvedPath -Parent
    if ($dir -and -not (Test-Path $dir)) {
        New-Item -Path $dir -ItemType Directory -Force | Out-Null
    }
    $json | Set-Content -Path $resolvedPath -Encoding UTF8

    Write-Host " [+] Config saved to: $resolvedPath" -ForegroundColor Green

    Show-ConfigSummary -Config $config
}