Modules/businessdev.ALbuild.Apps/Public/Install-BcAlTool.ps1

function Install-BcAlTool {
    <#
    .SYNOPSIS
        Ensures the cross-platform AL Tool CLI is installed (as a .NET global tool) and on PATH.
 
    .DESCRIPTION
        Installs 'Microsoft.Dynamics.BusinessCentral.Development.Tools' as a dotnet global tool so the
        'al' command (which wraps the AL compiler, alc) is available to Invoke-BcCompiler's AlTool
        engine on a build agent. Idempotent: when the AL Tool is already on PATH it does nothing
        (unless -Force). Requires the .NET SDK ('dotnet'). After installing it adds the global-tools
        folder (~/.dotnet/tools) to PATH for the current session so the freshly installed 'al' is
        immediately resolvable.
 
    .PARAMETER PackageId
        The dotnet tool package id. Default 'Microsoft.Dynamics.BusinessCentral.Development.Tools'.
 
    .PARAMETER Version
        Optional specific version to pin; otherwise the latest is installed.
 
    .PARAMETER Force
        Reinstall/update even when the AL Tool is already available.
 
    .PARAMETER DotNetExecutable
        The .NET CLI executable. Default 'dotnet'.
 
    .EXAMPLE
        Install-BcAlTool
 
    .OUTPUTS
        PSCustomObject: Installed (bool), Path, Version.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [string] $PackageId = 'Microsoft.Dynamics.BusinessCentral.Development.Tools',
        [string] $Version,
        [switch] $Force,
        [string] $DotNetExecutable = 'dotnet'
    )

    # Make the per-user global-tools folder resolvable for this session (it is not always on PATH on
    # a fresh agent right after install).
    $toolsDir = if ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet\tools' } else { Join-Path $HOME '.dotnet/tools' }
    $ensurePath = {
        if ((Test-Path -LiteralPath $toolsDir) -and (($env:PATH -split [System.IO.Path]::PathSeparator) -notcontains $toolsDir)) {
            $env:PATH = $toolsDir + [System.IO.Path]::PathSeparator + $env:PATH
        }
    }
    & $ensurePath

    $existing = @('al', 'altool', 'alc') | ForEach-Object { Get-Command -Name $_ -ErrorAction SilentlyContinue } | Select-Object -First 1
    if ($existing -and -not $Force) {
        Write-ALbuildLog "AL Tool already available: $($existing.Source)."
        return [PSCustomObject]@{ Installed = $false; Path = $existing.Source; Version = $existing.Version }
    }

    $dotnet = Get-Command -Name $DotNetExecutable -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $dotnet) {
        throw "The .NET SDK ('$DotNetExecutable') is required to install the AL Tool. Install the .NET SDK, or pass -CompilerPath to Invoke-BcCompiler / install '$PackageId' manually."
    }

    if ($PSCmdlet.ShouldProcess($PackageId, 'Install AL Tool (dotnet global tool)')) {
        $verb = if ($Force) { 'update' } else { 'install' }
        $installArgs = @('tool', $verb, '--global', $PackageId)
        if ($Version) { $installArgs += @('--version', $Version) }
        # Exit 1 with "already installed" is benign (the tool exists; we just need it on PATH).
        $result = Invoke-ALbuildProcess -FilePath $dotnet.Source -Arguments $installArgs -PassThru -SuccessExitCodes @(0, 1)
        $combined = "$($result.StdOut)`n$($result.StdErr)"
        if (-not $result.Success -and ($combined -notmatch 'already installed|is up to date')) {
            throw "Failed to install the AL Tool ('$PackageId'): $($combined.Trim())"
        }
        Write-ALbuildLog -Level Success "AL Tool '$PackageId' is installed."
    }

    & $ensurePath
    $al = @('al', 'altool', 'alc') | ForEach-Object { Get-Command -Name $_ -ErrorAction SilentlyContinue } | Select-Object -First 1
    if (-not $al) {
        throw "The AL Tool was installed but its command was not found on PATH (expected under '$toolsDir')."
    }
    return [PSCustomObject]@{ Installed = $true; Path = $al.Source; Version = $al.Version }
}